Skip to main content

Dynamic Routes in Next.js: Catch All Segments

Dynamic routes in the App Router let you create routes that respond to variable URL segments. Instead of hardcoding every possible page (like /product/1, /product/2, etc.), you define a single dynamic page that handles any value in that segment. Next.js provides three dynamic routing patterns: single segment [id], catch-all [...slug], and optional catch-all [[...slug]]. This article covers all three with real examples.

Single Dynamic Segment: [id]

A folder wrapped in square brackets, like [id], matches any single URL segment. For example, app/products/[id]/page.tsx matches /products/123, /products/456, /products/awesome-book—any product ID. Inside the page component, you access the parameter via the params prop.

Creating a Product Detail Page

Imagine a product catalog with dynamic product pages. Create the folder structure:

app/
└── products/
├── page.tsx # /products (listing page)
└── [id]/
└── page.tsx # /products/:id (detail page)

The detail page receives the id parameter from the URL:

// app/products/[id]/page.tsx
export default function ProductPage({
params,
}: {
params: { id: string };
}) {
const { id } = params;

// In a real app, fetch product data by ID
const products: Record<string, { name: string; price: number }> = {
"1": { name: "Laptop", price: 999 },
"2": { name: "Mouse", price: 25 },
"3": { name: "Keyboard", price: 75 },
};

const product = products[id];

if (!product) {
return <div>Product not found.</div>;
}

return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
<p className="text-xl text-gray-600 mb-6">${product.price}</p>
<button className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
Add to Cart
</button>
</div>
);
}

Visit /products/1 and you'll see the Laptop. Visit /products/2 and you'll see the Mouse. The [id] segment captures the value and passes it to your component.

Generating Static Pages

If you have a finite list of products, you can use generateStaticParams to pre-render all product pages at build time, avoiding dynamic rendering:

// app/products/[id]/page.tsx
export async function generateStaticParams() {
const products = ["1", "2", "3"];
return products.map((id) => ({
id,
}));
}

export default function ProductPage({
params,
}: {
params: { id: string };
}) {
// ... same as above
}

When you build the app, Next.js pre-generates /products/1, /products/2, and /products/3 as static HTML files. Fast, cacheable, and SEO-friendly.

Catch-All Segments: [...slug]

A folder with three dots and square brackets, like [...slug], captures all remaining segments in the URL. For example, app/docs/[...slug]/page.tsx matches /docs/intro, /docs/guide/installation, /docs/guide/config/advanced, and even /docs (depending on how you handle it).

Building a Documentation Site

A docs site often has multi-level routes like /docs/intro, /docs/guides/setup, /docs/guides/plugins/custom. Instead of creating a folder for each, use a catch-all:

