AiTechWorlds
AiTechWorlds
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.
A folder with brackets creates a dynamic segment:
src/app/
├── blog/
│ ├── page.tsx → /blog (list of posts)
│ └── [slug]/
│ └── page.tsx → /blog/how-to-learn-reactThe 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>
);
}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.
Combine multiple dynamic segments:
src/app/courses/[courseSlug]/[lessonSlug]/page.tsxinterface 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>
);
}Use [...slug] to match any number of segments:
src/app/docs/[...slug]/page.tsxMatches:
/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"]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.
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.
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).
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 → /settingsRoute 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.
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 viewThis 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