Home JavaScript Dynamic Routes in Next.js 16 — Wrong [slug] Matched 404 for 40% of Product Pages
Intermediate 4 min · July 12, 2026
Dynamic Routes, Route Groups, and Advanced Routing Patterns

Dynamic Routes in Next.js 16 — Wrong [slug] Matched 404 for 40% of Product Pages

Catch-all routes matching too greedily or not at all: [slug], [...slug], route groups, and 40% product page 404s in production.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 20 min
  • Next.js 14+ with App Router
  • Understanding of file-system routing basics
  • Node.js 18+
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Dynamic routes ([param], [...slug], [[...slug]]) have a predictable priority order — static first, dynamic last
  • The wrong [slug] matches when two dynamic segments are siblings — the file-system router picks the first match, not the most specific
  • Route groups ((group)) prevent layout nesting but can break route matching when combined with dynamic parameters
  • A catch-all [...slug] at /products/[...slug] catches EVERYTHING including /products — but only if you handle the root case
  • 40% of product pages returned 404 because /products/[category]/[product] was shadowed by /products/[...slug] with no empty array fallback
✦ Definition~90s read
What is Dynamic Routes, Route Groups, and Advanced Routing Patterns?

Dynamic routes in Next.js App Router use bracket-notation folders like [id] and [...slug] to create parameterized pages. The file-system router maps URL segments to folder names — /products/[id] matches /products/42 and passes { params: { id: '42' } }. Catch-all routes [...slug] match multiple segments as an array. Optional catch-all [[...slug]] also matches root paths.

Think of Next.js dynamic routes like a filing cabinet.

Route groups ((dashboard)) organize layouts without adding URL segments. Parallel routes (@sidebar) render independent page trees alongside the main content. Intercepting routes ((.)photo) intercept client-side navigation to show modals instead of full pages.

The critical skill is understanding the priority order for route resolution — when multiple patterns match the same URL, Next.js picks deterministically but not by specificity. This causes the most common class of route bugs: silent shadowing where a catch-all matches before a more specific dynamic segment.

Plain-English First

Think of Next.js dynamic routes like a filing cabinet. You label folders [customer] and [...customer]. When someone asks for the file 'John', the router opens whichever folder it finds first — which might be the catch-all bin instead of the specific folder. The fix is organizing folders so the specific one is checked before the catch-all.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Next.js file-system routing is elegant until it is not. The problem surfaces when you have multiple dynamic segments at the same level — /products/[category]/[product] and /products/[...slug] living side by side. The router picks one, and it is rarely the one you expect.

This article breaks down how Next.js resolves dynamic route conflicts, why route groups ((dashboard)) interact poorly with dynamic segments, and how to diagnose the 'wrong route matched' bug that caused 40% of product pages to 404 in a recent production incident.

Tested on Next.js 16.0.0-canary with App Router. The resolution rules are identical in Next.js 14 and 15 — this is a design invariant, not a versioning bug.

The File-System Router Is Not Smart — It Is Deterministic

Next.js App Router resolves routes by file-system traversal. It does not evaluate which route is more specific — it picks the first hit in a deterministic priority list:

  1. Static segments (exact literal paths like /products/featured)
  2. Dynamic single segments ([param] like [category])
  3. Catch-all segments ([...param] like [...slug])
  4. Optional catch-all ([[...param]] like [[...slug]])

This priority is documented but easy to miss when adding a new route. The order within each tier is alphabetical by folder name. So app/products/[category] and app/products/[...slug] are both tier 2 dynamic segments. If the file system sorts [...slug] before [category] (it usually does because '[' sorts before brackets with content), Next.js resolves [...slug] first.

The '[id]' pattern sorts before '[...slug]' lexicographically in most platforms — but '[category]' and '[...slug]' have no guaranteed priority. The result is unpredictable behavior depending on your OS and how the file system orders bracket-prefixed directory names.

This is why the fix is never 'reorder your folders' — it is always 'add a literal prefix segment'.

