Prerender
Static-by-default rendering — how Mandu turns your routes into HTML at build time, and the tiny lever that opts out.
On this page
Prerender
Prerender is the render mode Mandu assumes you want. Routes become HTML files at build time and the server does the boring work in advance, which is usually the work you wanted anyway.
This page documents what actually happens when the prerender engine runs, what the knobs do, and where the artifacts land.
The default path
Every file matching app/**/page.tsx is a candidate. If its URL pattern has no
dynamic parameters, it is prerendered unconditionally — no opt-in, no config.
// app/about/page.tsx
export default function About() {
return <article>Static. Done.</article>;
}
The engine walks the routes manifest, collects patterns where
route.pattern.includes(":") is false, and renders each one by issuing a
Request to the fetch handler (source: bundler/prerender.ts:69-73).
Output for /about lands at .mandu/static/about/index.html. The root route
/ lands at .mandu/static/index.html. The suffix is always index.html
because clean URLs are non-negotiable (source: bundler/prerender.ts:166-171).
Dynamic routes and generateStaticParams
Dynamic routes — those with [param], [...param], or [[...param]] —
produce a pattern that contains a colon (for example /blog/:slug). The
engine looks for a named export:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await loadPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export default function Post({ params }: { params: { slug: string } }) {
return <article>{params.slug}</article>;
}
For each param object returned, the engine substitutes the pattern via
resolvePattern() — catch-all segments preserve slashes and encode each path
piece individually (source: bundler/prerender.ts:145-159).
If generateStaticParams() returns anything other than an array, the engine
logs a warning and skips that route. If the export is missing, the route is
simply not prerendered; it will render dynamically at request time instead.
Crawl mode — discover without listing
You can tell the engine to follow internal links in the HTML it just rendered and prerender those too:
bun run build --crawl
Mechanically: after writing an HTML file, the engine pulls every
href attribute whose value starts with /, drops query strings and fragments, filters out
static-asset extensions (.js, .css, .png, .woff2, and friends), and
adds the remaining paths to the render queue (source:
bundler/prerender.ts:174-195).
Crawl mode is idempotent — already-rendered paths skip re-render thanks to the
renderedPaths set. It is most useful for catalog-style sites where an index
page enumerates detail pages and you would rather not maintain two lists.
Adding explicit routes
If a path is neither listed in the manifest nor reachable by crawling (for
example, a page you generate via generateStaticParams on a dynamic route
you also reference elsewhere), pass it in:
// mandu.config.ts
export default {
prerender: {
routes: ["/sitemap.xml", "/well-known/ai-plugin.json"],
},
};
These are merged into pathsToRender before any manifest-driven discovery
(source: bundler/prerender.ts:66).
Output layout
After a successful build:
.mandu/static/
├── index.html ← /
├── about/index.html ← /about
├── blog/
│ ├── index.html ← /blog
│ └── hello-world/index.html ← /blog/hello-world (from generateStaticParams)
└── docs/
└── architect/
└── prerender/index.html
Each page result carries { path, size, duration } so the build reporter can
show you which page cost what. Errors are collected per path and surfaced at
the end — a failed render does not abort the build, but a non-zero count is
treated as a build failure by the orchestrator (source: bundler/prerender.ts:99-135).
Opting out: dynamic rendering
The one-line escape hatch:
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";
Routes tagged force-dynamic are excluded from the prerender pass and served
at request time by the runtime server. Use it when the page's HTML truly
depends on the request (authenticated sessions, per-user data, request
headers). Do not use it to avoid learning generateStaticParams.
Anti-patterns
Do not do this.
- Calling
fetchinside the render path to reach your own API. The prerender engine runs the same fetch handler; hitting/api/postsfrom inside a page handler creates a cycle. Read the data source directly.- Returning a promise from
generateStaticParamsthat resolves to an object (not an array). The engine warns and skips the route — your page silently becomes request-time rendered, and you will not notice until a CDN miss at 3am.- Relying on
Date.now()orprocess.env.SOMETHINGinside a prerendered page without a rebuild trigger. The HTML freezes at build time. If the value needs to be fresh, the route should be dynamic.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/prerender to my project.
Summary of the page:
Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths.
Required invariants — must hold after your changes:
- A page route with no dynamic segment is always prerendered
- A dynamic page route is prerendered only for params returned by generateStaticParams()
- Output lives under .mandu/static/<pathname>/index.html (clean URLs)
- generateStaticParams() must return an array; non-array returns are warned and skipped
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
For Agents
Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths.
- A page route with no dynamic segment is always prerendered
- A dynamic page route is prerendered only for params returned by generateStaticParams()
- Output lives under .mandu/static/<pathname>/index.html (clean URLs)
- generateStaticParams() must return an array; non-array returns are warned and skipped