Next.js · lesson 7 of 11
Dynamic Routes and generateStaticParams
A Next.js 16 [slug] route awaits params as a Promise; generateStaticParams prerenders known paths, and Cache Components rejects dynamicParams: false.
By SkillByExample · Published
Dynamic by Default: Rendering Modes showed connection() and
cookies() failing a build outside <Suspense> as request APIs. params belongs to that
same request-API category — distinct from the uncached data category fetch() falls
into, which Caching with “use cache” covers separately.
This lesson builds a [slug] dynamic segment and watches that rule apply to params
specifically: reading it fails the build on its own, generateStaticParams is the fix
that’s unique to a dynamic route, and the route table ends up showing exactly which slugs
got prerendered and which didn’t. The project setup reuses the Docker pattern from the
first lesson — the node:26.13 image, the
nextjs-modules volume, MSYS_NO_PATHCONV=1 on Git Bash — see it for why each flag is
there.
params Fails the Build the Same Way connection() and cookies() Do
Five files set up the project: package.json, next.config.ts, tsconfig.json, and
app/layout.tsx are unchanged from the earlier lessons; app/page.tsx carries a new comment,
since this lesson’s new route is /posts/[slug].
{
"name": "next-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.4.3",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"typescript": "5.9.3",
"@types/node": "20.19.43",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0"
}
}
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
// app/layout.tsx — required; every page in the app renders inside this
export const metadata = {
title: "Next App",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/page.tsx — a plain static home page; this lesson's new route is /posts/[slug]
export default function HomePage() {
return <h1>Home</h1>;
}
app/posts/[slug]/page.tsx awaits params — typed as a Promise, per Next.js 16 — and looks
the resolved slug up in a small in-memory table. No generateStaticParams yet. Predict
whether this builds at all, given that Dynamic by Default: Rendering
Modes already showed connection() and cookies() failing the
same way outside <Suspense>:
// app/posts/[slug]/page.tsx — no generateStaticParams yet
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug] ?? "Unknown post";
return <p>Post: {title}</p>;
}
npm install is shown here, not hidden, so a reader running the same two commands sees the
same install happen — though its exact duration will differ from the one below:
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npm install --no-audit --no-fund
added 28 packages in 45s
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 263ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 12.0s
Running TypeScript ...
Finished TypeScript in 6.0s ...
Error: Route "/posts/[slug]": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
- [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
at body (<anonymous>)
at html (<anonymous>)
To get a more detailed stack trace and pinpoint the issue, try one of the following:
- Start the app in development mode by running `next dev`, then open "/posts/[slug]" in your browser to investigate the error.
- Rerun the production build with `next build --debug-prerender` to generate better stack traces.
Error occurred prerendering page "/posts/[slug]". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /posts/[slug]/page: /posts/[slug], exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null
The exact same generic error Dynamic by Default: Rendering
Modes showed for connection() — and the fixed list it prints
literally includes params alongside fetch(...), cookies(), and headers(). A dynamic
segment’s params is request-specific data too; Cache Components doesn’t treat it any
differently just because it came from the URL instead of a header.
generateStaticParams Prerenders a Known Set
The [cache]/[stream]/[block] fixes the error lists aren’t the only option for a dynamic
segment specifically: generateStaticParams tells Next.js exactly which slug values exist
ahead of time, so it can prerender each one to real static HTML instead of treating every
params read as unknowable:
// app/posts/[slug]/page.tsx — generateStaticParams supplies the known slugs
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export async function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug] ?? "Unknown post";
return <p>Post: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 136ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 3.4s
Running TypeScript ...
Finished TypeScript in 5.4s ...
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
└ /posts/[slug]
├ ◐ /posts/[slug]
├ ○ /posts/hello-world
└ ○ /posts/second-post
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
The build succeeds now, and the table grows a nested shape that no earlier lesson’s table has
shown: /posts/[slug] itself gets ◐ — a shell for any slug not in the known set — and the
two slugs generateStaticParams actually returned each get their own ○ row underneath,
fully prerendered as real static HTML files, /posts/hello-world and /posts/second-post.
An Unlisted Slug Still Renders — On Demand
Start the app and request a known slug, then a slug generateStaticParams never returned:
$ docker run -d --name nextjs-dynroutes-1 -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
19ba020d68cb003761557013bed348ab27c873b5fa8afd90e1a0ef881510d029
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ curl -s -D - -o /dev/null http://localhost:3100/posts/hello-world | grep -iE '^HTTP/|^cache-control|^x-nextjs'
HTTP/1.1 200 OK
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
Cache-Control: s-maxage=31536000
$ curl -s -D - -o /dev/null http://localhost:3100/posts/not-a-real-post | grep -iE '^HTTP/|^cache-control|^x-nextjs'
HTTP/1.1 200 OK
x-nextjs-stale-time: 300
x-nextjs-prerender: 1
x-nextjs-postponed: 1
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
$ curl -s http://localhost:3100/posts/not-a-real-post | grep -oE '<p>Post: [^/]+/p>'
<p>Post: <!-- -->Unknown post</p>
$ docker rm -f nextjs-dynroutes-1
nextjs-dynroutes-1
Both return 200, but the headers tell two different stories. /posts/hello-world carries
x-nextjs-cache: HIT and a year-long Cache-Control: s-maxage=31536000 — the exact static
file the build already produced. /posts/not-a-real-post carries x-nextjs-postponed: 1
instead, and Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate — the
◐ shell’s
dynamic hole ran for real, on this request, computing "Unknown post" from scratch. This is
dynamicParams’s default of true: an unlisted slug isn’t an error, it’s just rendered the
slow way instead of the fast way.
Forgetting await Fails at Compile Time
params is typed Promise<Params>, not Params — indexing into it directly, without
await, is a type error, not a runtime one. Predict what the build reports:
// app/posts/[slug]/page.tsx — params read without await
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export async function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const title = posts[params.slug] ?? "Unknown post";
return <p>Post: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 132ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 2.6s
Running TypeScript ...
app/posts/[slug]/page.tsx(14,30): error TS2339: Property 'slug' does not exist on type 'Promise<Params>'.
Failed to type check.
error TS2339: Property 'slug' does not exist on type 'Promise<Params>' — TypeScript itself
catches this, during Running TypeScript, before Next.js ever gets to prerendering a single
route. Restore the await:
// app/posts/[slug]/page.tsx — await restored
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export async function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug] ?? "Unknown post";
return <p>Post: {title}</p>;
}
dynamicParams Doesn’t Mix With Cache Components
The pre-Cache-Components App Router idiom for “never render an unlisted param, 404 it
instead” is a route segment config, export const dynamicParams = false. Predict whether
it still works under Cache Components:
// app/posts/[slug]/page.tsx — the older dynamicParams flag
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export async function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export const dynamicParams = false;
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug] ?? "Unknown post";
return <p>Post: {title}</p>;
}
A real terminal shows the lines below in color — this build’s code frame is Turbopack’s own,
and NO_COLOR/FORCE_COLOR=0/TERM=dumb don’t turn it off, so the command strips the ANSI
codes visibly instead:
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 108ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
> Build error occurred
Error: Turbopack build failed with 1 error:
./app/posts/[slug]/page.tsx:13:14
Error: Route segment config "dynamicParams" is not compatible with `nextConfig.cacheComponents`. Please remove it.
11 | }
12 |
> 13 | export const dynamicParams = false;
| ^^^^^^^^^^^^^
14 |
15 | export default async function PostPage({ params }: { params: Promise<Params> }) {
16 | const { slug } = await params;
Ecmascript file had an error
at ignore-listed frames
Next.js’s own error names the flag and the fix: "dynamicParams" is not compatible with nextConfig.cacheComponents. Please remove it. Under Cache Components, every unlisted slug
goes through the same dynamic-by-default mechanism the previous section showed, whether a
route wants that or not — there’s no build-time flag to turn that off. The correct way to make
an unlisted slug behave like “not found” is in the route’s own code, with notFound():
// app/posts/[slug]/page.tsx — notFound() instead of dynamicParams: false
import { notFound } from "next/navigation";
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
};
export async function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug];
if (!title) notFound();
return <p>Post: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 92ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 2.8s
Running TypeScript ...
Finished TypeScript in 2.5s ...
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
└ /posts/[slug]
├ ◐ /posts/[slug]
├ ○ /posts/hello-world
└ ○ /posts/second-post
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
$ docker run -d --name nextjs-dynroutes-2 -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
1c9e80858bb26a05f6dc0e75253b77b18a5d4eb69758f78dfaf17c6a24a4efb9
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3100/posts/not-a-real-post
200
$ curl -s http://localhost:3100/posts/not-a-real-post > /tmp/nf.html; grep -oE 'This page could not be found\.' /tmp/nf.html
This page could not be found.
This page could not be found.
$ grep -oE '<meta name="robots"[^/]+/>' /tmp/nf.html
<meta name="robots" content="noindex"/>
$ docker rm -f nextjs-dynroutes-2
nextjs-dynroutes-2
The build succeeds, and the saved response really does carry Next’s own “This page could
not be found.” text for /posts/not-a-real-post — but the status line curl reports is
200, not 404. The ◐ shell already committed a 200 status to the response before the
streamed, notFound()-triggering part of the page ever ran; a response can’t rewrite a
status line it already sent. notFound() changes what a visitor sees, but not, by itself,
the status code a monitoring tool or a crawler would read from this particular route shape.
The grep prints the phrase twice because two different pieces of Next.js’s own built-in
not-found UI both match it: the page’s <title>, which reads “404: This page could not be
found.”, and the visible <h2>, which reads exactly “This page could not be found.” —
one match each, not one element repeated. Neither is plain HTML text in this response,
though; /posts/not-a-real-post ships a client-rendered error shell with an empty <body>,
and both strings live inside the same <script> tag’s React Server Components payload,
the data a browser uses to build and hydrate the page. What is plain HTML, and new on this
route specifically, is <meta name="robots" content="noindex"> in <head> — none of this
lesson’s known-slug pages carry it; Next.js adds it to a notFound() response so a crawler
that lands on an unlisted slug directly is told not to index it.
Practice
Each task below builds on the files the previous ones (and the main lesson above) left in place — do them in order.
1. Add a third known slug, "third-post", to the posts table, and have
generateStaticParams return it first in its list, ahead of "hello-world" and
"second-post" — a non-alphabetical order on purpose. Confirm whether the route table’s
nested rows follow that returned order or sort alphabetically regardless.
Solution
// app/posts/[slug]/page.tsx — a third known slug, listed first on purpose
import { notFound } from "next/navigation";
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
"third-post": "Third Post",
};
export async function generateStaticParams() {
return [{ slug: "third-post" }, { slug: "hello-world" }, { slug: "second-post" }];
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug];
if (!title) notFound();
return <p>Post: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 72ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 1859ms
Running TypeScript ...
Finished TypeScript in 2.5s ...
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
└ /posts/[slug]
├ ◐ /posts/[slug]
├ ○ /posts/third-post
├ ○ /posts/hello-world
└ ○ /posts/second-post
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
/posts/third-post lands as the first nested row, not the last — the table lists these
three slugs in exactly the order generateStaticParams returned them, third-post first,
not alphabetically (which would put hello-world first) and not in the order their keys
appear in the posts object (which no longer matches the returned array at all). A dynamic
segment’s nested rows follow generateStaticParams’s own return order. Practice 2
below confirms the same rule a second way, with the catch-all route’s /docs/intro landing
before the alphabetically-earlier /docs/guide/install.
2. Add a catch-all segment, app/docs/[...slug]/page.tsx, whose
generateStaticParams returns two multi-part paths, and confirm the table shows nested paths
with more than one segment.
Solution
// app/docs/[...slug]/page.tsx — a catch-all segment, slug is an array
type Params = { slug: string[] };
const docs: Record<string, string> = {
"intro": "Introduction",
"guide/install": "Installation Guide",
};
export async function generateStaticParams() {
return [{ slug: ["intro"] }, { slug: ["guide", "install"] }];
}
export default async function DocPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const path = slug.join("/");
const title = docs[path] ?? "Unknown doc";
return <p>Doc: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 81ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 3.6s
Running TypeScript ...
Finished TypeScript in 4.3s ...
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ /docs/[...slug]
│ ├ ◐ /docs/[...slug]
│ ├ ○ /docs/intro
│ └ ○ /docs/guide/install
└ /posts/[slug]
├ ◐ /posts/[slug]
├ ○ /posts/third-post
├ ○ /posts/hello-world
└ ○ /posts/second-post
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
[...slug] collects every segment after /docs/ into an array — generateStaticParams
returning { slug: ["guide", "install"] } prerenders /docs/guide/install as one static row,
two URL segments deep, right alongside the single-segment /docs/intro. The same ◐ shell
row still covers every path the catch-all’s generateStaticParams didn’t list.
3. Remove generateStaticParams from app/posts/[slug]/page.tsx entirely, but keep the
build working by moving the params read into a child component wrapped in <Suspense> —
the [stream] fix from Dynamic by Default: Rendering
Modes — and confirm no slug gets its own static row anymore.
Solution
// app/posts/[slug]/page.tsx — no generateStaticParams; params read behind Suspense instead
import { Suspense } from "react";
import { notFound } from "next/navigation";
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
"third-post": "Third Post",
};
async function Post({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug];
if (!title) notFound();
return <p>Post: {title}</p>;
}
export default function PostPage({ params }: { params: Promise<Params> }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<Post params={params} />
</Suspense>
);
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 106ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 2.4s
Running TypeScript ...
Finished TypeScript in 2.7s ...
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ /docs/[...slug]
│ ├ ◐ /docs/[...slug]
│ ├ ○ /docs/intro
│ └ ○ /docs/guide/install
└ /posts/[slug]
└ ◐ /posts/[slug]
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
/posts/[slug] collapses to a single ◐ row with no static children at all — <Suspense>
satisfies Cache Components’ rule the same generic way it does for any other request-specific
read, but without generateStaticParams there’s no known set of slugs left to prerender, so
every single request, known slug or not, now renders on demand behind the shell.
4. Restore generateStaticParams on app/posts/[slug]/page.tsx, but make it return an
empty array, []. Predict whether Cache Components treats “zero known slugs” the same as “no
generateStaticParams at all.”
Solution
// app/posts/[slug]/page.tsx — generateStaticParams returns an empty array
import { notFound } from "next/navigation";
type Params = { slug: string };
const posts: Record<string, string> = {
"hello-world": "Hello World",
"second-post": "Second Post",
"third-post": "Third Post",
};
export async function generateStaticParams() {
return [];
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const title = posts[slug];
if (!title) notFound();
return <p>Post: {title}</p>;
}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 69ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 2.6s
Running TypeScript ...
Finished TypeScript in 2.7s ...
Error: When using Cache Components, all `generateStaticParams` functions must return at least one result. This is to ensure that we can perform build-time validation that there is no other dynamic accesses that would cause a runtime error.
Learn more: https://nextjs.org/docs/messages/empty-generate-static-params
at <unknown> (app/posts/[slug]/page.tsx:13:10)
11 |
12 | export async function generateStaticParams() {
> 13 | return [];
| ^
14 | }
15 |
16 | export default async function PostPage({ params }: { params: Promise<Params> }) {
> Build error occurred
Error: Failed to collect page data for /posts/[slug]
at ignore-listed frames {
type: 'Error'
}
A different, more specific error than the one earlier in this lesson — not the generic
uncached-data message, but one naming generateStaticParams directly: it has to return at
least one result under Cache Components, specifically so Next.js has a real page it can use to
validate there’s no other dynamic access hiding in the same route. An empty list isn’t treated
as “no generateStaticParams” (Practice 3’s case, which built fine with <Suspense>) — it’s
treated as a configuration mistake and fails the build outright.