File-System Priority Is Alphabetical Within Tiers
Do not rely on folder sort order. Two dynamic segments at the same level are resolved alphabetically — which varies by OS and filesystem. Always disambiguate with a literal segment.
Production Insight
A product team added [...slug] for CMS pages and broke [category]/[product] routes silently.
The catch-all matched first because of alphabetical priority — no build warning was emitted.
Rule: never have two dynamic segments at the same tree level without a literal prefix to separate them.
Key Takeaway
Next.js route priority is: static > [param] > [...slug] > [[...slug]]. Within the same tier, alphabetical order applies.
Catch-all routes at the same level as single dynamic segments cause unpredictable conflicts.
Always add a literal segment (like /cms/[...slug]) to prevent shadowing.
nextjs-dynamic-routes-route-groups THECODEFORGE.IO Next.js 16 Route Resolution Stack Layered hierarchy from URL to rendered component Request Layer URL Path | HTTP Method File-System Router Static Files | Dynamic Segments Route Groups (folder) Layouts | Parallel @slots Catch-All Handlers [...slug] | [[...slug]] Intercepting Routes (..) Patterns | Modal Overlays Output Layer Build Manifest | 404 Fallback THECODEFORGE.IO
thecodeforge.io
Nextjs Dynamic Routes Route Groups

Route Groups ((folder)): They Drop from URL but Not from Route Resolution

Route groups are parentheses-folders that organize layouts without affecting the URL path. app/(marketing)/about/page.tsx renders at /about. The parenthesis content is stripped from the URL.

The trap: route groups create a nesting level in the route tree but not in the URL. If you have app/(marketing)/products/[slug] and app/products/[...catchall], those two routes are siblings in URL space — but the file system places them in different branches. Next.js resolves routes by file-system path first, then maps to URL. The result is that (marketing)/products/[slug] may shadow app/products/[...catchall] or vice versa, depending on the sort order of the two directory structures.

Route groups should not be used to organize routes that share URL paths. They are for layout grouping only. If two routes need different layouts but share URL patterns, use parallel routes instead. If they need no layout relationship, put them in completely separate directory trees.

Route Groups Are Layout-Only — Not URL Structure
Think of route groups as transparent to the URL but opaque to the router. Your app/(group)/path resolves to /path, but the router sees it as a sibling of every other folder that produces /path. Conflicting? Yes. Documented? Barely.
Production Insight
Using route groups to organize an admin panel under (dashboard) is fine — but adding (dashboard)/settings/[id] that conflicts with a sibling settings/ directory will cause silent route shadowing.
Rule: keep route groups for layout hierarchy only. Never share URL patterns across route groups and regular folders.
Key Takeaway
Route groups organize layouts without changing URLs — but they still participate in route resolution.
A route group + dynamic segment can conflict with sibling dynamic routes that produce the same URL pattern.
Use route groups only for layout hierarchy — not for URL organization.

Catch-All [...slug] vs Optional Catch-All [[...slug]]: The Empty Array Edge Case

The difference between [...slug] and [[...slug]] is precisely one edge case: the root path. app/products/[...slug]/page.tsx matches /products/a and /products/a/b but NOT /products. app/products/[[...slug]]/page.tsx matches all three: /products, /products/a, and /products/a/b.

The catch-all variant passes an array: params.slug = ['a'] for /products/a. Optional catch-all passes an empty array for /products: params.slug = []. Handle this case or your root product listing page 404s.

In the production incident described earlier, the team used [...slug] instead of [[...slug]], so /products (the root listing) continued working via the [category]/[product] route. But /products/a/b (three segments) was caught by [...slug] before [category]/[product] could handle it. The fix with [[...slug]] would not have helped — the root case was fine. The issue was priority, not parameters.

Choose catch-all patterns deliberately: use [...slug] when the root path has its own handler, use [[...slug]] when the catch-all should also handle the root case.

