patterntypescriptnextjsModerate
Dynamic routes, catch-all routes, and optional catch-all routes
Viewed 0 times
Next.js 13+ App Router; params is async in Next.js 15
dynamic routescatch-alloptional catch-allgenerateStaticParams[...slug][[...slug]]
Problem
Choosing between [slug], [...slug], and [[...slug]] routes is confusing. Using the wrong one leads to routes not matching or generating incorrect static pages.
Solution
Match the route pattern to your use case:
// [slug] — matches exactly one segment: /blog/hello
// [...slug] — matches one or more segments: /docs/a/b/c
// [[...slug]] — matches zero or more: /docs and /docs/a/b
// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
const { slug } = await params; // string: 'hello-world'
}
// app/docs/[...slug]/page.tsx
export default async function Docs({ params }) {
const { slug } = await params; // string[]: ['guide', 'setup']
const path = slug.join('/');
}
// app/[[...slug]]/page.tsx — catch-all including root
export default async function CatchAll({ params }) {
const { slug } = await params; // string[] | undefined
if (!slug) return <Home />;
return <PageFor path={slug} />;
}
// generateStaticParams for static generation
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
// For [...slug]: return [{ slug: ['docs', 'intro'] }]
}
// [slug] — matches exactly one segment: /blog/hello
// [...slug] — matches one or more segments: /docs/a/b/c
// [[...slug]] — matches zero or more: /docs and /docs/a/b
// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
const { slug } = await params; // string: 'hello-world'
}
// app/docs/[...slug]/page.tsx
export default async function Docs({ params }) {
const { slug } = await params; // string[]: ['guide', 'setup']
const path = slug.join('/');
}
// app/[[...slug]]/page.tsx — catch-all including root
export default async function CatchAll({ params }) {
const { slug } = await params; // string[] | undefined
if (!slug) return <Home />;
return <PageFor path={slug} />;
}
// generateStaticParams for static generation
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
// For [...slug]: return [{ slug: ['docs', 'intro'] }]
}
Why
Different route patterns serve different URL shapes. [...slug] is ideal for nested documentation trees or CMS pages with unknown depth. [[...slug]] is useful for optional locale prefixes or CMS roots where the index is also dynamic.
Gotchas
- In Next.js 15, params is a Promise — always await it before accessing values
- Catch-all routes [...slug] do NOT match the parent path — /docs doesn't match if the file is in app/docs/[...slug]/
- Optional catch-all [[...slug]] matches both /docs and /docs/guide
- generateStaticParams for catch-all must return { slug: string[] } — an array inside the object
Code Snippets
generateStaticParams for catch-all route
// app/docs/[...path]/page.tsx
export async function generateStaticParams() {
const pages = await getDocPages();
return pages.map(p => ({ path: p.urlPath.split('/') }));
}Context
When building documentation sites, CMS-driven pages, or any multi-segment dynamic routing
Revisions (0)
No revisions yet.