Catch-All & Optional Catch-All Routes in Next.js
[...slug].js
vs [[...slug]].js
— the 2025 Guide
Dynamic routing is one of the super-powers that makes Next.js feel effortless. But what happens when you can’t predict how many path segments a URL will have—or whether it will have any at all? Enter catch-all and optional catch-all routes.
1 | What exactly is a “catch-all” route?
A catch-all segment matches “this segment plus anything that follows.”
You declare it by adding an ellipsis inside the brackets:
So /shop/clothes
, /shop/clothes/tops/t-shirts
, or any deeper path will all resolve to the same page. The param arrives as an array:
Official docs example β Next.js
2 | And what’s an optional catch-all?
Sometimes you want the base route to resolve as well (/shop
and deeper paths). Wrap the segment in double brackets:
pages/shop/[[...slug]].js # Matches /shop β
app/shop/[[...slug]]/page.js # App Router equivalent
If no extra path is present, slug
is undefined
, otherwise it’s an array.
Docs illustration β Next.jsNext.js
3 | Pages Router vs App Router syntax
Router | Regular catch-all | Optional catch-all |
---|---|---|
Pages (legacy) | pages/docs/[...slug].js |
pages/docs/[[...slug]].js |
App (Next 13-15) | app/docs/[...slug]/page.js |
app/docs/[[...slug]]/page.js |
Both routers pass the captured parts as arrays (string[]
)—the only difference is the file-system location and the data-fetching helpers you use.
4 | Data fetching for deep URLs
Router | Build-time SSG helper | Runtime SSR helper |
---|---|---|
Pages | getStaticPaths + getStaticProps |
getServerSideProps |
App | generateStaticParams |
dynamic = 'force-dynamic' or plain async code |
Static generation tips
-
Return one object per full path in
getStaticPaths
/generateStaticParams
. -
For huge site-maps, set
fallback: 'blocking'
(Pages) ordynamicParams: true
(App) so uncommon paths are rendered on demand and then cached. -
Don’t forget to
revalidate
when content can change.
5 | Linking & Navigation
Next.js pre-fetches the bundle automatically, so even the deepest path feels instant.
6 | SEO & usability checklist
-
Canonical URLs — compute them at render time so search engines understand each unique address.
-
Breadcrumbs — split the
slug
array to build hierarchical navigation. -
Sitemaps — iterate over your CMS data or route manifest to produce XML; split large sitemaps into files of ≤ 50 k URLs.
-
404 clarity — fall back to a custom 404 page if the slug array doesn’t map to real content.
7 | Common pitfalls
Symptom | Cause | Quick Fix |
---|---|---|
Build fails: route conflicts | You have both index.js and [[...slug]].js in the same folder |
Keep only one default entry point (optional catch-all can replace index ). |
router.query.slug is empty on first render |
Static page is hydrated client-side | Guard for undefined before accessing. |
Params array ordering is wrong | Manual .split('/') on URL |
Use framework-provided params /useParams instead. |
Optional segment ignored in App Router | Folder name typo—must be [[...slug]] not (slug) |
Rename the folder correctly. Next.js |
8 | Real-world use-cases
-
Docs sites —
/docs/intro
,/docs/guides/deployment/aws
. -
CMS pages — marketers add nested slugs without touching code.
-
Multi-level categories —
/shop/men/shirts/flannel
. -
User-generated paths —
/u/jane/recipes/cakes/chocolate
. -
Funnel builders / A/B tests — optional segment shows default landing page when slug missing.
9 | Conclusion
Catch-all ([...slug]
) and optional catch-all ([[...slug]]
) routes turn a single component into a swiss-army knife that gracefully handles any depth of URL. Combine them with static generation helpers for speed, add smart guards for undefined params, and you’ll have a routing setup that scales from ten pages to a million—without ever touching a server config file.
Happy routing! π
FAQS
# | Question | Answer |
---|---|---|
1 | What is a catch-all route? | A catch-all segment ([...slug] ) matches one or more path segments that follow its folder, so pages like /docs/a , /docs/a/b/c , etc. are all handled by the same file. The captured parts arrive as an array (e.g. ['a', 'b', 'c'] ). |
2 | How is an optional catch-all different? | [[...slug]] behaves like [...slug] and also matches the base path with zero extra segments (e.g. /docs ). If no segment exists, the param is undefined . |
3 | What file or folder names do I use? | Pages Router: pages/docs/[...slug].js (catch-all) or pages/docs/[[...slug]].js (optional). App Router: app/docs/[...slug]/page.js or app/docs/[[...slug]]/page.js . |
4 | How do I pre-render these routes at build time? | Pages: export getStaticPaths to list full paths and set fallback (false , true , or 'blocking' ). App: export generateStaticParams ; adjust dynamicParams or revalidate for runtime behavior. |
5 | What does the slug param look like at runtime? |
/shop/clothes/tops → slug = ['clothes','tops'] . For [[...slug]] , visiting /shop yields slug = undefined . |
6 | Can I combine other dynamic segments with a catch-all? | Yes. Example: app/users/[userId]/posts/[...slug]/page.tsx matches /users/42/posts/2025/july/highlights . |
7 | Any SEO concerns? | Not if you pre-render when possible, set canonical URLs dynamically, and ensure unique meta tags for each resolved path. |
8 | How do I link to deep paths? | Use <Link> with the joined array: <Link href={ /shop/${slug.join('/')}}>…</Link> . Next.js prefetches the bundle automatically. |
9 | Why do I get route conflicts at the project root? | A root-level [...slug] can shadow other routes (e.g. /users/[id] ). Place the catch-all inside its own folder or restructure your routing. |
10 | How do I show a default view when no slug is provided? | Use an optional catch-all ([[...slug]] ) and branch in your component: if !slug , render the landing page; otherwise render the deep page. |
11 | Why is router.query.slug empty on first render? |
In statically generated Pages routes, the first client render hydrates an already-rendered HTML page, so query.slug can be undefined initially—guard for it before using. |
12 | Can I build breadcrumbs from the slug array? | Absolutely. Split or reduce the slug array to construct hierarchical links for breadcrumb navigation. |