app/products/[[...slug]]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default async function ProductsPage({
  params
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params

  // slug is undefined for /products (if using [...slug])
  // slug is [] for /products (if using [[...slug]])
  // slug is ['category'] for /products/category
  // slug is ['category', 'product'] for /products/category/product

  if (!slug || slug.length === 0) {
    // Root listing page — show all products
    return <ProductListing />
  }

  if (slug.length === 1) {
    // Category page — /products/category
    return <CategoryPage categorySlug={slug[0]} />
  }

  if (slug.length === 2) {
    // Product page — /products/category/product
    return <ProductPage categorySlug={slug[0]} productSlug={slug[1]} />
  }

  // Deeper nesting — 404 or CMS content
  notFound()
}
Try it live
Always Handle Empty Catch-All Arrays
If using [[...slug]], always check if params.slug is undefined or empty before accessing elements. Skipping this check causes 'Cannot read properties of undefined' crashes on the root path.
Production Insight
A marketing site used [[...slug]] for a blog and forgot to handle the empty array case for /blog.
The root blog listing rendered 'undefined is not iterable' in production for 2 hours.
Rule: always guard catch-all parameter access with a length check.
Key Takeaway
Use [...slug] when root path has its own handler, [[...slug]] when the catch-all should handle everything.
Always guard params.slug access with !slug || slug.length === 0 check.
Catch-all routes at the same level as single dynamic segments cause priority conflicts — use literal prefixes.
Catch-All vs Optional Catch-All Routes Trade-offs in matching dynamic product paths [...slug] (Catch-All) [[...slug]] (Optional Catch-Al Matches root path No Yes Priority in resolution Higher than optional Lower than catch-all Common 404 cause Missing nested segments Overlapping with static routes Use case Required dynamic segments Optional trailing segments Build output visibility Listed as catch-all Listed as optional catch-all THECODEFORGE.IO
thecodeforge.io
Nextjs Dynamic Routes Route Groups

Route Resolution Order: The Full Priority Table with Examples

Next.js resolves routes in this exact priority order when multiple routes match the same URL:

  1. Static routes (exact match) — /products/featured
  2. Single dynamic segments — /products/[category]
  3. Catch-all segments — /products/[...slug]
  4. Optional catch-all — /products/[[...slug]]
  5. Root handler — /products/page.tsx

Within each tier, lexical order of the folder name determines priority. 'a' sorts before 'b', '[a]' sorts before '[b]'. But here is the problem: at the file system level, '[...slug]' and '[category]' are both dynamic — they belong to the same tier. The tier list above is misleading because it groups all dynamic patterns together.

The actual tier grouping is
  • Tiers 1-2: literal segments and static pages
  • Tier 3: ALL dynamic segments ([param], [...slug], [[...slug]]) — resolved alphabetically among themselves
  • Tier 4: notFound() and default.js

This means [category] and [...slug] compete at the same priority level. The winner is determined by which folder name sorts first in your specific file system — ext4 on Linux sorts differently than APFS on macOS in some edge cases.

Never rely on sort order. Always test the production build output with 'next build --debug' and verify route resolution for every dynamic pattern.

debug-route-priority.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# View all routes as Next.js resolves them
npx next build --debug 2>&1 | grep -E "Route|✓"

# List all dynamic route patterns in your app
find app -type d | grep -E '\[.*\]' | sort

# Check for overlapping patterns in CI
node -e "
const fs = require('fs');
const path = require('path');

function findDynamicDirs(dir, prefix = '') {
  const results = [];
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.isDirectory()) {
      const fullPath = path.join(dir, entry.name);
      if (entry.name.startsWith('[')) {
        results.push(path.join(prefix, entry.name));
      }
      results.push(...findDynamicDirs(fullPath, path.join(prefix, entry.name)));
    }
  }
  return results;
}

const dynamics = findDynamicDirs('app');
const patterns = dynamics.map(d => d.replace(/\\[.+?\\]/g, ':param'));
const unique = new Set(patterns);
if (unique.size !== patterns.length) {
  console.log('WARNING: Overlapping dynamic route patterns detected');
  process.exit(1);
}
"
Route Priority: Literal > Dynamic > Catch-All
Literal segments always win. Use them as tiebreakers when you need multiple dynamic patterns at the same URL level: /products/by-category/[category] vs /products/cms/[...slug]. Literal prefixes eliminate ambiguity.
Production Insight
A SaaS platform had /[workspace]/settings and /[workspace]/[...pages]. The [...pages] catch-all shadowed the settings route for workspaces named 'settings'.
Rule: always add a literal prefix like /[workspace]/app/[...pages] to prevent workspace-name collisions with route segments.
Key Takeaway
Route priority: static > all dynamic patterns (sorted alphabetically).
Two dynamic segments at the same level compete — alphabetically — with no specificity check.
Use literal segment prefixes as tiebreakers to eliminate ambiguity.

Parallel Routes and Intercepting Routes: The @slot Shadow War

Parallel routes (@slot) and intercepting routes ((.)pattern) introduce modal-style navigation patterns but also introduce a unique class of route conflicts. An @slot renders a separate route tree alongside the main page — it does not replace the main route. But the slot's default.js file can shadow the slot's page.tsx in ways that are hard to debug.

When a parallel route slot has no matching page for the current URL, Next.js falls back to default.js. If default.js exists at the parent level (outside the slot directory), it is used as a fallback. But if the slot directory has no page.tsx and no default.js, the slot renders nothing — which is often interpreted as a bug.

