A dynamic route such as app/blog/[slug] cannot be pre-rendered unless Next.js knows which slugs exist. generateStaticParams provides that list, turning thousands of database rows into static HTML at build time while still handling new entries on demand. After this lesson you will be able to pre-render dynamic routes, decide what happens for unknown parameters, and combine static generation with revalidation.
Without extra information, app/blog/[slug]/page.tsx must be rendered per request because the set of valid slugs is unknown at build time. That costs a server render on every visit and shows as ƒ (Dynamic) in the build output. Exporting generateStaticParams from the same file changes the picture: Next.js calls it during next build, renders one page per returned object, and marks the route ● (SSG).
The function returns an array of objects whose keys match the dynamic segments:
// app/products/[id]/page.tsx
export async function generateStaticParams() {
const res = await fetch("https://api.example.com/products");
const products: { id: number }[] = await res.json();
return products.map((p) => ({ id: String(p.id) }));
}Values must be strings, because they represent URL segments. Fetch calls made here are memoized, so if the page itself requests the same URL the data is not fetched twice. For a catch-all segment [...slug] return arrays: { slug: ["docs", "getting-started"] } produces /docs/getting-started.
What happens when a visitor requests /blog/new-post that did not exist at build time? The dynamicParams segment config decides:
| dynamicParams | Behaviour for a slug not in the list |
|---|---|
| true (default) | Rendered on demand, then cached like the pre-built pages |
| false | Responds with 404 |
export const dynamicParams = false; // only pre-built slugs are validKeep the default when content is added after deployment (blog posts, products). Set it to false for closed sets such as [locale] or a fixed list of categories, where an unexpected value should be an error.
Pre-built pages can still refresh. Add a revalidation window and the route behaves like ISR: built once, regenerated in the background after the window expires, and new slugs rendered on first visit:
export const revalidate = 600; // seconds
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.slice(0, 50).map((post) => ({ slug: post.slug })); // pre-build the popular ones
}Pre-building only a subset is a common trade-off: build time stays short, the most visited pages are instant, and the long tail is generated lazily. With Cache Components enabled, tag the underlying data with cacheTag("posts") so an editor's Server Action can purge every affected page at once.
For app/[category]/[product]/page.tsx, the parent segment's generateStaticParams runs first and each child call receives the parent's params:
// app/[category]/[product]/page.tsx
export async function generateStaticParams({ params }: { params: { category: string } }) {
const products = await getProducts(params.category);
return products.map((p) => ({ product: p.slug }));
}Alternatively, generate both keys from the child in one pass by returning { category, product } objects. Choose the single-pass form when one query already gives you every combination.
cookies() or headers() inside the page, which makes the route dynamic regardless of generateStaticParams.generateStaticParams runs only at build time; in next dev pages are rendered on request.What does `generateStaticParams` return?
generateStaticParams lists the parameter combinations to pre-render at build time.[param] folder names.dynamicParams controls whether unknown parameters render on demand (default) or 404.revalidate to refresh pre-built pages, and pre-build only the pages that matter most.Next lesson: Parallel and Intercepting Routes — render several pages in one layout and open routes as modals.