Rendering ahead of time
By default a dynamic route renders on demand: the first visitor to /blog/hello-world waits while the server fetches and renders. But your blog knows its post list in advance. generateStaticParams lets you hand Next.js that list at build time:
// app/blog/[slug]/page.js export async function generateStaticParams() { const posts = await fetch("https://api.example.com/posts").then((r) => r.json()); return posts.map((post) => ({ slug: post.slug })); }
Decode it:
- It runs during
next build, not per request. - It returns an array of params objects, one per page to prerender:
[{ slug: "hello-world" }, { slug: "why-frameworks-win" }, …]. - Next.js then renders each of those pages once, at build time, and serves the finished HTML to everyone. Fast for users, cheap for your server.
What the build produces from a params list
When generateStaticParams returns 50 slug objects at build time, next build finishes with 50 prerendered HTML pages, one per slug.
Each returned params object becomes one page. Next.js substitutes the slug into the dynamic segment, renders the component with that value, and writes the resulting HTML to disk. All 50 post pages therefore exist as finished files before any visitor arrives, so a request costs nothing more than serving a static file from a CDN.
Prebuild the popular, render the rest
generateStaticParams does not have to return every possible page, and at scale it should not. An e-commerce site with 200,000 products cannot rebuild them all on every deploy, because builds would take hours. The production pattern is a partial list:
// app/products/[id]/page.js export async function generateStaticParams() { const top = await getTop100ProductIds(); // best sellers only return top.map((id) => ({ id })); }
What this buys you:
- The 100 pages that receive most of the traffic are prebuilt and instant.
- The long tail renders on demand on first request, then is kept, so the second visitor to any product is fast too.
- Builds stay minutes long no matter how big the catalog grows.
Treat the list as a performance dial, not a registry. Turn it up for hot pages and let on-demand rendering cover the rest.
A shopper lands on an unlisted product
A store has 200,000 products but generateStaticParams returns only the 100 best sellers. A shopper visits product #150,000, which is not in the list. By default that page renders on demand on the first request, and the result is kept for later visitors.
The relevant setting is dynamicParams, which is on by default. With it on, params that were not prebuilt are rendered when first asked for: the first visitor waits for a single render, then the stored result serves everyone after them.
That behaviour is exactly why partial lists are the production pattern. You prebuild the hot pages and let the long tail fill in lazily as real traffic discovers it. Unlisted slugs only 404 if you explicitly turn dynamicParams off.
Slugs published after the build
Consider a visitor requesting /blog/brand-new-post, published after the last build finished. By default Next.js does not give up. It renders that page on demand on first request, then keeps the result for subsequent visitors.
So generateStaticParams is an optimization list, not a security wall. Pages you listed are ready instantly, and pages you did not are generated the first time someone asks for them.
If you would rather unlisted slugs 404, the exported dynamicParams config option turns on-demand rendering off. It is enough for now to know the option exists.
Prerendering a three-page docs site
A docs site has exactly three pages, intro, setup, and faq, all served by app/docs/[page]/page.js. To prerender them all, generateStaticParams should return three objects:
[{ page: "intro" }, { page: "setup" }, { page: "faq" }]Each object fills the [page] segment once, so next build prerenders /docs/intro, /docs/setup, and /docs/faq as static HTML.
Why it works out that way
- The rule is one params object per page you want prebuilt, with no batching or wildcards.
- The key inside each object is the bracket folder's name, which is
pagehere, so the objects look like{ page: "intro" }. - For a site this small there is no reason to leave anything out of the list. Partial lists only pay off when the page count is large enough to slow the build.