The intercepting route pattern (.) matches a sibling route level. (.)products at app/feed/(.)products/page.tsx intercepts /products when navigated from /feed. The problem: if /products is also a catch-all like /[...slug], the intercepting route tries to match the same path. The priority between intercepting routes and regular routes depends on the navigation source — client-side navigation follows intercepting patterns, direct URL access skips them.

This dual behavior (intercept on client, skip on direct) means your page renders differently depending on navigation source. Test both paths.

app/@modal/default.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// app/@modal/default.tsx
// This renders when no modal page matches the current URL
// Without this file, the @modal slot renders nothing — causing layout gaps

export default function ModalDefault() {
  return null // Slot contributes nothing to the layout
}

// app/@modal/(.)products/[id]/page.tsx
// Intercepts /products/:id when navigated from the parent route

export default async function InterceptedProductModal({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const product = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'force-cache'
  }).then(r => r.json())

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <Link href="/products">Close</Link>
      </div>
    </div>
  )
}

// Note: Direct navigation to /products/:id skips the intercept
// and renders the full product page at app/products/[id]/page.tsx
// This is intentional — intercept only applies on client navigation
Try it live
Always Provide default.js for Every Slot
Without default.js in each @slot directory, the slot renders as empty — which may break the layout. Return null from default.js if the slot should be invisible when inactive. Failing to provide it causes that section of the layout to disappear entirely.
Production Insight
A team added @modal for product quick-view without a default.js. The modal slot rendered nothing on non-modal pages — which pulled the layout out of its grid and collapsed the right sidebar.
Rule: every @slot directory needs a default.js. Even returning null is better than missing it.
Key Takeaway
Parallel routes @slot need default.js at every level — missing it collapses the slot area.
Intercepting routes only apply to client-side navigation — direct URL access uses the regular route.
Test both navigation modes to confirm consistent rendering.

Grouping Without Groups: Using Parallel Layouts for Multi-Level Navigation

When route groups ((folder)) feel like a hack for sharing layouts across non-adjacent routes, parallel routes offer a cleaner alternative. Instead of forcing every page under one layout, use parallel routes to compose multiple layout sections that each navigate independently.

Pattern: app/@sidebar renders a navigation sidebar. app/@main renders the content area. Both are parallel route slots fed into the same layout. The sidebar slot can navigate independently (showing different sections) while the main slot shows the primary content.

This works because each @slot renders its own matched page from the URL — the sidebar slot matches /@sidebar/... while the main slot matches /@main/... or the default route. The layout orchestrates both.

The cost: more files, more default.js files, and more cognitive overhead for new team members. Parallel routes solve layout problems but add complexity. Do not use them unless you genuinely need independent slot navigation.

app/layout.tsx (with parallel routes)TYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// app/layout.tsx — orchestrate parallel slots

export default function RootLayout({
  children,
  sidebar,
  modal
}: {
  children: React.ReactNode
  sidebar: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <div className="app-layout">
      <aside className="sidebar">
        {sidebar}
      </aside>
      <main className="content">
        {children}
      </main>
      {modal}
    </div>
  )
}

// app/@sidebar/default.tsx
export default function SidebarDefault() {
  return <DefaultNavigation />
}

// app/@modal/default.tsx
export default function ModalDefault() {
  return null
}

// app/@sidebar/users/page.tsx — sidebar matches /users
// app/(main)/users/page.tsx — main content matches /users
Try it live
Parallel Routes Double Server Load
Each @slot renders its own page tree independently. If a layout has 3 slots and the page loads, Next.js renders 3 separate route trees — tripling the initial server render work. Profile before and after adding parallel routes.
Production Insight
A dashboard app used 4 parallel routes (@header, @sidebar, @main, @footer). Server render time increased from 120ms to 480ms per request because all 4 slots rendered independently.
Rule: limit parallel slots to 2-3 max. Each slot adds a full page tree render to every request.
Key Takeaway
Parallel routes solve layout composition but each slot adds a full independent render.
Use parallel routes when slots navigate independently — not as a layout organization trick.
Always provide default.js for every slot at every URL level.

Use the Build Output to Debug Routes — Not the Browser

The browser devtools show the rendered page, not the route that produced it. Use 'next build' output to verify which page.tsx matches each URL pattern. The build output lists every route and the file that handles it.

