How to Fix: Catch all route within dynamic segment breaks dynamic params

6 min read

When a catch-all route lives inside a dynamic segment, Next.js can stop resolving params the way you expect.

This issue typically appears in route trees such as a locale segment like [lang] combined with a nested […slug] route. Everything works for the default locale, then breaks as soon as you switch to another one. The result is usually missing or malformed dynamic params, incorrect matching, or a page that resolves the wrong segment shape.

The core problem is not your page component logic. It is the way the App Router interprets route segments when a catch-all route is nested under another dynamic route. In this specific case, the locale segment and the catch-all segment compete to describe the URL, and param resolution becomes inconsistent.

Understanding the Root Cause

In the Next.js App Router, every folder in app/ defines a route segment. A folder like [locale] captures one path segment, while a folder like […slug] captures all remaining segments as an array.

That sounds straightforward, but nesting them creates an important routing constraint:

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

For a URL like /fr/products/shoes, Next.js must decide:

  • locale = fr
  • slug = ["products", "shoes"]

In theory, that split is valid. In practice, issues arise when the application also has locale logic, redirects, middleware, or route generation that assumes a stable param shape for both default and non-default locales. The bug becomes visible when non-default locales are introduced because the route tree now depends on one dynamic segment feeding into another.

Why this breaks:

  • The catch-all segment greedily matches the rest of the URL.
  • The parent dynamic segment must still be resolved first.
  • If locale switching, rewrites, or generated paths alter the incoming pathname, the router can produce params that do not align with the component’s expectations.
  • Code that treats params.slug as always defined or assumes params.locale is stable across all route variants can fail.

In short, the problem is a route design ambiguity. The router can match the URL, but the combination of dynamic locale prefixes and a nested catch-all route makes param extraction fragile.

Step-by-Step Solution

The safest fix is to avoid placing a catch-all route directly beneath a dynamic locale segment when that catch-all is expected to represent the rest of the app tree. Instead, restructure the route hierarchy so the locale is isolated and the remaining route segments are handled consistently.

1. Problematic route structure

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

This structure is compact, but it is exactly where param resolution becomes brittle.

2. Prefer a route group with explicit locale handling

Use a route group to separate localized routing concerns from your content routing:

app/(site)/[locale]/page.tsx
app/(site)/[locale]/[[...slug]]/page.tsx

Using optional catch-all [[...slug]] is often more stable than forcing every route through [...slug], especially for paths like /fr where no trailing slug exists.

3. Normalize params in the page component

Do not assume slug always exists. Normalize it before use.

type PageProps = {
  params: {
    locale: string
    slug?: string[]
  }
}

export default function Page({ params }: PageProps) {
  const locale = params.locale
  const slug = params.slug ?? []

  return (
    <main>
      <p>Locale: {locale}</p>
      <p>Slug: {JSON.stringify(slug)}</p>
    </main>
  )
}

This prevents runtime errors caused by treating an absent catch-all param as an array.

4. If using middleware, preserve pathname semantics carefully

Locale middleware often rewrites URLs. If the middleware rewrites a path incorrectly, the resulting route may no longer map to the expected param structure.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const locales = ['en', 'fr', 'de']
const defaultLocale = 'en'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  const hasLocale = locales.some(
    (locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)
  )

  if (hasLocale) {
    return NextResponse.next()
  }

  const url = request.nextUrl.clone()
  url.pathname = `/${defaultLocale}${pathname}`
  return NextResponse.redirect(url)
}

The key point is that the middleware must keep the locale as a distinct first segment. Do not flatten or merge locale and slug behavior.

5. Generate static params with the correct shape

If you use generateStaticParams, return params matching the route definition exactly.

export async function generateStaticParams() {
  return [
    { locale: 'en', slug: [] },
    { locale: 'fr', slug: [] },
    { locale: 'fr', slug: ['products'] },
    { locale: 'fr', slug: ['products', 'shoes'] }
  ]
}

If your route uses [[...slug]], an empty slug must be treated consistently. If you use [...slug], an empty slug is invalid.

For localized pages that may or may not have nested path segments, this pattern is safer:

app/
  (site)/
    [locale]/
      [[...slug]]/
        page.tsx

This gives you:

  • /en – locale only
  • /fr/products – locale plus one slug segment
  • /de/products/shoes – locale plus multiple slug segments

Most importantly, it avoids forcing the router into an all-or-nothing catch-all interpretation for every localized page.

Common Edge Cases

Default locale works, other locales fail

This usually means your app has logic that special-cases the default locale, often via middleware or redirects. Verify that all locales produce the same param shape.

Using [...slug] when the base localized path should also render

If /fr should render a page, use optional catch-all [[...slug]]. A non-optional catch-all requires at least one segment after the locale.

Static params mismatch

If generateStaticParams returns slug: 'products' instead of slug: ['products'], routing can break silently or generate invalid pages.

Incorrect assumptions in TypeScript

Type your params exactly. A catch-all param is an array, not a string.

slug?: string[]

Middleware rewrites hide the real issue

If middleware rewrites /products to /fr/products, confirm that the request actually lands on the route shape your page expects. Debug by logging both the original pathname and final pathname.

Mixing pages and app router patterns

If the codebase still contains legacy routing assumptions from the Pages Router, dynamic segment behavior may seem inconsistent. Keep route conventions aligned with the App Router.

FAQ

Why does this bug appear only when I switch away from the default locale?

Because many apps implicitly treat the default locale differently through redirects, rewrites, or hidden path assumptions. Once the locale becomes a visible path segment, the nested catch-all must resolve correctly, and that is where the mismatch shows up.

Should I always replace [...slug] with [[...slug]]?

No. Use [[...slug]] only if the route should also match the base path with no extra segments. If at least one trailing segment is required, keep [...slug].

What is the most reliable fix for this issue?

The most reliable fix is to redesign the route so the locale segment remains explicit and the rest of the path is handled by an optional catch-all only when needed. Then normalize params and ensure middleware and static param generation match that route shape exactly.

If you are reproducing this issue from the linked sandbox, the practical takeaway is simple: stop depending on a fragile [locale]/[…slug] assumption, switch to a route structure that supports empty and nested localized paths safely, and make sure every part of the stack treats params in the same format.

Reference reproduction: view the demo sandbox.

Leave a Reply

Your email address will not be published. Required fields are marked *