···5050cargo build
5151```
52525353+## Features
5454+5555+### URL Redirects and Rewrites
5656+5757+The hosting service supports Netlify-style `_redirects` files for managing URLs. Place a `_redirects` file in your site root to enable:
5858+5959+- **301/302 Redirects**: Permanent and temporary URL redirects
6060+- **200 Rewrites**: Serve different content without changing the URL
6161+- **404 Custom Pages**: Custom error pages for specific paths
6262+- **Splats & Placeholders**: Dynamic path matching (`/blog/:year/:month/:day`, `/news/*`)
6363+- **Query Parameter Matching**: Redirect based on URL parameters
6464+- **Conditional Redirects**: Route by country, language, or cookie presence
6565+- **Force Redirects**: Override existing files with redirects
6666+6767+Example `_redirects`:
6868+```
6969+# Single-page app routing (React, Vue, etc.)
7070+/* /index.html 200
7171+7272+# Simple redirects
7373+/home /
7474+/old-blog/* /blog/:splat
7575+7676+# API proxy
7777+/api/* https://api.example.com/:splat 200
7878+7979+# Country-based routing
8080+/ /us/ 302 Country=us
8181+/ /uk/ 302 Country=gb
8282+```
8383+5384## Limits
54855586- Max file size: 100MB (PDS limit)
5656-- Max site size: 300MB
5787- Max files: 2000
58885989## Tech Stack
-123
hosting-service/EXAMPLE.md
···11-# HTML Path Rewriting Example
22-33-This document demonstrates how HTML path rewriting works when serving sites via the `/s/:identifier/:site/*` route.
44-55-## Problem
66-77-When you create a static site with absolute paths like `/style.css` or `/images/logo.png`, these paths work fine when served from the root domain. However, when served from a subdirectory like `/s/alice.bsky.social/mysite/`, these absolute paths break because they resolve to the server root instead of the site root.
88-99-## Solution
1010-1111-The hosting service automatically rewrites absolute paths in HTML files to work correctly in the subdirectory context.
1212-1313-## Example
1414-1515-**Original HTML file (index.html):**
1616-```html
1717-<!DOCTYPE html>
1818-<html>
1919-<head>
2020- <meta charset="UTF-8">
2121- <title>My Site</title>
2222- <link rel="stylesheet" href="/style.css">
2323- <link rel="icon" href="/favicon.ico">
2424- <script src="/app.js"></script>
2525-</head>
2626-<body>
2727- <header>
2828- <img src="/images/logo.png" alt="Logo">
2929- <nav>
3030- <a href="/">Home</a>
3131- <a href="/about">About</a>
3232- <a href="/contact">Contact</a>
3333- </nav>
3434- </header>
3535-3636- <main>
3737- <h1>Welcome</h1>
3838- <img src="/images/hero.jpg"
3939- srcset="/images/hero.jpg 1x, /images/hero@2x.jpg 2x"
4040- alt="Hero">
4141-4242- <form action="/submit" method="post">
4343- <input type="text" name="email">
4444- <button>Submit</button>
4545- </form>
4646- </main>
4747-4848- <footer>
4949- <a href="https://example.com">External Link</a>
5050- <a href="#top">Back to Top</a>
5151- </footer>
5252-</body>
5353-</html>
5454-```
5555-5656-**When accessed via `/s/alice.bsky.social/mysite/`, the HTML is rewritten to:**
5757-```html
5858-<!DOCTYPE html>
5959-<html>
6060-<head>
6161- <meta charset="UTF-8">
6262- <title>My Site</title>
6363- <link rel="stylesheet" href="/s/alice.bsky.social/mysite/style.css">
6464- <link rel="icon" href="/s/alice.bsky.social/mysite/favicon.ico">
6565- <script src="/s/alice.bsky.social/mysite/app.js"></script>
6666-</head>
6767-<body>
6868- <header>
6969- <img src="/s/alice.bsky.social/mysite/images/logo.png" alt="Logo">
7070- <nav>
7171- <a href="/s/alice.bsky.social/mysite/">Home</a>
7272- <a href="/s/alice.bsky.social/mysite/about">About</a>
7373- <a href="/s/alice.bsky.social/mysite/contact">Contact</a>
7474- </nav>
7575- </header>
7676-7777- <main>
7878- <h1>Welcome</h1>
7979- <img src="/s/alice.bsky.social/mysite/images/hero.jpg"
8080- srcset="/s/alice.bsky.social/mysite/images/hero.jpg 1x, /s/alice.bsky.social/mysite/images/hero@2x.jpg 2x"
8181- alt="Hero">
8282-8383- <form action="/s/alice.bsky.social/mysite/submit" method="post">
8484- <input type="text" name="email">
8585- <button>Submit</button>
8686- </form>
8787- </main>
8888-8989- <footer>
9090- <a href="https://example.com">External Link</a>
9191- <a href="#top">Back to Top</a>
9292- </footer>
9393-</body>
9494-</html>
9595-```
9696-9797-## What's Preserved
9898-9999-Notice that:
100100-- ✅ Absolute paths are rewritten: `/style.css` → `/s/alice.bsky.social/mysite/style.css`
101101-- ✅ External URLs are preserved: `https://example.com` stays the same
102102-- ✅ Anchors are preserved: `#top` stays the same
103103-- ✅ The rewriting is safe and won't break your site
104104-105105-## Supported Attributes
106106-107107-The rewriter handles these HTML attributes:
108108-- `src` - images, scripts, iframes, videos, audio
109109-- `href` - links, stylesheets
110110-- `action` - forms
111111-- `data` - objects
112112-- `poster` - video posters
113113-- `srcset` - responsive images
114114-115115-## Testing Your Site
116116-117117-To test if your site works with path rewriting:
118118-119119-1. Upload your site to your PDS as a `place.wisp.fs` record
120120-2. Access it via: `https://hosting.wisp.place/s/YOUR_HANDLE/SITE_NAME/`
121121-3. Check that all resources load correctly
122122-123123-If you're using relative paths already (like `./style.css` or `../images/logo.png`), they'll work without any rewriting.
+134
hosting-service/example-_redirects
···11+# Example _redirects file for Wisp hosting
22+# Place this file in the root directory of your site as "_redirects"
33+# Lines starting with # are comments
44+55+# ===================================
66+# SIMPLE REDIRECTS
77+# ===================================
88+99+# Redirect home page
1010+# /home /
1111+1212+# Redirect old URLs to new ones
1313+# /old-blog /blog
1414+# /about-us /about
1515+1616+# ===================================
1717+# SPLAT REDIRECTS (WILDCARDS)
1818+# ===================================
1919+2020+# Redirect entire directories
2121+# /news/* /blog/:splat
2222+# /old-site/* /new-site/:splat
2323+2424+# ===================================
2525+# PLACEHOLDER REDIRECTS
2626+# ===================================
2727+2828+# Restructure blog URLs
2929+# /blog/:year/:month/:day/:slug /posts/:year-:month-:day/:slug
3030+3131+# Capture multiple parameters
3232+# /products/:category/:id /shop/:category/item/:id
3333+3434+# ===================================
3535+# STATUS CODES
3636+# ===================================
3737+3838+# Permanent redirect (301) - default if not specified
3939+# /permanent-move /new-location 301
4040+4141+# Temporary redirect (302)
4242+# /temp-redirect /temp-location 302
4343+4444+# Rewrite (200) - serves different content, URL stays the same
4545+# /api/* /functions/:splat 200
4646+4747+# Custom 404 page
4848+# /shop/* /shop-closed.html 404
4949+5050+# ===================================
5151+# FORCE REDIRECTS
5252+# ===================================
5353+5454+# Force redirect even if file exists (note the ! after status code)
5555+# /override-file /other-file.html 200!
5656+5757+# ===================================
5858+# CONDITIONAL REDIRECTS
5959+# ===================================
6060+6161+# Country-based redirects (ISO 3166-1 alpha-2 codes)
6262+# / /us/ 302 Country=us
6363+# / /uk/ 302 Country=gb
6464+# / /anz/ 302 Country=au,nz
6565+6666+# Language-based redirects
6767+# /products /en/products 301 Language=en
6868+# /products /de/products 301 Language=de
6969+# /products /fr/products 301 Language=fr
7070+7171+# Cookie-based redirects (checks if cookie exists)
7272+# /* /legacy/:splat 200 Cookie=is_legacy
7373+7474+# ===================================
7575+# QUERY PARAMETERS
7676+# ===================================
7777+7878+# Match specific query parameters
7979+# /store id=:id /blog/:id 301
8080+8181+# Multiple parameters
8282+# /search q=:query category=:cat /find/:cat/:query 301
8383+8484+# ===================================
8585+# DOMAIN-LEVEL REDIRECTS
8686+# ===================================
8787+8888+# Redirect to different domain (must include protocol)
8989+# /external https://example.com/path
9090+9191+# Redirect entire subdomain
9292+# http://blog.example.com/* https://example.com/blog/:splat 301!
9393+# https://blog.example.com/* https://example.com/blog/:splat 301!
9494+9595+# ===================================
9696+# COMMON PATTERNS
9797+# ===================================
9898+9999+# Remove .html extensions
100100+# /page.html /page
101101+102102+# Add trailing slash
103103+# /about /about/
104104+105105+# Single-page app fallback (serve index.html for all paths)
106106+# /* /index.html 200
107107+108108+# API proxy
109109+# /api/* https://api.example.com/:splat 200
110110+111111+# ===================================
112112+# CUSTOM ERROR PAGES
113113+# ===================================
114114+115115+# Language-specific 404 pages
116116+# /en/* /en/404.html 404
117117+# /de/* /de/404.html 404
118118+119119+# Section-specific 404 pages
120120+# /shop/* /shop/not-found.html 404
121121+# /blog/* /blog/404.html 404
122122+123123+# ===================================
124124+# NOTES
125125+# ===================================
126126+#
127127+# - Rules are processed in order (first match wins)
128128+# - More specific rules should come before general ones
129129+# - Splats (*) can only be used at the end of a path
130130+# - Query parameters are automatically preserved for 200, 301, 302
131131+# - Trailing slashes are normalized (/ and no / are treated the same)
132132+# - Default status code is 301 if not specified
133133+#
134134+