Run 'npx next build --debug' and pipe through grep for route output. The build prints something like: - /products → app/products/page.tsx (static) - /products/[category] → app/products/[category]/page.tsx (dynamic) - /products/[...slug] → app/products/[...slug]/page.tsx (catch-all)

If you see a route mapped to the wrong file, you have a conflict. The build does not warn about overlapping routes — it silently picks one. Only manual inspection reveals the conflict.

Add a CI step that compares expected route patterns against build output. Any discrepancy between your expectations and the resolved routes should fail the build.

ci-route-check.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
# CI route conflict detection
# Run after "npx next build" to catch overlapping dynamic routes

# Extract all routes from build output
ROUTES=$(npx next build --debug 2>&1 | grep -E '^[─├└]' | sed 's/.*\///' | sed 's/ .*//')

# Extract all route patterns from file system
PATTERNS=$(find app -name 'page.tsx' -exec dirname {} \; | sed 's|app/||' | sort)

# Check for multiple files that could match the same URL
# This is a simplified check — real implementation needs URL pattern collision detection

echo "Checking for overlapping dynamic routes..."

# Find dynamic segments at same depth
find app -type d -name '\[*.\]' | sed 's|app/||' | while read -r dir; do
  base=$(dirname "$dir")
  name=$(basename "$dir")
  count=$(find "app/$base" -type d -name '\[*.\]' | wc -l)
  if [ "$count" -gt 1 ]; then
    echo "WARNING: Multiple dynamic segments in $base/"
    find "app/$base" -type d -name '\[*.\]'
  fi
done

echo "Route check complete. Inspect warnings manually."
Route Debugging in CI
Add this route check to your CI pipeline. It catches overlapping dynamic segments before they reach production. The 40% 404 incident would have been caught by a simple 'multiple dynamic segments at same level' check.
Production Insight
A team running only manual browser testing missed the route conflict for 2 weeks. Their staging build had the same conflict but testers only navigated via the UI (which used correct links), never manually typed URLs.
Rule: automated route resolution checks catch what manual navigation misses.
Key Takeaway
Use 'next build --debug' to verify route resolution — browser devtools show rendered output, not route matching.
Build output silently picks one match for overlapping routes — no warnings emitted.
Add CI route conflict detection to prevent phantom 404s in production.
● Production incidentPOST-MORTEMseverity: high

Catch-All Products Shadowed Category Pages — 40% of Catalog Returned 404

Symptom
Users navigating to /products/electronics/laptops/macbook-pro received 404. Direct navigation to /products/[category]/[product] paths failed. Existing product links in emails and bookmarks returned 404. Google Search Console showed 8,000+ 404 errors within 24 hours of deployment. Revenue dropped 15% before rollback.
Assumption
The CMS migration team assumed the new catch-all route would only match paths that did not fit existing patterns — they did not know Next.js prioritizes file-system order over specificity.
Root cause
The team added app/products/[...slug]/page.tsx for a new CMS feature without checking app/products/[category]/[product]/page.tsx already existed. Next.js does not match the most specific route — it matches the first file-system entry that resolves. In most setups, catch-all routes (sorted by name, not specificity) take priority over explicit nested segments. The [...slug] caught all three-segment paths before [category]/[product] could handle them.
Fix
Removed the [...slug] route and replaced it with /products/cms/[...slug], moving the CMS catch-all under a literal prefix. Also added a middleware check that logged route resolution conflicts for future prevention: if two routes match the same path pattern, the middleware flags it in the build output.
Key lesson
  • Next.js route priority is file-system order, not specificity — catch-all routes often win over explicit nested dynamic segments
  • Never add a catch-all [...slug] at the same level as other dynamic routes without a literal prefix to disambiguate
  • Add automated route conflict detection to CI — a simple script that checks for overlapping route patterns
  • 40% 404 rate is not a user error — it is a router misconfiguration that should never reach production
