NextjsCatch-All & Optional Catch-All Routes

Catch-All & Optional Catch-All Routes

A regular dynamic segment like [slug] matches exactly one path segment. Sometimes you need a route that matches any number of segments — arbitrary depth, decided by the data rather than the folder structure. That's what a catch-all route is for.
Catch-all segments: [...slug]
Adding three dots inside the brackets turns a segment into a catch-all. Instead of matching one path piece, it matches everything after that point in the URL, and Next.js hands it to you as an array of strings.

Folder structure

Text
app/
  docs/
    [...slug]/
      page.tsx

app/docs/[...slug]/page.tsx

TSX
type Props = {
  params: { slug: string[] }
}

export default function DocsPage({ params }: Props) {
  // /docs/a          -> slug = ["a"]
  // /docs/a/b        -> slug = ["a", "b"]
  // /docs/a/b/c      -> slug = ["a", "b", "c"]
  const path = params.slug.join('/')

  return <h1>Docs: {path}</h1>
}
Warning
A regular catch-all requires at least one segment to match. Visiting /docs itself (with nothing after it) is not matched by [...slug] and results in a 404 unless a separate app/docs/page.tsx exists to handle the bare route.
Optional catch-all segments: [[...slug]]
Wrapping the catch-all in a second pair of brackets makes it optional. The key difference from a regular catch-all is that this variant also matches the base route with zero segments — so a single page component can handle both /docs and /docs/anything/nested/here.

Folder structure

Text
app/
  docs/
    [[...slug]]/
      page.tsx

app/docs/[[...slug]]/page.tsx

TSX
type Props = {
  params: { slug?: string[] }
}

export default function DocsPage({ params }: Props) {
  // /docs            -> slug = undefined
  // /docs/a          -> slug = ["a"]
  // /docs/a/b        -> slug = ["a", "b"]
  const path = params.slug?.join('/') ?? '(index)'

  return <h1>Docs: {path}</h1>
}
Note
Because params.slug can be undefined for the base route, its TypeScript type should mark it optional (slug?: string[]) and your code should handle the missing case explicitly.

[...slug]

[[...slug]]

Matches one or more segments

Yes

Yes

Matches zero segments (the base route)

No — 404 unless a separate page.tsx exists

Yes

params.slug type

string[]

string[] | undefined

Use cases

Catch-all routes shine whenever the shape of your URL tree is decided by external data rather than by your file system:

  • A CMS-driven page tree, where an editor can create pages at any depth (/about, /about/team, /about/team/leadership) without you adding a matching folder for each one.

  • A documentation site with nested, arbitrary-depth pages, where a single [...slug] route reads the full path and looks up the matching content on the server.

  • A single optional catch-all handling both a section's landing page and every nested page beneath it, avoiding a separate page.tsx just for the index route.