Static Export (SSG)
Export the whole app to a folder of plain HTML — the Python
equivalent of Next.js
output: export.
Every static route is rendered through the real app and written to
static/,
ready to drop on any CDN or static host — ideal for
docs, marketing sites, and landing pages.
App-level convention — no config flag
Static export is a build convention the application layers on top of
Caspian, the same category as the
quality gate.
The framework ships no exporter, and there is
no caspian.config.json flag
for it — it lives in two
settings/ scripts
wired to two package.json scripts.
Byte-identical render
The exporter boots the real ASGI app and requests every route through
Starlette’s TestClient,
so exported HTML matches the dev server — layouts, components,
PulsePoint deferral, and security headers included.
Warn & skip
Anything that cannot be fully static — auth-gated redirects,
non-200 or non-HTML responses, dynamic routes without
static_paths —
is reported and skipped, never shipped broken.
Hardened preview
A port-aware local server serves only static/,
auto-selects a free port, and binds loopback by default — an
occupied port never aborts the preview.
The two commands
One command builds the static/
folder; the other previews it over HTTP.
npm run static
Runs npm run build
(metadata + Tailwind) so the route index is fresh, then
settings/build-static.py
renders every static route into static/
and mirrors public assets.
npm run static:serve
Runs settings/serve-static.py,
which serves only static/,
walks up from port 8000
until one binds, and prints the URL it landed on.
Preview over HTTP — not file://
Don’t double-click static/index.html.
Asset paths are root-absolute (/css/…,
/js/…), and browsers block ES
module scripts from file:// origins —
so both break unless served over HTTP.
Pre-rendering dynamic routes
Dynamic routes ([id] /
[...slug]) are skipped
unless their index.py
exports static_paths —
Caspian’s equivalent of Next.js
getStaticPaths. It may be a
list of dicts, a list of scalars (mapped onto the route’s single param), or a
sync/async callable returning either.
# List of dicts — one page per entry
static_paths = [
{"slug": "hello-world"},
{"slug": "why-caspian"},
]
# Or resolve them at build time (sync or async)
async def static_paths():
posts = await prisma.post.find_many(where={"published": True})
return [{"slug": p.slug} for p in posts]
Each returned param set is rendered to its own
static/<route>/index.html.
Routes without static_paths
are reported and skipped by design.
What goes inert in a static build
A static export is HTML plus assets — there is no Python backend behind it. Pages that depend on the server still render their first paint, but these interactions are dead in the output:
Needs the server — inert
pp.rpc()server actions- Auth & sessions
- WebSockets & streaming
- Per-request server data
Works fine — fully static
- Server-rendered HTML & Jinja
- Client-only PulsePoint state & effects
- Components, layouts, metadata
- Static assets (css, js, images)
The exporter flags pages that appear to use
pp.rpc() so nothing
ships silently broken.
Preview server overrides
settings/serve-static.py
binds loopback 127.0.0.1
and auto-selects a free port. All overrides are optional environment variables;
invalid values fall back to the defaults with a warning instead of crashing.
| Variable | Default | What it does |
|---|---|---|
| HOST | 127.0.0.1 |
Bind address. Set HOST=0.0.0.0
to expose the preview on the local network (opt-in).
|
| PORT | 8000 | Preferred start port; the server walks upward until one binds. |
| PORT_TRIES | 50 | How many ports to try before giving up. |
Read the port it prints
The static preview is a separate process on its own auto-selected port —
it has nothing to do with the dev BrowserSync ports in
settings/bs-config.json.
Use the URL the serve command actually prints.
Deploying the output
Upload the static/
folder to any static host — Netlify, Vercel, GitHub Pages, nginx, S3, or any
CDN. Asset URLs are root-absolute
(/css/…,
/js/…), so serve
it at a domain root, or rewrite the base path for a subdirectory deploy. Treat
static/,
settings/files-list.json,
and settings/component-map.json
as generated outputs — don’t hand-edit them.