Dynamic Routes
Use paths() to enumerate the concrete URLs a [slug].tsx or [...slug].tsx page should build, and pass per-URL props to the component.
ℹ️ What this page covers
How paths() enumerates the URLs a dynamic or catchall page should
emit, the { params, props } contract it returns, and how page
components receive the data. For static-route fundamentals, see
Routing.
A dynamic route — pages/blog/[slug].tsx — doesn’t map to a single
URL. The bracketed segment is a parameter, and zfb needs to know
which concrete values to fill it with at build time. That’s the job
of paths().
Catchall routes — pages/docs/[...slug].tsx — work the same way, but
their slug parameter captures one or more trailing segments instead
of a single one.
The paths() contract
paths() is a synchronous export. It returns an array of
{ params, props } objects. The router consumes the array; one entry
becomes one rendered URL.
type PathEntry<P = Record<string, unknown>> = {
/** Values for the bracketed segments, keyed by parameter name. */
params: Record<string, string | string[]>;
/** Optional per-URL data threaded to the page component as `props`. */
props?: P;
};
export function paths(): PathEntry[];
paramskeys must match the bracketed names in the filename. For[slug].tsx, the key isslug. For[lang]/[slug].tsx, you supply bothlangandslug. For catchall[...slug].tsx,slugis astring[]of the trailing segments.propsis optional, opaque to the engine, and forwarded verbatim to the page component as thepropsprop. The engine never inspects it.
A blog post page
The canonical use of paths() is enumerating slugs from a content
collection:
// pages/blog/[slug].tsx
import { getCollection, getEntry } from "zfb/content";
export const frontmatter = { title: "Blog post" };
export function paths() {
const posts = getCollection("blog");
return posts.map((post) => ({
params: { slug: post.slug },
props: { title: post.data.title },
}));
}
export default function BlogPost({ params, props }) {
const post = getEntry("blog", params.slug);
if (!post) return <p>Not found.</p>;
return (
<article>
<h2>{props.title}</h2>
<post.Content />
</article>
);
}
A few things worth noting:
getCollectionandgetEntryare synchronous — the full content snapshot is pre-built in Rust before any TSX runs.paths()doesn’t needasync, and neither does the page component.params.slugis what hits the URL. A post withslug: "hello-zfb"becomes/.blog/ hello- zfb props.titleis opaque to the engine — it’s just data threaded to the component. You can put anything serializable there.
Catchall: a full-tree docs page
Catchall routes capture any number of trailing segments, useful when
the same template renders many depths under one prefix. The slug
parameter arrives as string[]:
// pages/docs/[...slug].tsx
import { getCollection, getEntry } from "zfb/content";
export const frontmatter = { title: "Docs" };
export function paths() {
const entries = getCollection("docs");
return entries.map((entry) => ({
// entry.slug looks like "guides/setup" or "concepts/routing"
params: { slug: entry.slug.split("/") },
}));
}
export default function DocsPage({ params }) {
const slugPath = params.slug.join("/");
const entry = getEntry("docs", slugPath);
if (!entry) return <p>Not found.</p>;
return <entry.Content />;
}
/ matches with params.slug === ["concepts", "routing"].
/ matches with params.slug === ["guides", "setup"].
The router rebuilds the slash-separated form (slug.) when
you need to look up an entry by it.
Static, dynamic, and catchall — how they fit together
| Filename | Kind | Example URL | params shape |
|---|---|---|---|
pages/ | static | / | n/a |
pages/blog/[slug].tsx | dynamic | / | { slug: string } |
pages/docs/[...slug].tsx | catchall | / | { slug: string[] } |
pages/[lang]/[slug].tsx | dynamic × 2 | / | { lang: string, slug: string } |
When two patterns can match the same URL, the more specific one wins: static beats dynamic; dynamic beats catchall. The router enforces this when it builds the route table — see Routing for the full sort.
Rules and gotchas
- Every
paramskey must have a value. Missing keys raise a build error before any HTML is written. - For catchall segments,
params.slugmust be astring[](even with one element). Passing a plain string is a type error. paths()is synchronous. The whole content snapshot is loaded before any page evaluates, so there’s no async boundary to wait on. Keep it pure-data and deterministic — the router calls it during route enumeration, well before render.- Two routes that resolve to the same template raise
RouterError::AmbiguousRouteat build time. The router never silently picks a winner.
See also
- Routing — static-route fundamentals.
- Content Collections — the data
source most
paths()calls draw from; also documents the synchronousgetCollection/getEntryAPI.