How to: export and deploy the portfolio
This guide covers the portfolio app's exporter. BATCH supplies the export mechanism, while the portfolio repository boots the app, crawls its routes, and writes the static files. The portfolio's GitHub Pages workflow runs the export and link verifier, then deploys the result on pushes to main and on its daily refresh. BATCH itself is a library and does not own a site deployment workflow.
Run it
Run these commands from the portfolio repository:
bun run export
bun run verify:exportThe export writes to dist/ with root-absolute paths for a custom domain. For a repository hosted at user.github.io/repo, set PUBLIC_BASE_PATH=/repo. Set PUBLIC_ORIGIN to the deployed origin so the sitemap, robots file, and language-model index carry public URLs.
Serve the result with any static file server, such as bunx serve dist, or deploy dist/ to a static host. The output is plain files and does not need the BATCH server at runtime.
What actually gets frozen
The exporter (tools/export.ts, on top of the generic @tjakoen/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:
- boots
server.tson a throwaway port, - crawls the live sitemap routes plus the content, deck, plan, and generated routes assembled by
pageRoutes()intools/export.ts, - copies every asset mount (
config.ts'sassetDirs+ fonts); when a base path is set, it rewrites CSS and JavaScript references to work under that path, - 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 for supported service-free actions (ARCHITECTURE §19.3, CLIENT_DOOR_PAGES), so the pitch ("watch the AI act") survives without an app backend.
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.