Dynamic Routes & generateStaticParams
Dynamic Routes in Next.js
Dynamic routes let you generate pages from data — blog posts, course lessons, product pages, user profiles. Instead of creating a file for every page, you create one template and let the data drive the content.
The Pattern
A folder with brackets creates a dynamic segment:
src/app/
├── blog/
│ ├── page.tsx → /blog (list of posts)
│ └── [slug]/
│ └── page.tsx → /blog/how-to-learn-react
The value in brackets becomes a named parameter available in params:
// src/app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Static Generation with generateStaticParams
For content you know ahead of time (blog posts, course lessons, product catalog), tell Next.js to generate static HTML at build time:
// src/app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({
slug: post.slug, // e.g., "how-to-learn-react"
}));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{post.title}</article>;
}
At build time, Next.js calls generateStaticParams, then calls the page component once per slug and saves the HTML. /blog/how-to-learn-react loads as a static file — instant, SEO-friendly, no server needed.
Nested Dynamic Routes
Combine multiple dynamic segments:
src/app/courses/[courseSlug]/[lessonSlug]/page.tsx
interface Props {
params: {
courseSlug: string;
lessonSlug: string;
};
}
export async function generateStaticParams() {
const courses = await getAllCourses();
// Return all course+lesson combinations
const params = [];
for (const course of courses) {
for (const lesson of course.lessons) {
params.push({
courseSlug: course.slug,
lessonSlug: lesson.slug,
});
}
}
return params;
}
export default async function LessonPage({ params }: Props) {
const lesson = await getLesson(params.courseSlug, params.lessonSlug);
return (
<div>
<h1>{lesson.title}</h1>
<p>{lesson.content}</p>
</div>
);
}
Catch-All Routes
Use [...slug] to match any number of segments:
src/app/docs/[...slug]/page.tsx
Matches:
/docs/intro→params.slug = ["intro"]/docs/react/hooks→params.slug = ["react", "hooks"]/docs/react/advanced/patterns→params.slug = ["react", "advanced", "patterns"]
export default async function DocsPage({ params }: { params: { slug: string[] } }) {
const path = params.slug.join("/"); // "react/advanced/patterns"
const doc = await getDoc(path);
return (
<div>
<nav>
{params.slug.map((segment, i) => (
<span key={i}>
<a href={`/docs/${params.slug.slice(0, i + 1).join("/")}`}>
{segment}
</a>
{i < params.slug.length - 1 && " / "}
</span>
))}
</nav>
<article>{doc.content}</article>
</div>
);
}
Use [[...slug]] (double brackets) for an optional catch-all that also matches the base route:
/docs→params.slug = undefined/docs/intro→params.slug = ["intro"]
Dynamic Metadata
Generate page titles and descriptions from the fetched data:
export async function generateMetadata({ params }: Props) {
const post = await getPost(params.slug);
if (!post) {
return { title: "Post Not Found" };
}
return {
title: `${post.title} — My Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
};
}
Next.js calls generateMetadata for each static param at build time, so every page gets unique SEO metadata without any runtime overhead.
Handling Missing Pages
When a dynamic route has no matching data:
import { notFound } from "next/navigation";
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) notFound(); // renders the nearest not-found.tsx
return <article>{post.title}</article>;
}
For static generation, notFound() at build time skips that param. At request time, it shows the 404 page.
dynamicParams
By default, if a user requests a slug that wasn't in generateStaticParams, Next.js tries to render it dynamically on the server. You can change this:
// Disallow unknown slugs — return 404 for anything not pre-generated
export const dynamicParams = false;
export async function generateStaticParams() {
return [{ slug: "react" }, { slug: "python" }];
}
With dynamicParams = false, /blog/some-unknown-post returns a 404 immediately instead of trying to fetch data.
Leave it at the default true for content that grows after deployment (user-submitted posts, new products added to the database).
Route Groups
Use (groupName) folders to organize routes without affecting the URL:
src/app/
├── (marketing)/
│ ├── page.tsx → /
│ ├── about/
│ │ └── page.tsx → /about
│ └── pricing/
│ └── page.tsx → /pricing
├── (app)/
│ ├── layout.tsx → Authenticated layout for /dashboard/**
│ ├── dashboard/
│ │ └── page.tsx → /dashboard
│ └── settings/
│ └── page.tsx → /settings
Route groups let you apply different layouts to different sections without the group name appearing in the URL. The marketing pages get a public layout. The app pages get a sidebar layout. Both live under the same app root.
Parallel Routes
Show multiple pages in the same layout simultaneously using @slot naming:
src/app/
├── layout.tsx
├── page.tsx
├── @modal/
│ └── (.)photos/[id]/
│ └── page.tsx → Intercepts /photos/123 and shows as modal
└── photos/
└── [id]/
└── page.tsx → Full-page photo view
This is what Instagram-style modal routing looks like — click a photo and it opens in a modal overlay, but paste the URL in a new tab and it opens as a full page. Advanced — but worth knowing this capability exists.
Next lesson: API routes in Next.js — building a backend directly inside your Next.js app.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises