How to: static export + deploy


Status: the export itself is built and works today; a zero-ops GitHub Pages Actions workflow is still planned (see README.md). This guide covers what runs now.

Run it

bun run export                                # → dist/, absolute paths (root host / custom domain)
PUBLIC_BASE_PATH=/repo bun run export         # → dist/ for user.github.io/<repo>/ (subpath host)
PUBLIC_ORIGIN=https://you.com bun run export  # bake the real origin into sitemap.xml/robots.txt

Serve the result with any static file server: bunx serve dist, or push dist/ to any static host (GitHub Pages, Netlify, S3 — it's plain files, nothing server-specific left in them).

What actually gets frozen

The exporter (tools/export.ts, on top of the generic batch/export/export.ts) fetches the running server and freezes its output — it never re-renders (ARCHITECTURE §18: the export is a projection, not a second renderer). Concretely, it:

  1. boots server.ts on a throwaway port,
  2. crawls every page route (the same sitemap the live server uses) + MILL's content routes

(/notes, /grain/docs, /batch/docs) + /catalog,

  1. copies every asset mount (config.ts's assetDirs + fonts) verbatim,
  2. rewrites the baked-in origin/base-path if you passed PUBLIC_ORIGIN / PUBLIC_BASE_PATH.

The one boundary: operable pages don't export

A static host has no backend, so anything behind the one door (/intent + SSE) is excluded from the crawl — see tools/export.ts's OPERABLE set (empty today, so no route is export-excluded). The /grain showcase still works in the static copy: its demo is flipped to the client-side door (a browser-only version of the same vocabulary, ARCHITECTURE §19.3, CLIENT_DOOR_PAGES), so the pitch ("watch the AI act") survives with zero server.

If you add a new operable page

Add its route to OPERABLE in tools/export.ts (or to CLIENT_DOOR_PAGES if you're giving it a client-side door like /grain's) — otherwise the export will try to crawl a page that needs a backend it won't have.