app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/*, /docs/a/b/c, etc.

The slug parameter is now an array of strings:

// app/docs/[...slug]/page.tsx
export default function DocsPage({
params,
}: {
params: { slug: string[] };
}) {
const { slug } = params;

// slug for /docs/guides/setup is ["guides", "setup"]
// slug for /docs is []

if (slug.length === 0) {
return <div>Welcome to docs. Choose a section.</div>;
}

const docPath = slug.join(" / ");

return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-4">{docPath}</h1>
<div className="prose">
<p>Content for {docPath} would be loaded here.</p>
</div>
</div>
);
}

Routes now work:

  • /docs - Welcome page (slug is [])
  • /docs/intro - slug is ["intro"]
  • /docs/guides/setup - slug is ["guides", "setup"]
  • /docs/api/reference/endpoints - slug is ["api", "reference", "endpoints"]

In a real app, you'd load content from a database or markdown files using the slug path.

Optional Catch-All Segments: [[...slug]]

The triple brackets [[...slug]] are an optional catch-all—they match the segment and everything below it, or nothing at all. A route at app/blog/[[...slug]]/page.tsx matches /blog, /blog/2026, /blog/2026/june, and /blog/2026/june/react-tips.

The key difference from [...slug] is that the page is rendered at the parent route too. For example:

app/
└── blog/
└── [[...slug]]/
└── page.tsx # /blog and /blog/*, /blog/a/b/c, etc.
// app/blog/[[...slug]]/page.tsx
export default function BlogPage({
params,
}: {
params: { slug?: string[] };
}) {
const slug = params.slug || [];

if (slug.length === 0) {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-4">Blog</h1>
<p>Latest posts:</p>
<ul className="space-y-2">
<li>
<a href="/blog/2026/react-18-features" className="text-blue-600">
React 18 Features
</a>
</li>
<li>
<a href="/blog/2026/nextjs-optimization" className="text-blue-600">
Next.js Optimization
</a>
</li>
</ul>
</div>
);
}

// Handle nested paths: /blog/2026/june/my-post
const [year, month, title] = slug;

return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-2">{title}</h1>
<p className="text-gray-600 mb-6">
{month} {year}
</p>
<article className="prose">
<p>Full blog post content here...</p>
</article>
</div>
);
}
  • /blog renders the blog listing (slug is undefined or [])
  • /blog/2026/june/my-post renders the post detail (slug is ["2026", "june", "my-post"])

Comparison: [id] vs [...slug] vs [[...slug]]

PatternMatch /docsMatch /docs/aMatch /docs/a/bslug value
[slug]NoYesNo"a"
[...slug]NoYesYes["a"], ["a", "b"]
[[...slug]]YesYesYesundefined or [], ["a"], ["a", "b"]

Use [id] for single-level routes (products, users). Use [...slug] for multi-level routes where the parent isn't rendered. Use [[...slug]] when both parent and nested routes exist.

Building a Dynamic Blog with Dynamic Metadata

Combine dynamic routes with generateMetadata to set SEO titles per post:

// app/blog/[slug]/page.tsx
import { Metadata } from "next";

// Simulate fetching blog post data
async function getBlogPost(slug: string) {
return {
title: "My Awesome Post",
description: "A great read about web development.",
date: "2026-06-02",
};
}

export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getBlogPost(params.slug);
return {
title: post.title,
description: post.description,
};
}

export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getBlogPost(params.slug);

return (
<article className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-2">{post.title}</h1>
<p className="text-gray-600 mb-8">{post.date}</p>
<div className="prose">
<p>{post.description}</p>
</div>
</article>
);
}

Each blog post gets its own <title> and meta description based on the slug parameter.

Key Takeaways

  • [id] captures a single segment; useful for detail pages (products, users, posts).
  • [...slug] captures all remaining segments as an array; ideal for multi-level routes (docs, nested categories).
  • [[...slug]] is optional catch-all; matches both parent and nested routes; use when /path and /path/a/b/c both render content.
  • params are passed as props to page components; use them to fetch data or render dynamic content.
  • generateStaticParams pre-renders dynamic routes at build time for speed and SEO.

Frequently Asked Questions

Can I have multiple dynamic segments in one path?

Yes. app/users/[userId]/posts/[postId]/page.tsx matches /users/123/posts/456. Access both as params.userId and params.postId.

What if a dynamic route conflicts with a static route?

Static routes take precedence. If you have both app/products/featured/page.tsx and app/products/[id]/page.tsx, the /products/featured route renders the static page, not the dynamic one.

How do I handle 404s for dynamic routes?

Return a 404 component or use notFound() from next/navigation. If a product ID doesn't exist in your database, call notFound() to trigger the custom not-found.tsx page.

Can I use dynamic segments in API routes?

Yes. app/api/posts/[id]/route.ts exports GET(request, { params }) where params.id is the URL segment. Same syntax as page routes.

Are dynamic routes automatically cached?

No. By default, dynamic routes are rendered on-demand. Use revalidateTag() or revalidatePath() to trigger re-renders on data changes.

Further Reading