Routing
How file-system routing under pages/ maps to URLs in zfb.
zfb uses file-system routing under the pages/ directory. The router scans pages/ at build time (and on every change in dev), turning each page source file into a route. The conventions match what you might recognise from Next.js or Astro.
File-to-route mapping
| File | Route | Notes |
|---|---|---|
pages/ | / | |
pages/ | / | |
pages/ | / | SSG-only; MDX pipeline |
pages/ | / | SSG-only; static-asset copy |
pages/ | / | |
pages/blog/[slug].tsx | / (dynamic) | |
pages/docs/[...slug].tsx | / (catchall) | |
pages/[lang]/[slug].tsx | / |
A few rules worth knowing up front:
- Files starting with
_(for example_app.tsx) are ignored. Use this prefix for shared helpers that live next to your routes but should not be exposed. - Accepted page extensions are
.tsx,.md, and.html. Files with any other extension inpages/are skipped (a warning is logged), so README files and ad-hoc notes are safe to drop there. - Two files that resolve to the same route raise
RouterError::AmbiguousRouteat build time, so the router never silently picks a winner.
See Markdown and HTML Pages for the full contract and v1 limitations of .md and .html page entries.
The scan is performed by Router::scan in the zfb-router crate. Results are sorted so that static routes win over dynamic, and dynamic wins over catchall — more specific routes are matched first.
Static, dynamic, and catchall routes
Static routes (pages/) match a single concrete URL. Dynamic routes use [param] brackets in the file name to capture a single path segment, and catchall routes use [...param] to capture any number of trailing segments.
// pages/blog/[slug].tsx
export default function BlogPost({ slug }: { slug: string }) {
return <article>Post for {slug}</article>;
}
// pages/docs/[...slug].tsx
export default function DocsPage({ slug }: { slug: string[] }) {
return <main>{slug.join("/")}</main>;
}
The paths() export
Dynamic and catchall routes need to know which concrete URLs to render at build time. This is done by exporting a paths() function from the same file:
// pages/blog/[slug].tsx
export function paths() {
const posts = getCollection("blog");
return posts.map((p) => ({ params: { slug: p.slug } }));
}
export default function BlogPost({ params }: { params: { slug: string } }) {
return <article>Post {params.slug}</article>;
}
paths() is evaluated by the embedded V8 host during the build. zfb build discovers static, dynamic, and catchall routes and evaluates paths() for each dynamic and catchall route to enumerate the concrete URLs to render. Static routes require no paths() export.