Production debug guideDiagnosing wrong route matches and phantom 404s5 entries
Symptom · 01
A page returns 404 but the file exists in the app directory
Fix
Check for a catch-all [...slug] or [[...slug]] route at the same level. Next.js may be matching a broader route that does not render the expected component. List all page.tsx files in the parent directory and check for conflicts.
Symptom · 02
Route group ((name)) pages are unreachable
Fix
Route groups are excluded from URL paths — they only organize layouts. Ensure no sibling route has the same effective URL pattern. A route group + dynamic segment ((group))/[id] conflicts with [id] at the parent level.
Symptom · 03
Parallel route @slot is blank or shows default content unexpectedly
Fix
The @slot route may be matching a default.js from a sibling level instead of the specific parallel page. Check that default.js exists in the correct slot directory, not at a parent level where it shadows slot-specific pages.
Symptom · 04
Linked page works on dev but returns 404 in production
Fix
Dev server rebuilds routes on every request and may match less ambiguous paths. Production build sorts routes once. Run 'next build' and inspect the route output. Use 'next build --debug' to see route priority resolution.
Symptom · 05
Route matched shows wrong component in browser devtools
Fix
Add a data-route attribute to the layout component that prints the matching page path. Check the actual page component being rendered against the expected path from the URL.
★ Dynamic Route Debugging Quick ReferenceCommon route matching issues and their fixes
Page returns 404 but file exists
Immediate action
Check for overlapping dynamic routes
Commands
find app -name 'page.tsx' | sort
npx next build --debug 2>&1 | grep 'Route'
Fix now
Add a literal segment prefix to disambiguate catch-all from explicit routes
Route group ignored in URL+
Immediate action
Understand that (group) is only for layout organization
Commands
ls -d app/*/\(*\)/ 2>/dev/null
Check if (group) contains page.tsx or just layout.tsx
Fix now
Move page.tsx outside the route group if you want it at that URL path
Catch-all returns 404 for root path+
Immediate action
Check if [...slug] handles empty array case
Commands
cat app/products/[...slug]/page.tsx | head -20
Check params.slug for undefined/null
Fix now
Use [[...slug]] (optional catch-all) instead of [...slug] to match root path
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
appproducts[[...slug]]page.tsxexport default async function ProductsPage({Catch-All [...slug] vs Optional Catch-All [[...slug]]
debug-route-priority.shnpx next build --debug 2>&1 | grep -E "Route|✓"Route Resolution Order
app@modaldefault.tsxexport default function ModalDefault() {Parallel Routes and Intercepting Routes
applayout.tsx (with parallel routes)export default function RootLayout({Grouping Without Groups
ci-route-check.shROUTES=$(npx next build --debug 2>&1 | grep -E '^[─├└]' | sed 's/.*\///' | sed '...Use the Build Output to Debug Routes

Key takeaways

1
Route priority
static > dynamic (alphabetical) > catch-all > optional catch-all — specificity is NOT evaluated
2
Never place [...slug] and [param] at the same directory level
they compete alphabetically with unpredictable results
3
Use literal prefix segments (e.g., /cms/[...slug]) to disambiguate catch-all routes from explicit dynamic routes
4
Route groups ((folder)) do not affect URL paths but do affect internal route resolution
they can shadow sibling routes
5
Always provide default.js for every parallel route @slot at every URL level
missing default.js collapses the slot
6
Intercepting routes (.) only trigger on client-side navigation
direct URL access uses the regular route
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the route resolution priority in Next.js App Router?
Q02SENIOR
Explain route groups and when you should avoid them.
Q03SENIOR
A user reports pages under /products/[category]/[product] return 404 whe...
Q04JUNIOR
What is the purpose of default.js in parallel routes?
Q01 of 04SENIOR

What is the route resolution priority in Next.js App Router?

ANSWER
Next.js resolves routes in this priority order: static literal routes first, then all dynamic segment patterns (sorted alphabetically among themselves), then catch-all segments ([...slug]), then optional catch-all segments ([[...slug]]). Within each group, alphabetical file system order determines priority. The critical detail is that [category] and [...slug] are in the same priority group — they compete alphabetically, not by specificity. This means adding a catch-all to an existing dynamic route can silently shadow the original route without any build warning. The fix is to always use literal prefix segments to create unambiguous route hierarchies. For example, instead of /products/[category] vs /products/[...slug], use /products/by-category/[category] vs /products/cms/[...slug].
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
How does Next.js resolve when multiple dynamic routes match the same URL?
02
What is the difference between [...slug] and [[...slug]]?
03
Do route groups affect URL matching?
04
Why does a page work in dev but 404 in production?
05
Can I use route groups to rename a URL segment?
06
How do interrupt intercepting routes with client-side navigation?
07
Is there a way to log which route file matched for a given URL?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

4 min read · try the examples if you haven't

Previous
Deploying Next.js 16: Vercel, Self-Hosting, and Static Export
30 / 56 · Next.js
Next
Parallel Routes and Intercepting Routes in Next.js 16