Prerendering
Opt views into build-time static rendering with a single export. The marker lives in the view file, not the controller — that's where "this is pure" is true.
// app/Views/Landing.tsx
export const prerender = true
export const route = '/'
export default function Landing() {
return <h1>Welcome</h1>
}pnpm build writes dist/client/index.html to disk. Production serves the static file before falling back to SSR — zero per-request cost.
Build-time only. pnpm dev still SSRs every request; the marker is inert in dev.
Static prerender
Use export const prerender = true for views whose markup doesn't depend on per-request state — marketing pages, docs, terms, 404. The scanner picks the flag up the same way it reads export const route.
// app/Views/Marketing/Pricing.tsx
export const prerender = true
export const route = '/pricing'
export default function Pricing() { /* … */ }The build emits one HTML file at the view's URL. Toggling the export off removes the generated artifact on the next scan.
Dynamic prerender
For parameterized routes (/blog/@slug, /products/@id), declare the URLs to materialize at build time. The scanner accepts an array literal, a sync function, or an async function — whatever shape fits the data source:
// app/Views/Blog/Post.tsx
export const route = '/blog/@slug'
export const prerender = ['/blog/hello-world', '/blog/another-post']
export default function Post() {
const ctx = usePageContext()
const slug = ctx.routeParams.slug
// …
}For DB-backed slugs, return the URLs from a function:
// app/Views/Docs/Article.tsx
import { Post } from 'App/Models/Post.js'
export const route = '/docs/@slug'
export const prerender = async (): Promise<string[]> => {
const posts = await Post.query().select(['slug']).all()
return posts.map(p => `/docs/${p.slug}`)
}pnpm build calls the function once and writes one HTML per returned URL. The full Vike OnBeforePrerenderStart return shape passes through — return { url, pageContext } entries when you need per-URL props injected at build time.
The scanner detects the dynamic form from the right-hand side of the assignment — array literal ([), arrow / call expression ((), or a function keyword. Variable-reference RHS (= MY_LIST) is intentionally not picked up; opt-in is explicit. Inline the value or wrap in a function.
How the scanner reads the export
The detection regex is anchored at the start of a logical line — ^export const prerender … with multiline matching. This means a documentation snippet like export const prerender = ['/a'] appearing inside a string elsewhere in the file (e.g. a card description) won't false-positive as the actual top-level export.
The generated artifacts under pages/__view/<id>/ are:
| Mode | Files emitted |
|---|---|
off |
(none) |
static |
+prerender.ts only |
dynamic |
+prerender.ts AND +onBeforePrerenderStart.ts |
Switching between modes is symmetric — removing the export drops all generated files; flipping dynamic → static drops the hook but keeps the boolean opt-in.
Constraints
- Build-time only. Dev mode is unaffected.
pnpm devalways SSRs. - Controllers don't run per-URL at prerender time. For dynamic prerender, route params come back on
pageContext.routeParams.slug(or whatever the segment is named). Either read them viausePageContext()in the view, or return{ url, pageContext: { viewProps: … } }entries from the function form to inject view props at build time. - Auth-guarded views are trivially incompatible. The user is unknown at build time. Opt-in is per-view, so this is the default —
Dashboard.tsxis never accidentally prerendered. - Variable-reference RHS is not detected.
export const prerender = MY_LISTstaysoff. Inline or wrap in a function.
Related
- Typed Views —
export interface Propsin the view file for typedview('id', props)calls. - Controllers — the routing surface that returns
view('id', props).