···11+# Starlight Starter Kit: Basics
22+33+[](https://starlight.astro.build)
44+55+```
66+bun create astro@latest -- --template starlight
77+```
88+99+> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
1010+1111+## 🚀 Project Structure
1212+1313+Inside of your Astro + Starlight project, you'll see the following folders and files:
1414+1515+```
1616+.
1717+├── public/
1818+├── src/
1919+│ ├── assets/
2020+│ ├── content/
2121+│ │ └── docs/
2222+│ └── content.config.ts
2323+├── astro.config.mjs
2424+├── package.json
2525+└── tsconfig.json
2626+```
2727+2828+Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
2929+3030+Images can be added to `src/assets/` and embedded in Markdown with a relative link.
3131+3232+Static assets, like favicons, can be placed in the `public/` directory.
3333+3434+## 🧞 Commands
3535+3636+All commands are run from the root of the project, from a terminal:
3737+3838+| Command | Action |
3939+| :------------------------ | :----------------------------------------------- |
4040+| `bun install` | Installs dependencies |
4141+| `bun dev` | Starts local dev server at `localhost:4321` |
4242+| `bun build` | Build your production site to `./dist/` |
4343+| `bun preview` | Preview your build locally, before deploying |
4444+| `bun astro ...` | Run CLI commands like `astro add`, `astro check` |
4545+| `bun astro -- --help` | Get help using the Astro CLI |
4646+4747+## 👀 Want to learn more?
4848+4949+Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
···11+---
22+title: Wisp CLI 0.2.0 (alpha)
33+description: Command-line tool for deploying static sites to the AT Protocol
44+---
55+66+**Deploy static sites to the AT Protocol**
77+88+The Wisp CLI is a command-line tool for deploying static websites directly to your AT Protocol account. Host your sites on wisp.place with full ownership and control, backed by the decentralized AT Protocol.
99+1010+## Features
1111+1212+- **Deploy**: Push static sites directly from your terminal
1313+- **Pull**: Download sites from the PDS for development or backup
1414+- **Serve**: Run a local server with real-time firehose updates
1515+- **Authenticate** with app password or OAuth
1616+- **Incremental updates**: Only upload changed files
1717+1818+## Downloads
1919+2020+<div class="downloads">
2121+2222+<h2>Download v0.3.0</h2>
2323+2424+<a href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-aarch64-darwin" class="download-link" download="">
2525+2626+<span class="platform">macOS (Apple Silicon):</span> wisp-cli-aarch64-darwin
2727+2828+</a>
2929+3030+<a href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-aarch64-linux" class="download-link" download="">
3131+3232+<span class="platform">Linux (ARM64):</span> wisp-cli-aarch64-linux
3333+3434+</a>
3535+3636+<a href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux" class="download-link" download="">
3737+3838+<span class="platform">Linux (x86_64):</span> wisp-cli-x86_64-linux
3939+4040+</a>
4141+4242+<a href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-windows.exe" class="download-link" download="">
4343+4444+<span class="platform">Windows (x86_64):</span> wisp-cli-x86_64-windows.exe
4545+4646+</a>
4747+4848+<h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">SHA-1 Checksums</h3>
4949+5050+<pre style="font-size: 0.75rem; padding: 1rem;" class="language-bash" tabindex="0"><code class="language-bash">
5151+9281454860f2eb07b39b80f7a9cc8e9bdcff491b wisp-cli-aarch64-darwin
5252+5353+d460863150c4c162b7e7e3801a67746da3aaf9d9 wisp-cli-aarch64-linux
5454+5555+94968abed20422df826b78c38cb506dd4b1b5885 wisp-cli-x86_64-linux
5656+5757+45293e47da38b97ef35258a08cb2682eee64a659 wisp-cli-x86_64-windows.exe
5858+</code></pre>
5959+6060+</div>
6161+6262+## CI/CD Integration
6363+6464+Deploy automatically on every push using Tangled Spindle:
6565+6666+```yaml
6767+when:
6868+ - event: ['push']
6969+ branch: ['main']
7070+ - event: ['manual']
7171+7272+engine: 'nixery'
7373+7474+clone:
7575+ skip: false
7676+ depth: 1
7777+ submodules: false
7878+7979+dependencies:
8080+ nixpkgs:
8181+ - nodejs
8282+ - coreutils
8383+ - curl
8484+ github:NixOS/nixpkgs/nixpkgs-unstable:
8585+ - bun
8686+8787+environment:
8888+ SITE_PATH: 'dist'
8989+ SITE_NAME: 'my-site'
9090+ WISP_HANDLE: 'your-handle.bsky.social'
9191+9292+steps:
9393+ - name: build site
9494+ command: |
9595+ export PATH="$HOME/.nix-profile/bin:$PATH"
9696+9797+ # you may need to regenerate the lockfile due to nixery being weird
9898+ # rm package-lock.json bun.lock
9999+ bun install
100100+101101+ bun run build
102102+103103+ - name: deploy to wisp
104104+ command: |
105105+ # Download Wisp CLI
106106+ curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
107107+ chmod +x wisp-cli
108108+109109+ # Deploy to Wisp
110110+ ./wisp-cli \
111111+ "$WISP_HANDLE" \
112112+ --path "$SITE_PATH" \
113113+ --site "$SITE_NAME" \
114114+ --password "$WISP_APP_PASSWORD"
115115+```
116116+117117+**Note:** Set `WISP_APP_PASSWORD` as a secret in your Tangled Spindle repository settings. Generate an app password from your AT Protocol account settings.
118118+119119+## Basic Usage
120120+121121+### Deploy a Site
122122+123123+```bash
124124+# Download and make executable
125125+curl -O https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-macos-arm64
126126+chmod +x wisp-cli-macos-arm64
127127+128128+# Deploy your site
129129+./wisp-cli-macos-arm64 deploy your-handle.bsky.social \
130130+ --path ./dist \
131131+ --site my-site
132132+```
133133+134134+Your site will be available at: `https://sites.wisp.place/your-handle/my-site`
135135+136136+### Pull a Site from PDS
137137+138138+Download a site from the PDS to your local machine:
139139+140140+```bash
141141+# Pull a site to a specific directory
142142+wisp-cli pull your-handle.bsky.social \
143143+ --site my-site \
144144+ --output ./my-site
145145+146146+# Pull to current directory
147147+wisp-cli pull your-handle.bsky.social \
148148+ --site my-site
149149+```
150150+151151+### Serve a Site Locally with Real-Time Updates
152152+153153+Run a local server that monitors the firehose for real-time updates:
154154+155155+```bash
156156+# Serve on http://localhost:8080 (default)
157157+wisp-cli serve your-handle.bsky.social \
158158+ --site my-site
159159+160160+# Serve on a custom port
161161+wisp-cli serve your-handle.bsky.social \
162162+ --site my-site \
163163+ --port 3000
164164+```
165165+166166+Downloads site, serves it, and watches firehose for live updates!
167167+168168+## Authentication
169169+170170+### OAuth (Recommended)
171171+172172+The CLI uses OAuth by default, opening your browser for secure authentication:
173173+174174+```bash
175175+wisp-cli deploy your-handle.bsky.social --path ./dist --site my-site
176176+```
177177+178178+This creates a session stored locally (default: `/tmp/wisp-oauth-session.json`).
179179+180180+### App Password
181181+182182+For headless environments or CI/CD, use an app password:
183183+184184+```bash
185185+wisp-cli deploy your-handle.bsky.social \
186186+ --path ./dist \
187187+ --site my-site \
188188+ --password YOUR_APP_PASSWORD
189189+```
190190+191191+**Generate app passwords** from your AT Protocol account settings.
192192+193193+## File Processing
194194+195195+The CLI handles all file processing automatically to ensure reliable storage and delivery. Files are compressed with gzip at level 9 for optimal size reduction, then base64 encoded to bypass PDS content sniffing restrictions. Everything is uploaded as `application/octet-stream` blobs while preserving the original MIME type as metadata. When serving your site, the hosting service automatically decompresses non-HTML/CSS/JS files, ensuring your content is delivered correctly to visitors.
196196+197197+## Incremental Updates
198198+199199+The CLI tracks file changes using CID-based content addressing to minimize upload times and bandwidth usage. On your first deploy, all files are uploaded to establish the initial site. For subsequent deploys, the CLI compares content-addressed CIDs to detect which files have actually changed, uploading only those that differ from the previous version. This makes fast iterations possible even for large sites, with deploys completing in seconds when only a few files have changed.
200200+201201+## Limits
202202+203203+- **Max file size**: 100MB per file (after compression)
204204+- **Max total size**: 300MB per site
205205+- **Max files**: 1000 files per site
206206+- **Site name**: Must follow AT Protocol rkey format (alphanumeric, hyphens, underscores)
207207+208208+## Command Reference
209209+210210+### Deploy Command
211211+212212+```bash
213213+wisp-cli deploy [OPTIONS] <INPUT>
214214+215215+Arguments:
216216+ <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL
217217+218218+Options:
219219+ -p, --path <PATH> Path to site directory [default: .]
220220+ -s, --site <SITE> Site name (defaults to directory name)
221221+ --store <STORE> OAuth session file path [default: /tmp/wisp-oauth-session.json]
222222+ --password <PASSWORD> App password for authentication
223223+ -h, --help Print help
224224+```
225225+226226+### Pull Command
227227+228228+```bash
229229+wisp-cli pull [OPTIONS] <INPUT>
230230+231231+Arguments:
232232+ <INPUT> Handle or DID
233233+234234+Options:
235235+ -s, --site <SITE> Site name to download
236236+ -o, --output <OUTPUT> Output directory [default: .]
237237+ -h, --help Print help
238238+```
239239+240240+### Serve Command
241241+242242+```bash
243243+wisp-cli serve [OPTIONS] <INPUT>
244244+245245+Arguments:
246246+ <INPUT> Handle or DID
247247+248248+Options:
249249+ -s, --site <SITE> Site name to serve
250250+ -o, --output <OUTPUT> Site files directory [default: .]
251251+ -p, --port <PORT> Port to serve on [default: 8080]
252252+ -h, --help Print help
253253+```
254254+255255+## Development
256256+257257+The CLI is written in Rust using the Jacquard AT Protocol library. To build from source:
258258+259259+```bash
260260+git clone https://tangled.org/@nekomimi.pet/wisp.place-monorepo
261261+cd cli
262262+cargo build --release
263263+```
264264+265265+Built binaries are available in `target/release/`.
266266+267267+## Related
268268+269269+- [place.wisp.fs](/lexicons/place-wisp-fs) - Site manifest lexicon
270270+- [place.wisp.subfs](/lexicons/place-wisp-subfs) - Subtree records for large sites
271271+- [AT Protocol](https://atproto.com) - The decentralized protocol powering Wisp
+361
docs/src/content/docs/deployment.md
···11+---
22+title: Self-Hosting Guide
33+description: Deploy your own Wisp.place instance
44+---
55+66+This guide covers deploying your own Wisp.place instance. Wisp.place consists of two services: the main backend (handles OAuth, uploads, domains) and the hosting service (serves cached sites).
77+88+## Prerequisites
99+1010+- **PostgreSQL** database (14 or newer)
1111+- **Bun** runtime for the main backend
1212+- **Node.js** (18+) for the hosting service
1313+- **Caddy** (optional, for custom domain TLS)
1414+- **Domain name** for your instance
1515+1616+## Architecture Overview
1717+1818+```
1919+┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
2020+│ Main Backend (port 8000) │ │ Hosting Service (port 3001) │
2121+│ - OAuth authentication │ │ - Firehose listener │
2222+│ - Site upload/management │ │ - Site caching │
2323+│ - Domain registration │ │ - Content serving │
2424+│ - Admin panel │ │ - Redirect handling │
2525+└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
2626+ │ │
2727+ └─────────────────┬───────────────────────────┘
2828+ ▼
2929+ ┌─────────────────────────────────────────┐
3030+ │ PostgreSQL Database │
3131+ │ - User sessions │
3232+ │ - Domain mappings │
3333+ │ - Site metadata │
3434+ └─────────────────────────────────────────┘
3535+```
3636+3737+## Database Setup
3838+3939+Create a PostgreSQL database for Wisp.place:
4040+4141+```bash
4242+createdb wisp
4343+```
4444+4545+The schema is automatically created on first run. Tables include:
4646+- `oauth_states`, `oauth_sessions`, `oauth_keys` - OAuth flow
4747+- `domains` - Wisp subdomains (*.yourdomain.com)
4848+- `custom_domains` - User custom domains with DNS verification
4949+- `sites` - Site metadata cache
5050+- `cookie_secrets` - Session signing keys
5151+5252+## Main Backend Setup
5353+5454+### Environment Variables
5555+5656+Create a `.env` file or set these environment variables:
5757+5858+```bash
5959+# Required
6060+DATABASE_URL="postgres://user:password@localhost:5432/wisp"
6161+BASE_DOMAIN="wisp.place" # Your domain (without protocol)
6262+DOMAIN="https://wisp.place" # Full domain with protocol
6363+CLIENT_NAME="Wisp.place" # OAuth client name
6464+6565+# Optional
6666+NODE_ENV="production" # production or development
6767+PORT="8000" # Default: 8000
6868+```
6969+7070+### Installation
7171+7272+```bash
7373+# Install dependencies
7474+bun install
7575+7676+# Development mode (with hot reload)
7777+bun run dev
7878+7979+# Production mode
8080+bun run start
8181+8282+# Or compile to binary
8383+bun run build
8484+./server
8585+```
8686+8787+The backend will:
8888+1. Initialize the database schema
8989+2. Generate OAuth keys (stored in DB)
9090+3. Start DNS verification worker (checks custom domains every 10 minutes)
9191+4. Listen on port 8000
9292+9393+### First-Time Admin Setup
9494+9595+On first run, you'll be prompted to create an admin account:
9696+9797+```
9898+No admin users found. Create one now? (y/n):
9999+```
100100+101101+Or create manually:
102102+103103+```bash
104104+bun run scripts/create-admin.ts
105105+```
106106+107107+Admin panel is available at `https://yourdomain.com/admin`
108108+109109+## Hosting Service Setup
110110+111111+The hosting service is a separate microservice that serves cached sites.
112112+113113+### Environment Variables
114114+115115+```bash
116116+# Required
117117+DATABASE_URL="postgres://user:password@localhost:5432/wisp"
118118+BASE_HOST="wisp.place" # Same as main backend
119119+120120+# Optional
121121+PORT="3001" # Default: 3001
122122+CACHE_DIR="./cache/sites" # Site cache directory
123123+CACHE_ONLY_MODE="false" # Set true to disable DB writes
124124+```
125125+126126+### Installation
127127+128128+```bash
129129+cd hosting-service
130130+131131+# Install dependencies
132132+npm install
133133+134134+# Development mode
135135+npm run dev
136136+137137+# Production mode
138138+npm run start
139139+140140+# With backfill (downloads all sites from DB on startup)
141141+npm run start -- --backfill
142142+```
143143+144144+The hosting service will:
145145+1. Connect to PostgreSQL
146146+2. Start firehose listener (watches for new sites)
147147+3. Create cache directory
148148+4. Serve sites on port 3001
149149+150150+### Cache Management
151151+152152+Sites are cached to disk at `./cache/sites/{did}/{sitename}/`. The cache is automatically populated:
153153+- **On first request**: Downloads from PDS and caches
154154+- **Via firehose**: Updates when sites are deployed
155155+- **Backfill mode**: Downloads all sites from database on startup
156156+157157+## Reverse Proxy Setup
158158+159159+### Caddy Configuration
160160+161161+Caddy handles TLS, on-demand certificates for custom domains, and routing:
162162+163163+```
164164+{
165165+ on_demand_tls {
166166+ ask http://localhost:8000/api/domain/registered
167167+ }
168168+}
169169+170170+# Wisp subdomains and DNS hash routing
171171+*.dns.wisp.place *.wisp.place {
172172+ reverse_proxy localhost:3001
173173+}
174174+175175+# Main web interface and API
176176+wisp.place {
177177+ reverse_proxy localhost:8000
178178+}
179179+180180+# Custom domains (on-demand TLS)
181181+https:// {
182182+ tls {
183183+ on_demand
184184+ }
185185+ reverse_proxy localhost:3001
186186+}
187187+```
188188+189189+### Nginx Alternative
190190+191191+```nginx
192192+# Main backend
193193+server {
194194+ listen 443 ssl http2;
195195+ server_name wisp.place;
196196+197197+ ssl_certificate /path/to/cert.pem;
198198+ ssl_certificate_key /path/to/key.pem;
199199+200200+ location / {
201201+ proxy_pass http://localhost:8000;
202202+ proxy_set_header Host $host;
203203+ proxy_set_header X-Real-IP $remote_addr;
204204+ }
205205+}
206206+207207+# Hosting service
208208+server {
209209+ listen 443 ssl http2;
210210+ server_name *.wisp.place sites.wisp.place;
211211+212212+ ssl_certificate /path/to/wildcard-cert.pem;
213213+ ssl_certificate_key /path/to/wildcard-key.pem;
214214+215215+ location / {
216216+ proxy_pass http://localhost:3001;
217217+ proxy_set_header Host $host;
218218+ proxy_set_header X-Real-IP $remote_addr;
219219+ }
220220+}
221221+```
222222+223223+**Note:** Custom domain TLS requires dynamic certificate provisioning. Caddy's on-demand TLS is the easiest solution.
224224+225225+## OAuth Configuration
226226+227227+Wisp.place uses AT Protocol OAuth. Your instance needs to be publicly accessible for OAuth callbacks.
228228+229229+Required endpoints:
230230+- `/.well-known/atproto-did` - Returns your DID for lexicon resolution
231231+- `/client-metadata.json` - OAuth client metadata
232232+- `/jwks.json` - OAuth signing keys
233233+234234+These are automatically served by the backend.
235235+236236+## DNS Configuration
237237+238238+For your main domain:
239239+240240+```
241241+wisp.place A YOUR_SERVER_IP
242242+*.wisp.place A YOUR_SERVER_IP
243243+*.dns.wisp.place A YOUR_SERVER_IP
244244+sites.wisp.place A YOUR_SERVER_IP
245245+```
246246+247247+Or use CNAME records if you're behind a CDN:
248248+249249+```
250250+wisp.place CNAME your-server.example.com
251251+*.wisp.place CNAME your-server.example.com
252252+```
253253+254254+## Custom Domain Verification
255255+256256+Users can add custom domains via DNS TXT records:
257257+258258+```
259259+_wisp.example.com TXT did:plc:abc123xyz...
260260+```
261261+262262+The DNS verification worker checks these every 10 minutes. Trigger manually:
263263+264264+```bash
265265+curl -X POST https://yourdomain.com/api/admin/verify-dns
266266+```
267267+268268+## Production Checklist
269269+270270+Before going live:
271271+272272+- [ ] PostgreSQL database configured with backups
273273+- [ ] `DATABASE_URL` set with secure credentials
274274+- [ ] `BASE_DOMAIN` and `DOMAIN` configured correctly
275275+- [ ] Admin account created
276276+- [ ] Reverse proxy (Caddy/Nginx) configured
277277+- [ ] DNS records pointing to your server
278278+- [ ] TLS certificates configured
279279+- [ ] Hosting service cache directory has sufficient space
280280+- [ ] Firewall allows ports 80/443
281281+- [ ] Process manager (systemd, pm2) configured for auto-restart
282282+283283+## Monitoring
284284+285285+### Health Checks
286286+287287+Main backend:
288288+```bash
289289+curl https://yourdomain.com/api/health
290290+```
291291+292292+Hosting service:
293293+```bash
294294+curl http://localhost:3001/health
295295+```
296296+297297+### Logs
298298+299299+The services log to stdout. View with your process manager:
300300+301301+```bash
302302+# systemd
303303+journalctl -u wisp-backend -f
304304+journalctl -u wisp-hosting -f
305305+306306+# pm2
307307+pm2 logs wisp-backend
308308+pm2 logs wisp-hosting
309309+```
310310+311311+### Admin Panel
312312+313313+Access observability metrics at `https://yourdomain.com/admin`:
314314+- Recent logs
315315+- Error tracking
316316+- Performance metrics
317317+- Cache statistics
318318+319319+## Scaling Considerations
320320+321321+- **Multiple hosting instances**: Run multiple hosting services behind a load balancer
322322+- **Separate databases**: Split read/write with replicas
323323+- **CDN**: Put Cloudflare or Bunny in front for global caching
324324+- **Cache storage**: Use NFS/S3 for shared cache across instances
325325+- **Redis**: Add Redis for session storage at scale
326326+327327+## Security Notes
328328+329329+- Use strong cookie secrets (auto-generated and stored in DB)
330330+- Keep dependencies updated: `bun update`, `npm update`
331331+- Enable rate limiting in reverse proxy
332332+- Set up fail2ban for brute force protection
333333+- Regular database backups
334334+- Monitor logs for suspicious activity
335335+336336+## Updates
337337+338338+To update your instance:
339339+340340+```bash
341341+# Pull latest code
342342+git pull
343343+344344+# Update dependencies
345345+bun install
346346+cd hosting-service && npm install && cd ..
347347+348348+# Restart services
349349+# (The database schema updates automatically)
350350+```
351351+352352+## Support
353353+354354+For issues and questions:
355355+- Check the [documentation](https://docs.wisp.place)
356356+- Review [Tangled issues](https://tangled.org/nekomimi.pet/wisp.place-monorepo)
357357+- Join the [Bluesky community](https://bsky.app)
358358+359359+## License
360360+361361+Wisp.place is MIT licensed. You're free to host your own instance and modify it as needed.
+11
docs/src/content/docs/guides/example.md
···11+---
22+title: Example Guide
33+description: A guide in my new Starlight docs site.
44+---
55+66+Guides lead a user through a specific task they want to accomplish, often with a sequence of steps.
77+Writing a good guide requires thinking about what your users are trying to do.
88+99+## Further reading
1010+1111+- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diátaxis framework
+154
docs/src/content/docs/index.mdx
···11+---
22+title: Wisp.place Documentation
33+description: Decentralized static site hosting on the AT Protocol
44+template: doc
55+---
66+77+**Decentralized static site hosting on the AT Protocol.**
88+99+Wisp.place enables you to host static websites directly in your AT Protocol repository. Your Personal Data Server (PDS) holds the cryptographically signed manifest and files as the authoritative source of truth, while hosting services index and serve them with CDN-like performance.
1010+1111+## Quick Start
1212+1313+### Using the Web Interface
1414+Visit [https://wisp.place](https://wisp.place) and sign in with your AT Protocol account to deploy sites through the browser.
1515+1616+### Using the CLI
1717+```bash
1818+# Download the CLI binary for your platform
1919+curl -L https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
2020+chmod +x wisp-cli
2121+2222+# Deploy your site
2323+./wisp-cli your-handle.bsky.social --path ./my-site --site my-site
2424+```
2525+2626+Your site will be available at:
2727+```
2828+https://sites.wisp.place/{your-did}/{site-name}
2929+```
3030+3131+## Key Features
3232+3333+### Decentralized Storage
3434+- Sites stored as `place.wisp.fs` records in your AT Protocol repo
3535+- Cryptographically verifiable ownership
3636+- Your PDS is the source of truth
3737+- Portable across hosting providers
3838+3939+### Custom Domains
4040+- Point your own domain to your Wisp site
4141+- DNS verification ensures secure ownership
4242+- Automatic SSL/TLS certificates
4343+4444+### URL Redirects & Rewrites
4545+- Netlify-style `_redirects` file support
4646+- Single-page app (SPA) routing
4747+- API proxying and conditional routing
4848+- Custom 404 pages
4949+5050+### Efficient Deployment
5151+- **Incremental updates**: Only upload changed files
5252+- **Smart compression**: Automatic gzip for text files
5353+- **Large site support**: Automatic splitting into subfs records for sites with 250+ files to get around 150KB record size limit
5454+- **Blob reuse**: Content-addressed storage prevents duplicate uploads
5555+5656+## How It Works
5757+5858+The deployment process starts when you upload your files. Each file is compressed with gzip, base64-encoded, and uploaded as a blob to your PDS. A `place.wisp.fs` record then stores the complete site structure with references to these blobs, creating a verifiable manifest of your site.
5959+6060+Hosting services continuously watch the AT Protocol firehose for new and updated sites. When your site is first accessed or updated, the hosting service downloads the manifest and blobs, caching them locally for optimized delivery. Custom domains work through DNS verification, allowing your site to be served from your own domain while maintaining the cryptographic guarantees of the AT Protocol.
6161+6262+## Architecture Overview
6363+6464+```
6565+ ┌──────────────┐ ┌──────────────┐
6666+ │ wisp-cli │ │ wisp.place │
6767+ │ (Rust Binary)│ │ Website │
6868+ │ │ │ (React UI) │
6969+ └──────────────┘ └──────────────┘
7070+ │ │
7171+ │ │
7272+ ▼ ▼
7373+┌─────────────────────────────────────────────────────────┐
7474+│ AT Protocol PDS │
7575+│ (Authoritative Source - Cryptographically Signed) │
7676+│ │
7777+│ ┌──────────────────────────────────────────────┐ │
7878+│ │ place.wisp.fs record │ │
7979+│ │ - Site manifest (directory tree) │ │
8080+│ │ - Blob references (CID-based) │ │
8181+│ │ - Metadata (file count, timestamps) │ │
8282+│ └──────────────────────────────────────────────┘ │
8383+│ │
8484+│ ┌──────────────────────────────────────────────┐ │
8585+│ │ Blobs (gzipped + base64 encoded) │ │
8686+│ │ - index.html │ │
8787+│ │ - styles.css │ │
8888+│ │ - assets/* │ │
8989+│ └──────────────────────────────────────────────┘ │
9090+└─────────────────────────────────────────────────────────┘
9191+ │
9292+ ▼
9393+ ┌─────────────────────────────────┐
9494+ │ AT Protocol Firehose │
9595+ │ (Jetstream WebSocket Stream) │
9696+ └─────────────────────────────────┘
9797+ │
9898+ ▼
9999+┌─────────────────────────────────────────────────────────┐
100100+│ Wisp Hosting Service │
101101+│ │
102102+│ ┌──────────────────────────────────────────────┐ │
103103+│ │ Cache (Disk + In-Memory) │ │
104104+│ │ - Downloads sites on first access │ │
105105+│ │ - Auto-updates on firehose events │ │
106106+│ │ - LRU eviction for memory limits │ │
107107+│ └──────────────────────────────────────────────┘ │
108108+│ │
109109+│ ┌──────────────────────────────────────────────┐ │
110110+│ │ Routing & Serving │ │
111111+│ │ - Custom domains (example.com) │ │
112112+│ │ - Subdomains (alice.wisp.place) │ │
113113+│ │ - Direct URLs (sites.wisp.place/did/site) │ │
114114+│ └──────────────────────────────────────────────┘ │
115115+└─────────────────────────────────────────────────────────┘
116116+ │
117117+ ▼
118118+ ┌─────────────┐
119119+ │ Browser │
120120+ └─────────────┘
121121+```
122122+123123+## Tech Stack
124124+125125+- **Backend**: Bun + Elysia + PostgreSQL
126126+- **Frontend**: React 19 + Tailwind 4 + Radix UI
127127+- **Hosting**: Node.js + Hono
128128+- **CLI**: Rust + Jacquard (AT Protocol library)
129129+- **Protocol**: AT Protocol OAuth + custom lexicons
130130+131131+## Limits
132132+133133+- **Max file size**: 100MB per file (PDS limit)
134134+- **Max files**: 1000 files per site
135135+- **Max total size**: 300MB per site (compressed)
136136+137137+Files are automatically compressed with gzip before upload, so actual limits may be higher depending on your content compressibility.
138138+139139+## Getting Started
140140+141141+- [CLI Documentation](/cli) - Deploy sites from the command line
142142+- [Deployment Guide](/deployment) - Configure domains, redirects, and hosting
143143+- [Lexicons](/lexicons) - AT Protocol record schemas and data structures
144144+145145+## Links
146146+147147+- **Website**: [https://wisp.place](https://wisp.place)
148148+- **Repository**: [https://tangled.org/@nekomimi.pet/wisp.place-monorepo](https://tangled.org/@nekomimi.pet/wisp.place-monorepo)
149149+- **AT Protocol**: [https://atproto.com](https://atproto.com)
150150+- **Jacquard Library**: [https://tangled.org/@nonbinary.computer/jacquard](https://tangled.org/@nonbinary.computer/jacquard)
151151+152152+## License
153153+154154+MIT License - See the repository for details.
+94
docs/src/content/docs/lexicons/index.md
···11+---
22+title: Lexicon Reference
33+description: AT Protocol lexicons used by Wisp.place
44+---
55+66+Wisp.place uses custom AT Protocol lexicons to store and manage static site data. These lexicons define the structure of records stored in your PDS.
77+88+## Available Lexicons
99+1010+### [place.wisp.fs](/lexicons/place-wisp-fs)
1111+The main lexicon for storing static site manifests. Contains the directory tree structure with references to file blobs.
1212+1313+### [place.wisp.subfs](/lexicons/place-wisp-subfs)
1414+Subtree lexicon for splitting large sites across multiple records. Entries from subfs records are merged (flattened) into the parent directory.
1515+1616+### [place.wisp.domain](/lexicons/place-wisp-domain)
1717+Domain registration record for claiming wisp.place subdomains.
1818+1919+## How Lexicons Work
2020+2121+### Storage Model
2222+2323+Sites are stored as `place.wisp.fs` records in your AT Protocol repository:
2424+2525+```
2626+at://did:plc:abc123/place.wisp.fs/my-site
2727+```
2828+2929+Each record contains:
3030+- **Site metadata** (name, file count, timestamps)
3131+- **Directory tree** (hierarchical structure)
3232+- **Blob references** (content-addressed file storage)
3333+3434+### File Processing
3535+3636+1. Files are **gzipped** for compression
3737+2. Text files are **base64 encoded** to bypass PDS content sniffing
3838+3. Uploaded as blobs with `application/octet-stream` MIME type
3939+4. Original MIME type stored in manifest metadata
4040+4141+### Large Site Splitting
4242+4343+Sites with 250+ files are automatically split:
4444+4545+1. Large directories are extracted into `place.wisp.subfs` records
4646+2. Main manifest references subfs records via AT-URI
4747+3. Hosting services merge (flatten) subfs entries when serving
4848+4. Keeps manifest size under 150KB PDS limit
4949+5050+## Example Record Structure
5151+5252+```json
5353+{
5454+ "$type": "place.wisp.fs",
5555+ "site": "my-site",
5656+ "root": {
5757+ "type": "directory",
5858+ "entries": [
5959+ {
6060+ "name": "index.html",
6161+ "node": {
6262+ "type": "file",
6363+ "blob": {
6464+ "$type": "blob",
6565+ "ref": { "$link": "bafyreiabc123..." },
6666+ "mimeType": "application/octet-stream",
6767+ "size": 12345
6868+ },
6969+ "encoding": "gzip",
7070+ "mimeType": "text/html",
7171+ "base64": true
7272+ }
7373+ },
7474+ {
7575+ "name": "assets",
7676+ "node": {
7777+ "type": "directory",
7878+ "entries": [...]
7979+ }
8080+ }
8181+ ]
8282+ },
8383+ "fileCount": 42,
8484+ "createdAt": "2024-01-15T10:30:00Z"
8585+}
8686+```
8787+8888+## Learn More
8989+9090+- [place.wisp.fs Reference](/lexicons/place-wisp-fs)
9191+- [place.wisp.subfs Reference](/lexicons/place-wisp-subfs)
9292+- [place.wisp.domain Reference](/lexicons/place-wisp-domain)
9393+- [AT Protocol Lexicons](https://atproto.com/specs/lexicon)
9494+
···11+---
22+title: place.wisp.domain
33+description: Reference for the place.wisp.domain lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Overview
99+1010+The `place.wisp.domain` lexicon defines **metadata records for wisp.place subdomains**,
1111+such as `alice.wisp.place` or `miku-fan.wisp.place`.
1212+1313+- **What lives in the PDS:** a small record that says “this DID claimed this domain at this time”.
1414+- **What is authoritative:** the PostgreSQL `domains` table on the wisp.place backend
1515+ (routing and availability checks use the DB, not this record).
1616+1717+Use this page as a schema reference; routing and TLS details are covered elsewhere.
1818+1919+---
2020+2121+## Record: `main`
2222+2323+<a name="main"></a>
2424+2525+### `main` (record)
2626+2727+**Type:** `record`
2828+2929+**Description:** Metadata record for a claimed wisp.place subdomain.
3030+3131+**Properties:**
3232+3333+| Name | Type | Req'd | Description | Constraints |
3434+| ----------- | -------- | ----- | --------------------------------------------- | ------------------ |
3535+| `domain` | `string` | ✅ | Full domain name, e.g. `alice.wisp.place` | |
3636+| `createdAt` | `string` | ✅ | When the domain was claimed | Format: `datetime` |
3737+3838+---
3939+4040+## Claim Flow & `rkey`
4141+4242+### Subdomain claiming (high‑level)
4343+4444+When a user claims `handle.wisp.place`:
4545+4646+1. **User authenticates** via OAuth (proves DID control).
4747+2. **Handle is validated**:
4848+ - 3–63 characters
4949+ - `a-z`, `0-9`, `-` only
5050+ - Does not start/end with `-`
5151+ - Not in the reserved set (`www`, `api`, `admin`, `static`, `public`, `preview`, …)
5252+3. **Domain limit enforced:** max 3 wisp.place subdomains per DID.
5353+4. **Database row created** in `domains`:
5454+5555+```sql
5656+INSERT INTO domains (domain, did, rkey)
5757+VALUES ('handle.wisp.place', did, NULL);
5858+```
5959+6060+5. **PDS record written** in `place.wisp.domain` as metadata.
6161+6262+### Record key (`rkey`)
6363+6464+The **record key is the normalized handle** (the subdomain label):
6565+6666+```text
6767+at://did:plc:abc123/place.wisp.domain/wisp
6868+```
6969+7070+If a DID claims multiple subdomains, it will have multiple records:
7171+7272+- `at://did:plc:abc123/place.wisp.domain/wisp`
7373+- `at://did:plc:abc123/place.wisp.domain/miku-fan`
7474+7575+---
7676+7777+## Examples
7878+7979+### Basic domain record
8080+8181+```json
8282+{
8383+ "$type": "place.wisp.domain",
8484+ "domain": "alice.wisp.place",
8585+ "createdAt": "2024-01-15T10:30:00.000Z"
8686+}
8787+```
8888+8989+### URI structure
9090+9191+Complete AT-URI for a domain record:
9292+9393+```text
9494+at://did:plc:7puq73yz2hkvbcpdhnsze2qw/place.wisp.domain/wisp
9595+```
9696+9797+Breakdown:
9898+9999+- **`did:plc:7puq73yz2hkvbcpdhnsze2qw`** – User DID
100100+- **`place.wisp.domain`** – Collection ID
101101+- **`wisp`** – Record key (subdomain handle)
102102+103103+---
104104+105105+## Domain rules (summary)
106106+107107+- **Length:** 3–64 characters
108108+- **Characters:** `a-z`, `0-9`, and `-`
109109+- **Shape:** must start and end with alphanumeric
110110+- **Case:** stored/compared in lowercase
111111+- **Limit:** up to **3** wisp.place subdomains per DID
112112+- **Uniqueness:** each `*.wisp.place` can only be owned by one DID at a time.
113113+114114+Valid examples:
115115+116116+- `alice` → `alice.wisp.place`
117117+- `my-site` → `my-site.wisp.place`
118118+- `dev2024` → `dev2024.wisp.place`
119119+120120+Invalid examples:
121121+122122+- `ab` (too short)
123123+- `-alice` / `alice-` (leading or trailing hyphen)
124124+- `alice.bob` (dot)
125125+- `alice_bob` (underscore)
126126+127127+---
128128+129129+## Database & routing
130130+131131+The **lexicon record is not used for routing**. All real decisions use the DB:
132132+133133+```sql
134134+CREATE TABLE domains (
135135+ domain TEXT PRIMARY KEY, -- "alice.wisp.place"
136136+ did TEXT NOT NULL, -- User DID
137137+ rkey TEXT, -- Site rkey (place.wisp.fs)
138138+ created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())
139139+);
140140+141141+CREATE INDEX domains_did_rkey ON domains (did, rkey);
142142+```
143143+144144+The `domains` table powers:
145145+146146+- **Availability checks** (`/api/domain/check`)
147147+- **Mapping** from hostname → `(did, rkey)` for the hosting service
148148+- **Safety:** you must explicitly change/delete routing in the DB, avoiding accidental takeovers.
149149+150150+The `place.wisp.domain` PDS record is there for:
151151+152152+- **Audit trail** – who claimed what, when
153153+- **User-visible history** in their repo
154154+- **Optional cross‑checking** against the DB if you care to.
155155+156156+---
157157+158158+## Related
159159+160160+- [place.wisp.fs](/lexicons/place-wisp-fs) – Site manifest lexicon
161161+- Custom domains / DNS verification – covered in separate routing/hosting docs
162162+- [AT Protocol Lexicons](https://atproto.com/specs/lexicon)
163163+164164+## Additional commentary
165165+166166+For a detailed write‑up of the full domain system (subdomains, custom domains, DNS TXT/CNAME flow, Caddy on‑demand TLS, and hosting routing), see
167167+**[How wisp.place maps domains to DIDs](https://nekomimi.leaflet.pub/3m5fy2jkurk2a)**.
168168+169169+
+299
docs/src/content/docs/lexicons/place-wisp-fs.md
···11+---
22+title: place.wisp.fs
33+description: Reference for the place.wisp.fs lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Overview
99+1010+The `place.wisp.fs` lexicon defines the structure for storing static site manifests in AT Protocol repositories. Each record represents a complete website with its directory structure and file references.
1111+1212+## Record Structure
1313+1414+<a name="main"></a>
1515+1616+### `main` (Record)
1717+1818+**Type:** `record`
1919+2020+**Description:** Virtual filesystem manifest for a Wisp site
2121+2222+**Properties:**
2323+2424+| Name | Type | Req'd | Description | Constraints |
2525+| ----------- | --------------------------- | ----- | ------------------------------------ | ------------------ |
2626+| `site` | `string` | ✅ | Site name (used as record key) | |
2727+| `root` | [`#directory`](#directory) | ✅ | Root directory of the site | |
2828+| `fileCount` | `integer` | | Total number of files in the site | Min: 0, Max: 1000 |
2929+| `createdAt` | `string` | ✅ | Timestamp of site creation/update | Format: `datetime` |
3030+3131+---
3232+3333+<a name="entry"></a>
3434+3535+### `entry`
3636+3737+**Type:** `object`
3838+3939+**Description:** Named entry in a directory (file, directory, or subfs)
4040+4141+**Properties:**
4242+4343+| Name | Type | Req'd | Description | Constraints |
4444+| ------ | ------------------------------------------------------- | ----- | ----------------------------------- | ----------- |
4545+| `name` | `string` | ✅ | File or directory name | Max: 255 chars |
4646+| `node` | Union of [`#file`](#file), [`#directory`](#directory), [`#subfs`](#subfs) | ✅ | The node (file, directory, or subfs reference) | |
4747+4848+---
4949+5050+## Type Definitions
5151+5252+<a name="file"></a>
5353+5454+### `file`
5555+5656+**Type:** `object`
5757+5858+**Description:** Represents a file node in the directory tree
5959+6060+**Properties:**
6161+6262+| Name | Type | Req'd | Description | Constraints |
6363+| ---------- | --------- | ----- | ------------------------------------------------------------ | ---------------------- |
6464+| `type` | `string` | ✅ | Node type identifier | Const: `"file"` |
6565+| `blob` | `blob` | ✅ | Content blob reference | Max size: 1000000000 (1GB) |
6666+| `encoding` | `string` | | Content encoding (e.g., gzip for compressed files) | Enum: `["gzip"]` |
6767+| `mimeType` | `string` | | Original MIME type before compression | |
6868+| `base64` | `boolean` | | True if blob content is base64-encoded (bypasses PDS sniffing) | |
6969+7070+**Notes:**
7171+- Files are typically gzip compressed before upload
7272+- Text files (HTML/CSS/JS) are also base64 encoded to prevent PDS content-type sniffing
7373+- The blob is uploaded with MIME type `application/octet-stream`
7474+- Original MIME type is preserved in the `mimeType` field
7575+7676+---
7777+7878+<a name="directory"></a>
7979+8080+### `directory`
8181+8282+**Type:** `object`
8383+8484+**Description:** Represents a directory node in the file tree
8585+8686+**Properties:**
8787+8888+| Name | Type | Req'd | Description | Constraints |
8989+| --------- | -------------------------- | ----- | ------------------------------ | ----------- |
9090+| `type` | `string` | ✅ | Node type identifier | Const: `"directory"` |
9191+| `entries` | Array of [`#entry`](#entry) | ✅ | Child entries in this directory | Max: 500 entries |
9292+9393+**Notes:**
9494+- Directories can contain files, subdirectories, or subfs references
9595+- Maximum 500 entries per directory to stay within record size limits
9696+9797+<a name="subfs"></a>
9898+9999+### `subfs`
100100+101101+**Type:** `object`
102102+103103+**Description:** Reference to a `place.wisp.subfs` record for splitting large directories
104104+105105+**Properties:**
106106+107107+| Name | Type | Req'd | Description | Constraints |
108108+| --------- | -------- | ----- | --------------------------------------------------------------------------- | ---------------- |
109109+| `type` | `string` | ✅ | Node type identifier | Const: `"subfs"` |
110110+| `subject` | `string` | ✅ | AT-URI pointing to a place.wisp.subfs record containing this subtree | Format: `at-uri` |
111111+| `flat` | `boolean` | | Controls merging behavior (default: true) | |
112112+113113+**Notes:**
114114+- When `flat` is true (default), the subfs record's root entries are **merged (flattened)** into the parent directory
115115+- When `flat` is false, the subfs entries are placed in a subdirectory with the subfs entry's name
116116+- The `flat` property controls whether the subfs acts as a content merge or directory replacement
117117+- Allows splitting large directories across multiple records while optionally maintaining flat or nested structure
118118+- Used automatically when sites exceed 250 files or 140KB manifest size
119119+120120+---
121121+122122+## Usage Examples
123123+124124+### Simple Site
125125+126126+```json
127127+{
128128+ "$type": "place.wisp.fs",
129129+ "site": "my-blog",
130130+ "root": {
131131+ "type": "directory",
132132+ "entries": [
133133+ {
134134+ "name": "index.html",
135135+ "node": {
136136+ "type": "file",
137137+ "blob": {
138138+ "$type": "blob",
139139+ "ref": { "$link": "bafyreiabc..." },
140140+ "mimeType": "application/octet-stream",
141141+ "size": 4521
142142+ },
143143+ "encoding": "gzip",
144144+ "mimeType": "text/html",
145145+ "base64": true
146146+ }
147147+ },
148148+ {
149149+ "name": "style.css",
150150+ "node": {
151151+ "type": "file",
152152+ "blob": {
153153+ "$type": "blob",
154154+ "ref": { "$link": "bafyreidef..." },
155155+ "mimeType": "application/octet-stream",
156156+ "size": 2134
157157+ },
158158+ "encoding": "gzip",
159159+ "mimeType": "text/css",
160160+ "base64": true
161161+ }
162162+ }
163163+ ]
164164+ },
165165+ "fileCount": 2,
166166+ "createdAt": "2024-01-15T10:30:00.000Z"
167167+}
168168+```
169169+170170+### Site with Subdirectory
171171+172172+```json
173173+{
174174+ "$type": "place.wisp.fs",
175175+ "site": "portfolio",
176176+ "root": {
177177+ "type": "directory",
178178+ "entries": [
179179+ {
180180+ "name": "index.html",
181181+ "node": { "type": "file", "blob": {...}, "encoding": "gzip", "mimeType": "text/html", "base64": true }
182182+ },
183183+ {
184184+ "name": "assets",
185185+ "node": {
186186+ "type": "directory",
187187+ "entries": [
188188+ {
189189+ "name": "logo.png",
190190+ "node": { "type": "file", "blob": {...}, "encoding": "gzip", "mimeType": "image/png", "base64": false }
191191+ }
192192+ ]
193193+ }
194194+ }
195195+ ]
196196+ },
197197+ "fileCount": 2,
198198+ "createdAt": "2024-01-15T10:30:00.000Z"
199199+}
200200+```
201201+202202+### Large Site with Subfs
203203+204204+```json
205205+{
206206+ "$type": "place.wisp.fs",
207207+ "site": "documentation",
208208+ "root": {
209209+ "type": "directory",
210210+ "entries": [
211211+ {
212212+ "name": "index.html",
213213+ "node": { "type": "file", "blob": {...}, "encoding": "gzip", "mimeType": "text/html", "base64": true }
214214+ },
215215+ {
216216+ "name": "docs",
217217+ "node": {
218218+ "type": "subfs",
219219+ "subject": "at://did:plc:abc123/place.wisp.subfs/3kl2jd9s8f7g",
220220+ "flat": true
221221+ }
222222+ }
223223+ ]
224224+ },
225225+ "fileCount": 150,
226226+ "createdAt": "2024-01-15T10:30:00.000Z"
227227+}
228228+```
229229+230230+## Lexicon Source
231231+232232+```json
233233+{
234234+ "lexicon": 1,
235235+ "id": "place.wisp.fs",
236236+ "defs": {
237237+ "main": {
238238+ "type": "record",
239239+ "description": "Virtual filesystem manifest for a Wisp site",
240240+ "record": {
241241+ "type": "object",
242242+ "required": ["site", "root", "createdAt"],
243243+ "properties": {
244244+ "site": { "type": "string" },
245245+ "root": { "type": "ref", "ref": "#directory" },
246246+ "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
247247+ "createdAt": { "type": "string", "format": "datetime" }
248248+ }
249249+ }
250250+ },
251251+ "file": {
252252+ "type": "object",
253253+ "required": ["type", "blob"],
254254+ "properties": {
255255+ "type": { "type": "string", "const": "file" },
256256+ "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
257257+ "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
258258+ "mimeType": { "type": "string", "description": "Original MIME type before compression" },
259259+ "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" }
260260+ }
261261+ },
262262+ "directory": {
263263+ "type": "object",
264264+ "required": ["type", "entries"],
265265+ "properties": {
266266+ "type": { "type": "string", "const": "directory" },
267267+ "entries": {
268268+ "type": "array",
269269+ "maxLength": 500,
270270+ "items": { "type": "ref", "ref": "#entry" }
271271+ }
272272+ }
273273+ },
274274+ "entry": {
275275+ "type": "object",
276276+ "required": ["name", "node"],
277277+ "properties": {
278278+ "name": { "type": "string", "maxLength": 255 },
279279+ "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
280280+ }
281281+ },
282282+ "subfs": {
283283+ "type": "object",
284284+ "required": ["type", "subject"],
285285+ "properties": {
286286+ "type": { "type": "string", "const": "subfs" },
287287+ "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." },
288288+ "flat": { "type": "boolean", "description": "If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." }
289289+ }
290290+ }
291291+ }
292292+}
293293+```
294294+295295+## Related
296296+297297+- [place.wisp.subfs](/lexicons/place-wisp-subfs) - Subtree records for large sites
298298+- [AT Protocol Lexicons](https://atproto.com/specs/lexicon)
299299+
···11+---
22+title: place.wisp.subfs
33+description: Reference for the place.wisp.subfs lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Overview
99+1010+The `place.wisp.subfs` lexicon defines subtree records for splitting large sites across multiple AT Protocol records. When a site exceeds size limits (250+ files or 140KB manifest), large directories are extracted into separate `place.wisp.subfs` records.
1111+1212+**Key Feature:** Subfs entries are referenced from `place.wisp.fs` records and can be either **merged (flattened)** into the parent directory or placed as a subdirectory, depending on the `flat` property in the parent `place.wisp.fs` record.
1313+1414+## Record Structure
1515+1616+<a name="main"></a>
1717+1818+### `main` (Record)
1919+2020+**Type:** `record`
2121+2222+**Description:** Virtual filesystem subtree referenced by place.wisp.fs records. How this subtree is integrated depends on the `flat` property in the referencing subfs entry.
2323+2424+**Properties:**
2525+2626+| Name | Type | Req'd | Description | Constraints |
2727+| ----------- | -------------------------- | ----- | ----------------------------------------- | ------------------ |
2828+| `root` | [`#directory`](#directory) | ✅ | Root directory containing subtree entries | |
2929+| `fileCount` | `integer` | | Number of files in this subtree | Min: 0, Max: 1000 |
3030+| `createdAt` | `string` | ✅ | Timestamp of subtree creation | Format: `datetime` |
3131+3232+---
3333+3434+## Type Definitions
3535+3636+<a name="file"></a>
3737+3838+### `file`
3939+4040+**Type:** `object`
4141+4242+**Description:** Represents a file node in the directory tree
4343+4444+**Properties:**
4545+4646+| Name | Type | Req'd | Description | Constraints |
4747+| ---------- | --------- | ----- | ------------------------------------------------------------ | ------------------------- |
4848+| `type` | `string` | ✅ | Node type identifier | Const: `"file"` |
4949+| `blob` | `blob` | ✅ | Content blob reference | Max size: 1000000000 (1GB) |
5050+| `encoding` | `string` | | Content encoding (e.g., gzip for compressed files) | Enum: `["gzip"]` |
5151+| `mimeType` | `string` | | Original MIME type before compression | |
5252+| `base64` | `boolean` | | True if blob content is base64-encoded (bypasses PDS sniffing) | |
5353+5454+---
5555+5656+<a name="directory"></a>
5757+5858+### `directory`
5959+6060+**Type:** `object`
6161+6262+**Description:** Represents a directory node in the file tree
6363+6464+**Properties:**
6565+6666+| Name | Type | Req'd | Description | Constraints |
6767+| --------- | -------------------------- | ----- | ------------------------------ | -------------------- |
6868+| `type` | `string` | ✅ | Node type identifier | Const: `"directory"` |
6969+| `entries` | Array of [`#entry`](#entry) | ✅ | Child entries in this directory | Max: 500 entries |
7070+7171+---
7272+7373+<a name="entry"></a>
7474+7575+### `entry`
7676+7777+**Type:** `object`
7878+7979+**Description:** Named entry in a directory (file, directory, or nested subfs)
8080+8181+**Properties:**
8282+8383+| Name | Type | Req'd | Description | Constraints |
8484+| ------ | ------------------------------------------------------- | ----- | ----------------------------------- | ----------- |
8585+| `name` | `string` | ✅ | File or directory name | Max: 255 chars |
8686+| `node` | Union of [`#file`](#file), [`#directory`](#directory), [`#subfs`](#subfs) | ✅ | The node (file, directory, or subfs reference) | |
8787+8888+---
8989+9090+<a name="subfs"></a>
9191+9292+### `subfs`
9393+9494+**Type:** `object`
9595+9696+**Description:** Reference to another `place.wisp.subfs` record for nested subtrees. When expanded, entries are merged (flattened) into the parent directory by default, unless the parent `place.wisp.fs` record specifies `flat: false`.
9797+9898+**Properties:**
9999+100100+| Name | Type | Req'd | Description | Constraints |
101101+| --------- | -------- | ----- | --------------------------------------------------------------------------- | ---------------- |
102102+| `type` | `string` | ✅ | Node type identifier | Const: `"subfs"` |
103103+| `subject` | `string` | ✅ | AT-URI pointing to another place.wisp.subfs record for nested subtrees | Format: `at-uri` |
104104+105105+**Notes:**
106106+- Subfs records can reference other subfs records recursively
107107+- When expanded, entries are merged (flattened) into the parent directory by default
108108+- The `flat` property in the parent `place.wisp.fs` record controls integration behavior
109109+- Allows splitting very large directory structures
110110+111111+---
112112+113113+## How Subfs Merging Works
114114+115115+### Before Expansion
116116+117117+Main record (`place.wisp.fs`):
118118+```json
119119+{
120120+ "root": {
121121+ "type": "directory",
122122+ "entries": [
123123+ { "name": "index.html", "node": { "type": "file", ... } },
124124+ { "name": "docs", "node": { "type": "subfs", "subject": "at://did:plc:abc/place.wisp.subfs/xyz" } }
125125+ ]
126126+ }
127127+}
128128+```
129129+130130+Referenced subfs record (`at://did:plc:abc/place.wisp.subfs/xyz`):
131131+```json
132132+{
133133+ "root": {
134134+ "type": "directory",
135135+ "entries": [
136136+ { "name": "guide.html", "node": { "type": "file", ... } },
137137+ { "name": "api.html", "node": { "type": "file", ... } }
138138+ ]
139139+ }
140140+}
141141+```
142142+143143+### After Expansion (What Hosting Service Sees)
144144+145145+**With `flat: true` (default):**
146146+147147+```json
148148+{
149149+ "root": {
150150+ "type": "directory",
151151+ "entries": [
152152+ { "name": "index.html", "node": { "type": "file", ... } },
153153+ { "name": "guide.html", "node": { "type": "file", ... } },
154154+ { "name": "api.html", "node": { "type": "file", ... } }
155155+ ]
156156+ }
157157+}
158158+```
159159+160160+The subfs entries are merged directly into the parent directory.
161161+162162+**With `flat: false`:**
163163+164164+```json
165165+{
166166+ "root": {
167167+ "type": "directory",
168168+ "entries": [
169169+ { "name": "index.html", "node": { "type": "file", ... } },
170170+ { "name": "docs", "node": {
171171+ "type": "directory",
172172+ "entries": [
173173+ { "name": "guide.html", "node": { "type": "file", ... } },
174174+ { "name": "api.html", "node": { "type": "file", ... } }
175175+ ]
176176+ }}
177177+ ]
178178+ }
179179+}
180180+```
181181+182182+The subfs entries are placed in a subdirectory named "docs".
183183+184184+---
185185+186186+## Usage Examples
187187+188188+### Basic Subfs Record
189189+190190+```json
191191+{
192192+ "$type": "place.wisp.subfs",
193193+ "root": {
194194+ "type": "directory",
195195+ "entries": [
196196+ {
197197+ "name": "chapter1.html",
198198+ "node": {
199199+ "type": "file",
200200+ "blob": {
201201+ "$type": "blob",
202202+ "ref": { "$link": "bafyreiabc..." },
203203+ "mimeType": "application/octet-stream",
204204+ "size": 8421
205205+ },
206206+ "encoding": "gzip",
207207+ "mimeType": "text/html",
208208+ "base64": true
209209+ }
210210+ },
211211+ {
212212+ "name": "chapter2.html",
213213+ "node": {
214214+ "type": "file",
215215+ "blob": {
216216+ "$type": "blob",
217217+ "ref": { "$link": "bafyreidef..." },
218218+ "mimeType": "application/octet-stream",
219219+ "size": 9234
220220+ },
221221+ "encoding": "gzip",
222222+ "mimeType": "text/html",
223223+ "base64": true
224224+ }
225225+ }
226226+ ]
227227+ },
228228+ "fileCount": 2,
229229+ "createdAt": "2024-01-15T10:30:00.000Z"
230230+}
231231+```
232232+233233+### Nested Subfs (Recursive)
234234+235235+A subfs record can reference another subfs record:
236236+237237+```json
238238+{
239239+ "$type": "place.wisp.subfs",
240240+ "root": {
241241+ "type": "directory",
242242+ "entries": [
243243+ {
244244+ "name": "section-a.html",
245245+ "node": { "type": "file", "blob": {...}, "encoding": "gzip", "mimeType": "text/html", "base64": true }
246246+ },
247247+ {
248248+ "name": "subsection",
249249+ "node": {
250250+ "type": "subfs",
251251+ "subject": "at://did:plc:abc123/place.wisp.subfs/nested123"
252252+ }
253253+ }
254254+ ]
255255+ },
256256+ "fileCount": 50,
257257+ "createdAt": "2024-01-15T10:30:00.000Z"
258258+}
259259+```
260260+261261+---
262262+263263+## When Are Subfs Records Created?
264264+265265+The Wisp CLI and web interface automatically create subfs records when:
266266+267267+1. **File count threshold**: Site has 250+ files (keeps main manifest under 200 files)
268268+2. **Size threshold**: Main manifest exceeds 140KB (PDS limit is 150KB)
269269+3. **Large directories**: Individual directories with many files
270270+271271+### Splitting Algorithm
272272+273273+The `flat` property in the parent `place.wisp.fs` record controls integration behavior:
274274+- `flat: true` (default): Merge subfs entries directly into parent directory
275275+- `flat: false`: Create subdirectory with the subfs entry's name
276276+277277+---
278278+279279+## Best Practices
280280+281281+### For Hosting Services
282282+283283+- **Fetch recursively**: Load all subfs records referenced in the tree
284284+- **Merge entries**: Replace subfs nodes with directory nodes containing referenced entries
285285+- **Cache merged tree**: Store the fully expanded tree for serving
286286+- **Update on firehose**: Re-fetch and re-merge when subfs records change
287287+288288+### For Upload Tools
289289+290290+- **Reuse subfs records**: Check existing subfs URIs before creating new ones
291291+- **Clean up old records**: Delete unused subfs records after updates
292292+- **Maintain file paths**: Preserve original directory structure when extracting to subfs
293293+294294+---
295295+296296+## Lexicon Source
297297+298298+```json
299299+{
300300+ "lexicon": 1,
301301+ "id": "place.wisp.subfs",
302302+ "defs": {
303303+ "main": {
304304+ "type": "record",
305305+ "description": "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.",
306306+ "record": {
307307+ "type": "object",
308308+ "required": ["root", "createdAt"],
309309+ "properties": {
310310+ "root": { "type": "ref", "ref": "#directory" },
311311+ "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
312312+ "createdAt": { "type": "string", "format": "datetime" }
313313+ }
314314+ }
315315+ },
316316+ "file": {
317317+ "type": "object",
318318+ "required": ["type", "blob"],
319319+ "properties": {
320320+ "type": { "type": "string", "const": "file" },
321321+ "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
322322+ "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
323323+ "mimeType": { "type": "string", "description": "Original MIME type before compression" },
324324+ "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" }
325325+ }
326326+ },
327327+ "directory": {
328328+ "type": "object",
329329+ "required": ["type", "entries"],
330330+ "properties": {
331331+ "type": { "type": "string", "const": "directory" },
332332+ "entries": {
333333+ "type": "array",
334334+ "maxLength": 500,
335335+ "items": { "type": "ref", "ref": "#entry" }
336336+ }
337337+ }
338338+ },
339339+ "entry": {
340340+ "type": "object",
341341+ "required": ["name", "node"],
342342+ "properties": {
343343+ "name": { "type": "string", "maxLength": 255 },
344344+ "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
345345+ }
346346+ },
347347+ "subfs": {
348348+ "type": "object",
349349+ "required": ["type", "subject"],
350350+ "properties": {
351351+ "type": { "type": "string", "const": "subfs" },
352352+ "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees. Integration behavior (flat vs nested) is controlled by the flat property in the parent place.wisp.fs record." }
353353+ }
354354+ }
355355+ }
356356+}
357357+```
358358+359359+## Related
360360+361361+- [place.wisp.fs](/lexicons/place-wisp-fs) - Main site manifest lexicon
362362+- [AT Protocol Lexicons](https://atproto.com/specs/lexicon)
363363+
+222
docs/src/content/docs/redirects.md
···11+---
22+title: Redirects & Rewrites
33+description: Netlify-style _redirects file support for flexible URL routing
44+---
55+66+# Redirects & Rewrites
77+88+Wisp.place supports Netlify-style `_redirects` files, giving you powerful control over URL routing and redirects. Whether you're migrating an old site, setting up a single-page app, or creating clean URLs, the `_redirects` file lets you handle complex routing scenarios without changing your actual file structure.
99+1010+## Getting Started
1111+1212+Drop a file named `_redirects` in your site's root directory. Each line defines a redirect rule with the format:
1313+1414+```
1515+/from/path /to/path [status] [conditions]
1616+```
1717+1818+For example:
1919+```
2020+/old-page /new-page
2121+/blog/* /posts/:splat 301
2222+```
2323+2424+## Basic Redirects
2525+2626+The simplest redirects move traffic from one URL to another:
2727+2828+```
2929+/home /
3030+/about-us /about
3131+/old-blog /blog
3232+```
3333+3434+These use a permanent redirect (301) by default, telling browsers and search engines the page has moved permanently.
3535+3636+## Status Codes
3737+3838+You can specify different HTTP status codes to change how the redirect behaves:
3939+4040+**301 - Permanent Redirect**
4141+```
4242+/legacy-page /new-page 301
4343+```
4444+Tells browsers and search engines the page has moved permanently. Good for SEO when content has truly moved.
4545+4646+**302 - Temporary Redirect**
4747+```
4848+/temp-sale /sale-page 302
4949+```
5050+Indicates a temporary move. Browsers won't cache this as strongly, and search engines won't transfer SEO value.
5151+5252+**200 - Rewrite**
5353+```
5454+/api/* /functions/:splat 200
5555+```
5656+Serves different content but keeps the original URL visible to users. Perfect for API routing or single-page apps.
5757+5858+**404 - Custom Error Page**
5959+```
6060+/shop/* /shop-closed.html 404
6161+```
6262+Shows a custom error page instead of the default 404. Useful for seasonal closures or section-specific error handling.
6363+6464+**Force with `!`**
6565+```
6666+/existing-file /other-file 200!
6767+```
6868+Normally, if the original path exists as a file, the redirect won't trigger. Add `!` to force it anyway.
6969+7070+## Wildcard Redirects
7171+7272+Splats (`*`) let you match entire path segments:
7373+7474+**Simple wildcards:**
7575+```
7676+/news/* /blog/:splat
7777+/old-site/* /new-site/:splat
7878+```
7979+8080+If someone visits `/news/tech-update`, they'll be redirected to `/blog/tech-update`.
8181+8282+**Multiple wildcards:**
8383+```
8484+/products/*/details/* /shop/:splat/info/:splat
8585+```
8686+8787+This captures multiple path segments and maps them to the new structure.
8888+8989+## Placeholders
9090+9191+Placeholders let you restructure URLs with named parameters:
9292+9393+```
9494+/blog/:year/:month/:day/:slug /posts/:year-:month-:day/:slug
9595+/products/:category/:id /shop/:category/item/:id
9696+```
9797+9898+These are more precise than splats because you can reference the captured values by name. Visiting `/blog/2024/01/15/my-post` redirects to `/posts/2024-01-15/my-post`.
9999+100100+## Query Parameters
101101+102102+You can match and redirect based on URL parameters:
103103+104104+```
105105+/store?id=:id /products/:id
106106+/search?q=:query /find/:query
107107+```
108108+109109+The query parameter becomes part of the redirect path. `/store?id=123` becomes `/products/123`.
110110+111111+## Conditional Redirects
112112+113113+Make redirects happen only under certain conditions:
114114+115115+**Country-based:**
116116+```
117117+/ /us/ 302 Country=us
118118+/ /uk/ 302 Country=gb
119119+```
120120+121121+Redirects users based on their country (using ISO 3166-1 alpha-2 codes).
122122+123123+**Language-based:**
124124+```
125125+/products /en/products 301 Language=en
126126+/products /de/products 301 Language=de
127127+```
128128+129129+Routes based on browser language preferences.
130130+131131+**Cookie-based:**
132132+```
133133+/* /legacy/:splat 200 Cookie=is_legacy
134134+```
135135+136136+Only redirects if the user has a specific cookie set.
137137+138138+## Advanced Patterns
139139+140140+**Single-page app routing:**
141141+```
142142+/* /index.html 200
143143+```
144144+145145+Send all unmatched routes to your main app file. Perfect for React, Vue, or Angular apps.
146146+147147+**API proxying:**
148148+```
149149+/api/* https://api.example.com/:splat 200
150150+```
151151+152152+Proxy API calls to external services while keeping the URL clean.
153153+154154+**Domain redirects:**
155155+```
156156+http://blog.example.com/* https://example.com/blog/:splat 301!
157157+```
158158+159159+Redirect from subdomains or entirely different domains.
160160+161161+**Extension removal:**
162162+```
163163+/page.html /page
164164+```
165165+166166+Clean up old `.html` extensions for a modern look.
167167+168168+## How It Works
169169+170170+1. **Processing order:** Rules are checked from top to bottom - first match wins
171171+2. **Specificity:** More specific rules should come before general ones
172172+3. **Caching:** Redirects are cached for performance but respect the site's cache headers
173173+4. **Performance:** All processing happens at the edge, close to your users
174174+175175+## Examples
176176+177177+Here's a complete `_redirects` file for a typical site migration:
178178+179179+```
180180+# Old blog structure to new
181181+/blog/* /posts/:splat 301
182182+183183+# API proxy
184184+/api/* https://api.example.com/:splat 200
185185+186186+# Country redirects for homepage
187187+/ /us/ 302 Country=us
188188+/ /uk/ 302 Country=gb
189189+190190+# Single-page app fallback
191191+/* /index.html 200
192192+193193+# Custom 404 for shop section
194194+/shop/* /shop/closed.html 404
195195+```
196196+197197+## Tips
198198+199199+- **Order matters:** Put specific rules before general ones
200200+- **Test thoroughly:** Use the preview feature to check your redirects
201201+- **Use 301 for SEO:** Permanent redirects pass SEO value to new pages
202202+- **Use 200 for SPAs:** Rewrites keep your app's routing intact
203203+- **Force when needed:** The `!` flag overrides existing files
204204+- **Keep it simple:** Most sites only need a few redirect rules
205205+206206+## Troubleshooting
207207+208208+**Redirect not working?**
209209+- Check the order - rules are processed top to bottom
210210+- Make sure the file is named exactly `_redirects` (no extension)
211211+- Verify the file is in your site's root directory
212212+213213+**Wildcard not matching?**
214214+- Wildcards only work at the end of paths
215215+- Use placeholders for more complex restructuring
216216+217217+**Conditional redirect not triggering?**
218218+- Country detection uses IP geolocation
219219+- Language uses Accept-Language headers
220220+- Cookies must match exactly
221221+222222+The `_redirects` system gives you the flexibility to handle complex routing scenarios while keeping your site structure clean and maintainable.
+11
docs/src/content/docs/reference/example.md
···11+---
22+title: Example Reference
33+description: A reference page in my new Starlight docs site.
44+---
55+66+Reference pages are ideal for outlining how things work in terse and clear terms.
77+Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you're documenting.
88+99+## Further reading
1010+1111+- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework