Generate a Static Site
Generate a Static Site: a worked guide from route registry through to a production build.
Example
Keep static routes in the shared registry and make the document, assets, and output directory explicit in ssg.config.ts.
export const staticConfig = {
registry,
outputDir: 'dist',
document: renderDocument,
assets: [{ from: resolve('public'), to: '.' }],
};
// package.json: "build": "askr ssg --config ./ssg.config.ts --output ./dist"Goal and architecture
createStaticGen({ registry, outputDir }) from @askrjs/askr/ssg walks the explicit route registry and writes fully rendered HTML per route, plus a metadata.json describing the run. Routes carrying real request-auth requirements are excluded from prerendering by default, since there's no request to authenticate against at build time — those stay runtime-only unless you explicitly override that.
Implementation
For parameterized paths like /posts/{slug}, supply an entries() function that returns one param map per page you want generated — createStaticGen expands the path template against each entry to produce concrete URLs. dataOverrides lets you feed deterministic seed data to resources during generation instead of hitting a live backend, and assets copies static files or directories alongside the generated HTML. askr ssg --config <path> --output <dir> [--incremental] runs the same generator from the CLI, with --changed-key/--changed-route (forwarded internally as changedKeys/changedRoutes) marking what changed so incremental mode skips everything else.
Failure states
Check result.failed and each route's RouteRenderResult.error after generate() — a single route throwing during render doesn't stop the whole run, but it does mean that route's HTML wasn't written, and a naive script that only checks a zero exit code will miss it. Routes with policies (advanced runtime access checks) are excluded from prerendering by default for the same reason auth-gated routes are — don't fight this by forcing them in unless you're certain the policy is safe to evaluate without a real request.
Verification
Confirm result.successful matches result.totalRoutes and inspect result.routes[].reason — a real RouteRenderReason is one of full, changed-key, changed-route, new-route, no-keys, unchanged, deleted, or runtime-only (there's no cache-hit value; unchanged is what a correctly-skipped route reports). unchanged routes on an incremental run should genuinely be unchanged, not silently stale because a changed key wasn't wired up. Open a generated file directly (not through a dev server) and confirm it's real standalone HTML with no unresolved markers. If you're shipping incrementally, re-run with a deliberately changed key and confirm exactly the expected routes rebuild, not the entire set.