Askr documentation
Guides

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.