Layouts and Routing · Lesson 5 of 24
Next.js Dynamic Routes and generateStaticParams
A course platform has hundreds of course pages, a shop thousands of product pages – all with the same layout and different data. Instead of a folder per item, you create one dynamic segment by wrapping a folder name in square brackets: app/courses/[slug]/page.tsx matches /courses/react, /courses/nextjs and so on.
In this lesson
- Dynamic segments and the
paramsprop - Prerendering pages with
generateStaticParams - Showing a 404 for unknown slugs
- Catch-all and optional catch-all routes
A dynamic course page
We'll keep the data in a small module – in a real app this would query a database or CMS:
// lib/courses.ts
export type Course = { slug: string; title: string; lessons: number; level: string };
const courses: Course[] = [
{ slug: "react", title: "React.js", lessons: 31, level: "Beginner to Pro" },
{ slug: "nextjs", title: "Next.js", lessons: 24, level: "Intermediate" },
{ slug: "typescript", title: "TypeScript", lessons: 17, level: "Intermediate" },
];
export async function getCourses() {
return courses;
}
export async function getCourse(slug: string) {
return courses.find((c) => c.slug === slug) ?? null;
}
// app/courses/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getCourse, getCourses } from "@/lib/courses";
// Prerender these pages at build time
export async function generateStaticParams() {
const courses = await getCourses();
return courses.map((c) => ({ slug: c.slug }));
}
export default async function CoursePage({ params }: PageProps<"/courses/[slug]">) {
const { slug } = await params; // params is a Promise
const course = await getCourse(slug);
if (!course) notFound(); // renders the 404 page
return (
<main>
<h1>{course.title}</h1>
<p>{course.lessons} lessons · {course.level}</p>
</main>
);
}
// app/page.tsx
import Link from "next/link";
import { getCourses } from "@/lib/courses";
export default async function Home() {
const courses = await getCourses();
return (
<ul>
{courses.map((c) => (
<li key={c.slug}><Link href={`/courses/${c.slug}`}>{c.title}</Link></li>
))}
<li><Link href="/courses/vue">Vue (no such course)</Link></li>
</ul>
);
}
Route (app) ┌ ○ / └ /courses/[slug] ├ ◐ /courses/[slug] ├ ○ /courses/react ├ ○ /courses/nextjs └ ○ /courses/typescript ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content URL: / • React.js • Next.js • TypeScript • Vue (no such course) › click “Next.js” URL: /courses/nextjs Next.js 24 lessons · Intermediate › open /courses/vue URL: /courses/vue 404 This page could not be found.
paramsis a Promise –awaitit in async components.PageProps<"/courses/[slug]">is a type Next.js generates from your folders, soslugis typed as a string automatically.generateStaticParamsreturns the slugs to build ahead of time. The build output lists each prerendered page under the route.notFound()stops rendering and shows the nearestnot-found.tsx(or the default 404 page), and tells search engines not to index it. The last lesson in this chapter explains the HTTP status it gets.- Slugs not returned by
generateStaticParamsare still rendered on demand the first time they're requested.
Without generateStaticParams
If you can't list the values in advance, the params are only known when a request arrives. With Cache Components, code that waits for request-time data must be wrapped in <Suspense>, so Next.js can still send the rest of the page instantly:
// app/orders/[id]/page.tsx
import { Suspense } from "react";
async function OrderDetails({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <p>Order #{id}: 2 items, shipped</p>;
}
export default function OrderPage({ params }: PageProps<"/orders/[id]">) {
return (
<main>
<h1>Your order</h1>
<Suspense fallback={<p>Loading order…</p>}>
<OrderDetails params={params} />
</Suspense>
</main>
);
}
Route (app) └ /orders/[id] └ ◐ /orders/[id] ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content Your order Order #4521: 2 items, shipped
The route is marked ◐ Partial Prerender: the heading and fallback form a static shell built at deploy time, and the order details are filled in per request. If you forget the Suspense, next build refuses to continue and tells you how to fix it:
// app/orders/[id]/page.tsx
export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
const { id } = await params; // request-time data, no Suspense around it
return <H2>Order #{id};
}
Error: Route "/orders/[id]": 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
Error occurred prerendering page "/orders/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /orders/[id]/page: /orders/[id], exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: nullThe three fixes it lists are the three tools of Next.js 16 rendering: stream it behind Suspense, cache it with "use cache", or deliberately allow a blocking route. You'll use all three in this course.
Catch-all segments
| Folder | Matches | params |
|---|---|---|
[slug] | /docs/intro | { slug: "intro" } |
[...slug] | /docs/a, /docs/a/b/c (not /docs) | { slug: ["a", "b", "c"] } |
[[...slug]] | the same, and /docs | { slug: undefined } for /docs |
// app/docs/[[...slug]]/page.tsx
export function generateStaticParams() {
return [{ slug: [] }, { slug: ["routing"] }, { slug: ["routing", "dynamic-routes"] }];
}
export default async function Docs({ params }: PageProps<"/docs/[[...slug]]">) {
const { slug = [] } = await params;
return (
<main>
<p>Docs / {slug.join(" / ") || "home"}</p>
<p>{slug.length} segment(s)</p>
</main>
);
}
Docs / home 0 segment(s) › open /docs/routing/dynamic-routes Docs / routing / dynamic-routes 2 segment(s)
Practice
Try it yourself
Create
/blog/[slug]for two posts withgenerateStaticParams, showing the slug converted to a title ("my-first-post" → "My first post").Show solution
// app/blog/[slug]/page.tsx export function generateStaticParams() { return [{ slug: "my-first-post" }, { slug: "learning-nextjs" }]; } export default async function Post({ params }: PageProps<"/blog/[slug]">) { const { slug } = await params; const title = slug.replaceAll("-", " "); return <h1>{title.charAt(0).toUpperCase() + title.slice(1)}</h1>; }Route (app) └ /blog/[slug] ├ ◐ /blog/[slug] ├ ○ /blog/my-first-post └ ○ /blog/learning-nextjs ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content My first post
Quick check
How do you read params in a Next.js 16 page?
const { slug } = await params; – params is a Promise.
What does generateStaticParams do?
Returns the param values to prerender at build time.
What's the difference between [...slug] and [[...slug]]?
The optional version also matches the route without any segments.
Summary
- [slug] folders create dynamic routes; await the params Promise to read them.
- generateStaticParams prerenders known pages; others render on demand.
- Call notFound() for missing data; wrap request-time params in Suspense when they aren't prerendered.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.