Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
18 minLesson 27 of 40
Next.js 15 App Router

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/introparams.slug = ["intro"]
  • /docs/react/hooksparams.slug = ["react", "hooks"]
  • /docs/react/advanced/patternsparams.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:

  • /docsparams.slug = undefined
  • /docs/introparams.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

Get Notes Free →
!