cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

generateStaticParams

Prerender a dynamic route's popular pages with generateStaticParams, render the rest on their first visit, and handle unknown params with Cache Components.

By the end, you can
  • 01
    List params for the buildWrite generateStaticParams for one dynamic segment or several, and read the route table and the files the build wrote.
  • 02
    Know what happens to the restTell what the first and the second visit to an unlisted param get, and why dynamicParams doesn't work with Cache Components.
  • 03
    Give the build what it needsReturn at least one param, wait for params inside Suspense, and choose which pages to make ahead.

Two teas made before anyone asks

Dynamic routes gave the tea shop one page for every tea, in app/teas/[slug]/page.tsx, and listed the teas with generateStaticParams so Next.js could make their pages during the build. Real shops have more teas than they want to build ahead. Here, the shop lists only its popular teas, from its catalog. The catalog's API is a stand-in on the same computer, whose address is in CATALOG_API_URL: it has green, mint and oolong, and says green and mint are popular. The shop is a real Next.js 16.4.3 project, built with next build and run with next start.

The tea page, with the popular teas listed for the buildNext.js
app/teas/[slug]/lib/
app/teas/[slug]/page.tsx
import { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams() {  const teas = await getPopularTeas();  return teas.map((tea) => ({ slug: tea.slug }));} async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {  const { slug } = await params;  const tea = await getTea(slug);  if (!tea) notFound();  return (    <>      <h1>{tea.name}</h1>      <p>${tea.price} a box</p>    </>  );} export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {  return (    <main>      <Suspense fallback={<p>Loading the tea…</p>}>        <TeaDetails params={params} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (6/6)  Finalizing page optimization ... Route (app)         Revalidate  Expire┌ ○ /                      15m      1y├ ○ /_not-found└   /teas/[slug]  ├ ◐ /teas/[slug]  ├ ○ /teas/green          15m      1y  └ ○ /teas/mint           15m      1y ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html  9.2 kB.next/server/app/_global-error.meta  681 bytes.next/server/app/_global-error.rsc  4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc  4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc  4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc  115 bytes.next/server/app/_not-found.html  7.6 kB.next/server/app/_not-found.meta  774 bytes.next/server/app/_not-found.rsc  4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc  4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc  4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc  172 bytes.next/server/app/index.html  5.6 kB.next/server/app/index.meta  617 bytes.next/server/app/index.rsc  3.4 kB.next/server/app/index.segments/__PAGE__.segment.rsc  3.0 kB.next/server/app/index.segments/_full.segment.rsc  3.3 kB.next/server/app/index.segments/_tree.segment.rsc  115 bytes.next/server/app/teas/[slug].html  1.3 kB.next/server/app/teas/[slug].meta  4.2 kB.next/server/app/teas/[slug].segments/_full.segment.rsc  4.0 kB.next/server/app/teas/[slug].segments/_tree.segment.rsc  245 bytes.next/server/app/teas/[slug].segments/teas/$d$slug/__PAGE__.segment.rsc  3.2 kB.next/server/app/teas/green.html  6.5 kB.next/server/app/teas/green.meta  914 bytes.next/server/app/teas/green.rsc  4.0 kB.next/server/app/teas/green.segments/_full.segment.rsc  4.0 kB.next/server/app/teas/green.segments/_tree.segment.rsc  245 bytes.next/server/app/teas/green.segments/teas/$d$slug/__PAGE__.segment.rsc  3.3 kB.next/server/app/teas/mint.html  6.5 kB.next/server/app/teas/mint.meta  912 bytes.next/server/app/teas/mint.rsc  4.0 kB.next/server/app/teas/mint.segments/_full.segment.rsc  4.0 kB.next/server/app/teas/mint.segments/_tree.segment.rsc  245 bytes.next/server/app/teas/mint.segments/teas/$d$slug/__PAGE__.segment.rsc  3.3 kB

Lines 5 to 8 are generateStaticParams. Line 6 asks the catalog for the popular teas, with getPopularTeas from lib/catalog.ts. Line 7 turns each tea into an object with a slug: { slug: "green" } and { slug: "mint" }. Lines 10 to 20 are TeaDetails, which waits for params on line 11 and finds the tea on line 12. The page puts it inside a Suspense on lines 25 to 27, with the fallback Loading the tea….

The Terminal tab shows what the build did. In the route table, /teas/[slug] has three lines under it: ○ /teas/green and ○ /teas/mint, one for each listed tea, and ◐ /teas/[slug]. The legend says ○ means (Static) prerendered as static content, and ◐ means (Partial Prerender) prerendered as static HTML with dynamic server-streamed content. Under the output, the list of files the build wrote shows the same thing as files: teas/green.html, teas/mint.html, and teas/[slug].html, the shell for every tea that isn't listed.

On the left, a shaded card for generateStaticParams, and under it the two objects it returned: slug green and slug mint. Arrows lead to a window for next build, which writes three files: teas/green.html, the Green tea page, teas/mint.html, the Mint tea page, and teas/[slug].html, a shell with Loading the tea, shaded, for every other slug.generateStaticParams()returned{ slug: "green" }{ slug: "mint" }next buildteas/green.htmlteas/mint.htmla page for each listed teateas/[slug].htmlone shell for every other tea
generateStaticParams returned two teas, so next build made a page for each: teas/green.html and teas/mint.html. It also made one shell for every slug it didn't list, teas/[slug].html, with Loading the tea… in the tea's place.
Real-life analogyA bakery's morning shelf

A bakery bakes its best sellers before it opens, so they're on the shelf when the doors open. It doesn't bake every bread it knows: a rare one is baked when someone first asks for it, and the extra loaves go on the shelf for whoever asks next.

In real life: The list of breads the baker bakes before opening
In Next.js: What generateStaticParams returns
In real life: A loaf on the shelf, ready the moment you ask
In Next.js: A page made during the build, like teas/green.html
In real life: A rare bread, baked when the first customer orders it
In Next.js: An unlisted tea, rendered on its first visit
In real life: Extra loaves of it, put on the shelf for the next customers
In Next.js: The page Next.js keeps for the next visits
The main idea

generateStaticParams tells next build which values of a dynamic segment to make pages for. It returns a list of params objects, and the build makes a page for each one. Every other value is rendered on its first visit, from a shell the build also made, and Next.js keeps the result for the next visits.

Listing params at build time

generateStaticParams is a function you export from a page, a layout or a Route Handler in a folder with a dynamic segment. It returns an array. Each object in the array is one page, and each key is a segment's name, the word inside its brackets. Each row is one kind of route, and the shape its list has, from the docs:

What generateStaticParams returns, route by route
RouteWhat it returnsOne object, for example
app/teas/[slug]{ slug: string }[]{ slug: "green" }
app/teas/[kind]/[slug]{ kind: string, slug: string }[]{ kind: "green", slug: "sencha" }
app/guides/[...topic]{ topic: string[] }[]{ topic: ["brewing", "green-tea"] }

The values are strings, because an address is text; a catch-all segment takes an array of strings, one per segment.

When does it run? The docs give three rules:

  • During next build, before the route's pages are made. The recording kept every request the catalog's stand-in received: /teas/popular came during the build, once.
  • In next dev, when you open the route.
  • Not again when a page is made again later: its list is fixed by the build.

It can await anything a Server Component can: here, a cached fetch from the catalog. The build then made one page per object. The catalog also got /teas/green and /teas/mint during the build, from getTea on line 12, once for each listed page. Each row is a file the build wrote in .next/server/app:

What the build wrote for /teas/[slug]
FileWhat it holdsWhat it was used for
teas/green.htmlThe whole Green tea page, made during the build./teas/green, sent whole: x-nextjs-cache: HIT.
teas/mint.htmlThe whole Mint tea page, made during the build./teas/mint, sent whole.
teas/[slug].htmlThe shell for every other tea: the layout and Loading the tea….The first part of the first visit to /teas/oolong.
teas/[slug].metaPostponed state, to finish that shell's render once a slug is known.Used when an unlisted tea is rendered.

The 15m and 1y next to the listed teas in the route table come from the cache lifetime of the "use cache" functions in lib/catalog.ts. Cache lifetimes and tags teaches those lifetimes. The file names are what this build of Next.js 16.4.3 wrote: read them to see what was made ahead, not as something to build on.

Some now, the rest on demand

Oolong is in the catalog, but not in the list. Go back to the first example, open its Network tab, and pick /teas/oolong, first request. The note above the headers says the response began with .next/server/app/teas/[slug].html, byte for byte: the shell for unlisted teas. Its first part shows All teas and Loading the tea…; further down, a hidden part holds Oolong tea and $5 a box, rendered for this visit. Now pick asked for again: the whole page is in the first part, the headers say x-nextjs-cache: HIT, and the catalog's stand-in got /teas/oolong only once, for the first visit. Next.js kept the page it made, and sent that.

Step through the build and both visits. Before you start, guess: when does the second visitor see the tea?

Playground: two visits to a tea the build didn't list
Who does what/teas/oolong
What the visitor seesafter step 1
localhost:3000/teas/oolong

A blank page

React isn't running yet: clicks do nothing.

The page isn't complete yet.

Step 1 of 8
Next.js: next build calls generateStaticParams

There's no visitor yet. generateStaticParams asks the catalog for the popular teas, and gets green and mint.

Each step is one thing that happens, in order: first during next build, then two visits to /teas/oolong. The screens are the recorded responses: the first visit's first part, the page once the tea had arrived, and the second visit's. Times aren't to scale. Reset goes back to the first step.

The second visitor sees it at once, at step 8: the page was made on the first visit and kept. The docs call this Incremental Static Regeneration, or ISR: static pages made, or made again, after the build, without a new build. Incremental Static Regeneration covers it, with how a kept page is made again when its data changes.

Two windows. The first, first visit to /teas/oolong, holds a shaded card, Loading the tea, sent first from teas/[slug].html, and below it Oolong tea, 5 dollars a box, streamed in. An arrow labeled kept leads to the second window, second visit, which holds Oolong tea, 5 dollars a box, sent whole, with x-nextjs-cache HIT.first visitLoading the tea…the shell, sent firstOolong tea · $5 a boxstreamed inkeptsecond visitOolong tea · $5 a boxsent whole: HITone address, two visits
The first visit to /teas/oolong starts with the shell for unlisted teas, and the tea streams in. Next.js keeps the finished page, so the second visit gets it whole, and the catalog isn't asked again.

Sending the shell for an unlisted param right away is recent. The docs say the shell for unlisted params is served from Next.js 16.3; earlier versions waited for a full render on the server before sending anything. It needs Partial Prefetching on, as in new projects, and params awaited inside Suspense, as on line 11 of the page. Partial Prerendering showed how a shell and its holes are made.

dynamicParams, and teas the shop doesn't have

Without Cache Components, as in projects that don't turn them on, a page can export dynamicParams: true, the default, renders an unlisted param on request; false answers 404 for it. It replaced the fallback option of getStaticPaths in the Pages Router. The docs also say it isn't available with Cache Components. Each button is a version of the tea page, built with real Next.js. Before you click, guess what each does for a tea that isn't listed.

Playground: a tea that isn't listed
The tea shop, with this version of app/teas/[slug]/page.tsxNext.js
app/teas/[slug]/lib/
app/teas/[slug]/page.tsx
import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export const dynamicParams = false; export async function generateStaticParams() {  const teas = await getPopularTeas();  return teas.map((tea) => ({ slug: tea.slug }));} export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {  const { slug } = await params;  const tea = await getTea(slug);  if (!tea) notFound();  return (    <main>      <h1>{tea.name}</h1>      <p>${tea.price} a box</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /teas/oolong404

The whole response is .next/server/app/_not-found.html, a file next build wrote, byte for byte (the Terminal tab lists it).

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
Cache-Control private, no-cache, no-store, max-age=0, must-revalidate
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 10 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1x6obpmlu1_c2.js">
    <script src="/_next/static/chunks/2r4s4h5xul1n-.js" async=""></script>
    <script src="/_next/static/chunks/3wbm-994r45kr.js" async=""></script>
    <script src="/_next/static/chunks/2e268anpfvdoc.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/27gjg8e11zukd.js" async=""></script>
    <meta name="robots" content="noindex">
    <title>Tea shop</title>
    <title>404: This page could not be found.</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <nav><a href="/">All teas</a></nav>
    <div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center">
      <div>
        <style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style>
        <h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1>
        <div style="display:inline-block">
          <h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2>
        </div>
      </div>
    </div>
    <!--$-->
    <!--/$-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/1x6obpmlu1_c2.js" async=""></script>
    <script>…</script>
    <script>…</script>
  </body>
</html>
Try it yourself
dynamicParams, without Cache Components

In a project without Cache Components, line 4 turns away every tea that isn't listed. The build lists ● /teas/green and ● /teas/mint, and /teas/oolong answers 404 with the not-found page, though the catalog has oolong.

Every version was built with next build from Next.js 16.4.3, with the same stand-in catalog, which has green, mint and oolong. The Network tab opens on a tea that isn't listed. Reset goes back to the first version.

Without Cache Components, dynamicParams = false on line 4 kept the build to the listed teas: the route table lists ● /teas/green and ● /teas/mint, where ● is (SSG) prerendered as static HTML (uses generateStaticParams), and /teas/oolong answered 404. With Cache Components on, the same line stops the build: Route segment config "dynamicParams" is not compatible with `nextConfig.cacheComponents`. Please remove it. Each row compares the three versions:

A tea that isn't listed, three ways
The pageWhat next build didWhat an unlisted tea got
dynamicParams = false, without Cache ComponentsBuilds: ● /teas/green, ● /teas/mint.404, the not-found page, even for a tea the catalog has.
dynamicParams = false, with Cache ComponentsStops: the option is not compatible with cacheComponents.Nothing: there's no build to run.
notFound() in the page, with Cache ComponentsBuilds: ○ /teas/green, ○ /teas/mint, ◐ /teas/[slug].Rendered on request: oolong shows; rooibos 200 with noindex, then 404 when asked again.

With Cache Components, every param the list leaves out is rendered on request. To turn away a slug that has no tea, do what the migration guide says: call notFound() when the data isn't there, as line 13 does. The first visit to /teas/rooibos answered 200, because the shell had already been sent when notFound() ran, with a noindex tag that tells search engines to skip it; asked for again, it answered 404 from the kept result. Dynamic routes recorded the same, and Not found, forbidden and unauthorized explains the status codes.

Nested dynamic segments

The shop sorts its teas by kind: /teas/green/sencha, /teas/black/assam. That's two dynamic segments, app/teas/[kind]/[slug], and each can list its own params. The kind's layout lists the kinds, and the page lists the popular teas of each kind:

Tea kinds and teas, each listed by its own segmentNext.js
app/teas/[kind]/[slug]/lib/
app/teas/[kind]/[slug]/page.tsx
import { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams({ params }: { params: { kind: string } }) {  const teas = await getPopularTeas(params.kind);  return teas.map((tea) => ({ slug: tea.slug }));} async function TeaName({ params }: Pick<PageProps<"/teas/[kind]/[slug]">, "params">) {  const { kind, slug } = await params;  const tea = await getTea(kind, slug);  if (!tea) notFound();  return <p>{tea.name}</p>;} export default function TeaPage({ params }: PageProps<"/teas/[kind]/[slug]">) {  return (    <Suspense fallback={<p>Loading the tea…</p>}>      <TeaName params={params} />    </Suspense>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (9/9)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└   /teas/[kind]/[slug]  ├ ◐ /teas/[kind]/[slug]  ├ ◐ /teas/green/[slug]  ├ ○ /teas/green/sencha  └ ◐ [+3 more paths] ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html  9.2 kB.next/server/app/_global-error.meta  681 bytes.next/server/app/_global-error.rsc  4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc  4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc  4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc  115 bytes.next/server/app/_not-found.html  7.6 kB.next/server/app/_not-found.meta  774 bytes.next/server/app/_not-found.rsc  4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc  4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc  4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc  172 bytes.next/server/app/index.html  5.0 kB.next/server/app/index.meta  617 bytes.next/server/app/index.rsc  3.0 kB.next/server/app/index.segments/__PAGE__.segment.rsc  2.7 kB.next/server/app/index.segments/_full.segment.rsc  3.0 kB.next/server/app/index.segments/_tree.segment.rsc  115 bytes.next/server/app/teas/[kind]/[slug].html  1.4 kB.next/server/app/teas/[kind]/[slug].meta  4.8 kB.next/server/app/teas/[kind]/[slug].segments/_full.segment.rsc  4.5 kB.next/server/app/teas/[kind]/[slug].segments/_tree.segment.rsc  325 bytes.next/server/app/teas/[kind]/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.6 kB.next/server/app/teas/black/[slug].html  1.4 kB.next/server/app/teas/black/[slug].meta  4.7 kB.next/server/app/teas/black/[slug].segments/_full.segment.rsc  4.6 kB.next/server/app/teas/black/[slug].segments/_tree.segment.rsc  325 bytes.next/server/app/teas/black/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.7 kB.next/server/app/teas/black/assam.html  7.2 kB.next/server/app/teas/black/assam.meta  1.1 kB.next/server/app/teas/black/assam.rsc  4.6 kB.next/server/app/teas/black/assam.segments/_full.segment.rsc  4.6 kB.next/server/app/teas/black/assam.segments/_tree.segment.rsc  325 bytes.next/server/app/teas/black/assam.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.7 kB.next/server/app/teas/green/[slug].html  1.4 kB.next/server/app/teas/green/[slug].meta  4.8 kB.next/server/app/teas/green/[slug].segments/_full.segment.rsc  4.6 kB.next/server/app/teas/green/[slug].segments/_tree.segment.rsc  325 bytes.next/server/app/teas/green/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.7 kB.next/server/app/teas/green/matcha.html  7.2 kB.next/server/app/teas/green/matcha.meta  1.1 kB.next/server/app/teas/green/matcha.rsc  4.6 kB.next/server/app/teas/green/matcha.segments/_full.segment.rsc  4.6 kB.next/server/app/teas/green/matcha.segments/_tree.segment.rsc  325 bytes.next/server/app/teas/green/matcha.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.7 kB.next/server/app/teas/green/sencha.html  7.2 kB.next/server/app/teas/green/sencha.meta  1.1 kB.next/server/app/teas/green/sencha.rsc  4.6 kB.next/server/app/teas/green/sencha.segments/_full.segment.rsc  4.6 kB.next/server/app/teas/green/sencha.segments/_tree.segment.rsc  325 bytes.next/server/app/teas/green/sencha.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc  3.7 kB

In app/teas/[kind]/layout.tsx, lines 4 to 7 list the kinds from the catalog: green and black. In app/teas/[kind]/[slug]/page.tsx, the page's generateStaticParams on line 5 takes an argument: params, with one kind the layout listed. It's a plain object, not a promise, and it holds only the parent's params. Line 6 asks the catalog for that kind's popular teas, and line 7 returns their slugs.

The docs say a child's generateStaticParams runs once for each set of params the parent's returns. The stand-in's requests show it: during the build, /kinds once, then /kinds/green/popular once and /kinds/black/popular once. The route table shows only a few of the pages, then [+3 more paths]; the list of files has all of them: teas/green/sencha.html, teas/green/matcha.html, teas/black/assam.html, a shell for each kind's unlisted teas, like teas/green/[slug].html, and teas/[kind]/[slug].html for kinds that aren't listed.

A tag for the layout, whose generateStaticParams lists the kinds, leads to two cards, kind green and kind black. From green, an arrow leads to a tag for the page's generateStaticParams with kind green, which lists sencha and matcha. From black, an arrow leads to a tag for the page's generateStaticParams with kind black, which lists assam.layoutkind: "green"kind: "black"page runs: greensenchamatchapage runs: blackassamthe page's generateStaticParams runs once per kind
The layout's generateStaticParams lists the kinds. The page's runs once for each, with that kind in params, and lists its teas: three pages, teas/green/sencha, teas/green/matcha and teas/black/assam.

Pick the requests in the Network tab. /teas/green/gyokuro, a green tea that isn't listed, began with teas/green/[slug].html: the heading green teas was already in it, with only the tea left to stream. /teas/herbal/mint, a kind that isn't listed, began with teas/[kind]/[slug].html, with both fallbacks. There are two ways to list nested params, and the docs show both:

  • From the top down, as here: the parent lists its segment, and the child lists its own for each parent value.
  • From the bottom up: the page alone returns every combination, like { kind: "green", slug: "sencha" }. A layout can list params for its own segment and the ones above it, never for the segments below it.

What the build needs with Cache Components

Dynamic routes showed that with Cache Components on, a page that awaits params needs either the list or a Suspense. With a list, there's more to get right. Each button is one version of the tea page, built with real Next.js. Before you click, guess which version gives an unlisted tea its fallback at once.

Playground: what does the build need?
The tea shop, with this version of app/teas/[slug]/page.tsxNext.js
app/teas/[slug]/lib/
app/teas/[slug]/page.tsx
import { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams() {  const teas = await getPopularTeas();  return teas.map((tea) => ({ slug: tea.slug }));} async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {  const { slug } = await params;  const tea = await getTea(slug);  if (!tea) notFound();  return (    <>      <h1>{tea.name}</h1>      <p>${tea.price} a box</p>    </>  );} export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {  return (    <main>      <Suspense fallback={<p>Loading the tea…</p>}>        <TeaDetails params={params} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (6/6)  Finalizing page optimization ... Route (app)         Revalidate  Expire┌ ○ /                      15m      1y├ ○ /_not-found└   /teas/[slug]  ├ ◐ /teas/[slug]  ├ ○ /teas/green          15m      1y  └ ○ /teas/mint           15m      1y ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html  9.2 kB.next/server/app/_global-error.meta  681 bytes.next/server/app/_global-error.rsc  4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc  4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc  4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc  115 bytes.next/server/app/_not-found.html  7.6 kB.next/server/app/_not-found.meta  774 bytes.next/server/app/_not-found.rsc  4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc  4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc  4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc  172 bytes.next/server/app/index.html  5.6 kB.next/server/app/index.meta  617 bytes.next/server/app/index.rsc  3.4 kB.next/server/app/index.segments/__PAGE__.segment.rsc  3.0 kB.next/server/app/index.segments/_full.segment.rsc  3.3 kB.next/server/app/index.segments/_tree.segment.rsc  115 bytes.next/server/app/teas/[slug].html  1.3 kB.next/server/app/teas/[slug].meta  4.2 kB.next/server/app/teas/[slug].segments/_full.segment.rsc  4.0 kB.next/server/app/teas/[slug].segments/_tree.segment.rsc  245 bytes.next/server/app/teas/[slug].segments/teas/$d$slug/__PAGE__.segment.rsc  3.2 kB.next/server/app/teas/green.html  6.5 kB.next/server/app/teas/green.meta  914 bytes.next/server/app/teas/green.rsc  4.0 kB.next/server/app/teas/green.segments/_full.segment.rsc  4.0 kB.next/server/app/teas/green.segments/_tree.segment.rsc  245 bytes.next/server/app/teas/green.segments/teas/$d$slug/__PAGE__.segment.rsc  3.3 kB.next/server/app/teas/mint.html  6.5 kB.next/server/app/teas/mint.meta  912 bytes.next/server/app/teas/mint.rsc  4.0 kB.next/server/app/teas/mint.segments/_full.segment.rsc  4.0 kB.next/server/app/teas/mint.segments/_tree.segment.rsc  245 bytes.next/server/app/teas/mint.segments/teas/$d$slug/__PAGE__.segment.rsc  3.3 kB
Try it yourself
params inside Suspense

The popular teas are listed on lines 5 to 8, and TeaDetails waits for params inside the Suspense on lines 25 to 27. The build makes ○ /teas/green and ○ /teas/mint, and a shell for every other tea, teas/[slug].html, with Loading the tea….

Every version was built with next build from Next.js 16.4.3, with Cache Components on, and the same stand-in catalog. The Terminal shows the route table and the files the build wrote; the Network tab shows what next start sent. Reset goes back to the first version.

Each row is one version: what the build did, and what the first visit to /teas/oolong got.

Four tea pages with Cache Components on
The pageWhat next build didThe first visit to an unlisted tea
params inside Suspense, popular teas listedListed teas ○, a shell for the restThe shell with Loading the tea… at once, then the tea; the second visit got the kept page.
params at the top, popular teas listedListed teas ○, an empty shell for the restNothing until the whole page was rendered.
No generateStaticParamsOnly ◐ /teas/[slug]The shell, then the tea, on every visit: never a kept page.
generateStaticParams returns []StopsNothing: there's no build to run.

So the build needs three things from a route with a dynamic segment:

  • At least one param. An empty list stops the build: When using Cache Components, all `generateStaticParams` functions must return at least one result. The build renders the route with a real param to check it, for example that it doesn't read cookies() outside Suspense.
  • params awaited inside Suspense. Then the shell for unlisted params holds the layout and the fallback. Awaited at the top, the shell was empty, and the first visitor waited for the whole page.
  • The list itself, to keep pages. Without generateStaticParams, every visit to /teas/green was rendered again, from the shell: the migration guide says removing the export opts the route out of ISR, even when its data is cached.

Choosing which params to list

Every page you list makes the build longer and has to be stored and deployed. The docs suggest listing the pages that gain most from being ready ahead, like the most visited ones, and letting the rest be made on their first visit. For the shop:

  • List the popular teas, so most visitors get a page made during the build.
  • Leave the rest on demand. The first visitor gets the shell at once, and everyone after gets the kept page.
  • Call notFound() for slugs with no data, since Cache Components renders every unlisted slug.

Sort these requests by what their response starts with, as the recordings showed:

What does this response start with?
  • /teas/green
  • /teas/mint
  • /teas/oolong, its first visit
  • /teas/oolong, asked for again
  • /teas/green/sencha, in the shop with kinds
  • /teas/green/gyokuro, its first visit
Try it yourself
0 of 6 correct

Sort each request: does its response start with a page the build made, with a shell for params the build didn't list, or with a page kept from an earlier visit?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Only the listed params work." No. Every other value is rendered on its first visit: oolong showed, though it wasn't listed.
  • "An unlisted page is rendered for every visit." No. Next.js kept it: the second visit was a HIT, and the catalog was asked once.
  • "dynamicParams = false blocks unlisted params." Only without Cache Components. With them, the build stops; call notFound() instead.
  • "Return [] to make every page on demand." Not with Cache Components: the build stops. Return at least one real param.
  • "A child's generateStaticParams runs once." No. It runs once for each set of params its parent returns: twice for two kinds.
  • "The child gets params as a promise." No. In generateStaticParams, the parent's params are a plain object. Pages and layouts get a promise.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints. They use the same stand-in catalog: green, mint and oolong, with green and mint popular.

Exercise 1 · Warm-upFind the pages the build makes

Which tea pages does next build make ahead for this page?

Starter codeTSX
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";

export async function generateStaticParams() {
  const teas = await getPopularTeas();
  return teas.map((tea) => ({ slug: tea.slug }));
}

async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
  const { slug } = await params;
  const tea = await getTea(slug);
  if (!tea) notFound();
  return (
    <>
      <h1>{tea.name}</h1>
      <p>${tea.price} a box</p>
    </>
  );
}

export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
  return (
    <main>
      <Suspense fallback={<p>Loading the tea…</p>}>
        <TeaDetails params={params} />
      </Suspense>
    </main>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upThe first visit to an unlisted tea

    The first visitor opens /teas/oolong. Besides the nav, what does the first part of the HTML show?

    Starter codeTSX
    import { Suspense } from "react";
    import { notFound } from "next/navigation";
    import { getPopularTeas, getTea } from "@/lib/catalog";
    
    export async function generateStaticParams() {
      const teas = await getPopularTeas();
      return teas.map((tea) => ({ slug: tea.slug }));
    }
    
    async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
      const { slug } = await params;
      const tea = await getTea(slug);
      if (!tea) notFound();
      return (
        <>
          <h1>{tea.name}</h1>
          <p>${tea.price} a box</p>
        </>
      );
    }
    
    export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
      return (
        <main>
          <Suspense fallback={<p>Loading the tea…</p>}>
            <TeaDetails params={params} />
          </Suspense>
        </main>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCount the catalog's requests

      Two visitors open /teas/oolong, one after the other. How many times does the catalog get a request for /teas/oolong?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the build

        With Cache Components on, this page stops the build. Which line do you remove?

        Starter codeTSX
        import { Suspense } from "react";
        import { notFound } from "next/navigation";
        import { getPopularTeas, getTea } from "@/lib/catalog";
        
        export const dynamicParams = false;
        
        export async function generateStaticParams() {
          const teas = await getPopularTeas();
          return teas.map((tea) => ({ slug: tea.slug }));
        }
        
        async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
          const { slug } = await params;
          const tea = await getTea(slug);
          if (!tea) notFound();
          return (
            <>
              <h1>{tea.name}</h1>
              <p>${tea.price} a box</p>
            </>
          );
        }
        
        export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
          return (
            <main>
              <Suspense fallback={<p>Loading the tea…</p>}>
                <TeaDetails params={params} />
              </Suspense>
            </main>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeCount the child's runs

          This is app/teas/[kind]/layout.tsx, and the page below it has its own generateStaticParams. How many times does the page's run during the build?

          Starter codeTSX
          import { Suspense } from "react";
          import { getKinds } from "@/lib/catalog";
          
          export async function generateStaticParams() {
            const kinds = await getKinds();
            return kinds.map((kind) => ({ kind }));
          }
          
          async function KindHeading({ params }: Pick<LayoutProps<"/teas/[kind]">, "params">) {
            const { kind } = await params;
            return <h1>{kind} teas</h1>;
          }
          
          export default function KindLayout({ children, params }: LayoutProps<"/teas/[kind]">) {
            return (
              <section>
                <Suspense fallback={<h1>Loading the kind…</h1>}>
                  <KindHeading params={params} />
                </Suspense>
                {children}
              </section>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeAn empty list

            This page wants every tea made on demand, so it returns []. With Cache Components on, the build stops. How many params must the list hold, at least?

            Starter codeTSX
            import { Suspense } from "react";
            import { notFound } from "next/navigation";
            import { getTea } from "@/lib/catalog";
            
            export async function generateStaticParams() {
              return [];
            }
            
            async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
              const { slug } = await params;
              const tea = await getTea(slug);
              if (!tea) notFound();
              return (
                <>
                  <h1>{tea.name}</h1>
                  <p>${tea.price} a box</p>
                </>
              );
            }
            
            export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
              return (
                <main>
                  <Suspense fallback={<p>Loading the tea…</p>}>
                    <TeaDetails params={params} />
                  </Suspense>
                </main>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which values the build knew, and what the build made for the rest.

              The generateStaticParams quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does generateStaticParams in app/teas/[slug]/page.tsx return?

                Choose an answer to see the explanation.

              2. Question 2 of 7The build prints /teas/[slug] with ◐ /teas/[slug], ○ /teas/green and ○ /teas/mint under it. What is ◐ /teas/[slug] for?

                Choose an answer to see the explanation.

              3. Question 3 of 7A second visitor asks for /teas/oolong, which isn't listed. What do they get?

                Choose an answer to see the explanation.

              4. Question 4 of 7This page runs in a project with Cache Components on. What does next build do?

                Read the code, then predictTSX
                import { Suspense } from "react";
                import { notFound } from "next/navigation";
                import { getPopularTeas, getTea } from "@/lib/catalog";
                
                export const dynamicParams = false;
                
                export async function generateStaticParams() {
                  const teas = await getPopularTeas();
                  return teas.map((tea) => ({ slug: tea.slug }));
                }
                
                async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
                  const { slug } = await params;
                  const tea = await getTea(slug);
                  if (!tea) notFound();
                  return (
                    <>
                      <h1>{tea.name}</h1>
                      <p>${tea.price} a box</p>
                    </>
                  );
                }
                
                export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
                  return (
                    <main>
                      <Suspense fallback={<p>Loading the tea…</p>}>
                        <TeaDetails params={params} />
                      </Suspense>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              5. Question 5 of 7In the shop with kinds, the layout's generateStaticParams returns green and black. How many times does the page's own generateStaticParams run?

                Read the code, then predictTSX
                import { Suspense } from "react";
                import { notFound } from "next/navigation";
                import { getPopularTeas, getTea } from "@/lib/catalog";
                
                export async function generateStaticParams({ params }: { params: { kind: string } }) {
                  const teas = await getPopularTeas(params.kind);
                  return teas.map((tea) => ({ slug: tea.slug }));
                }
                
                async function TeaName({ params }: Pick<PageProps<"/teas/[kind]/[slug]">, "params">) {
                  const { kind, slug } = await params;
                  const tea = await getTea(kind, slug);
                  if (!tea) notFound();
                  return <p>{tea.name}</p>;
                }
                
                export default function TeaPage({ params }: PageProps<"/teas/[kind]/[slug]">) {
                  return (
                    <Suspense fallback={<p>Loading the tea…</p>}>
                      <TeaName params={params} />
                    </Suspense>
                  );
                }
                

                Choose an answer to see the explanation.

              6. Question 6 of 7With Cache Components on, this generateStaticParams returns []. What happens?

                Read the code, then predictTSX
                import { Suspense } from "react";
                import { notFound } from "next/navigation";
                import { getTea } from "@/lib/catalog";
                
                export async function generateStaticParams() {
                  return [];
                }
                
                async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
                  const { slug } = await params;
                  const tea = await getTea(slug);
                  if (!tea) notFound();
                  return (
                    <>
                      <h1>{tea.name}</h1>
                      <p>${tea.price} a box</p>
                    </>
                  );
                }
                
                export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
                  return (
                    <main>
                      <Suspense fallback={<p>Loading the tea…</p>}>
                        <TeaDetails params={params} />
                      </Suspense>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7This page waits for params on line 10, outside any Suspense. What does the first visitor to /teas/oolong get first?

                Read the code, then predictTSX
                import { notFound } from "next/navigation";
                import { getPopularTeas, getTea } from "@/lib/catalog";
                
                export async function generateStaticParams() {
                  const teas = await getPopularTeas();
                  return teas.map((tea) => ({ slug: tea.slug }));
                }
                
                export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {
                  const { slug } = await params;
                  const tea = await getTea(slug);
                  if (!tea) notFound();
                  return (
                    <main>
                      <h1>{tea.name}</h1>
                      <p>${tea.price} a box</p>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • generateStaticParams returns a list of params objects; next build makes a page for each, and a shell for every other value.
              • An unlisted value is rendered on its first visit, from that shell, and the page is kept: the second visit got it whole.
              • dynamicParams doesn't work with Cache Components: the build stops. Call notFound() for values with no data.
              • A child's generateStaticParams runs once for each set of its parent's params, which it gets as a plain object.
              • With Cache Components: return at least one param, await params inside Suspense, and keep the list, or no page is kept.

              Remember the one-liner.
              List the pages worth making ahead; the rest are made on their first visit and kept.

              Coming next: Cache Components. It explains the model behind "use cache", where everything is fresh unless you cache it, and what changed from older versions.

              CompleteFrontend Clear concepts. Working examples.