I’m seeing inconsistent routing behavior between visiting my local Next.js production build and my Vercel production deployment.
Setup
- Next.js v15.2.3
- Using pages router
- Node v20.17.0
This is a simplified representation of the routes
pages/
├── [[...catchallslug]].tsx <-- Top-level catch-all route. Requests external CMS
└── shop/
└── shirts/
└── [...slug].tsx <-- Nested dynamic route for shirt product pages. These should be generated on buildtime
Route Configuration:
[...slug].tsx
usesfallback: false
with a hardcoded array of paths:/shop/shirts/adidas/original-tee
/shop/shirts/adidas/performance-tee
[[...catchallslug]].tsx
usesfallback: 'blocking'
to handle unmatched routes
The Issue:
Local behavior (expected): When visiting /shop/shirts/adidas/something
(a non-generated path), the request falls back to the top-level catch-all route [[...catchallslug]].tsx
.
Vercel behavior (unexpected): The same URL triggers [...slug].tsx
and runs its getStaticProps
, despite fallback: false
. I expect this to return a 404 since the path wasn’t pre-generated. Also, the response includes the header x-matched-path: /[[...catchallslug]]
, indicating Vercel internally knows it should match the optional catch-all route, yet it still renders the [...slug].tsx
template and executes its getStaticProps
Key observation: When I remove [[...catchallslug]].tsx
entirely, Vercel correctly returns a 404 for /shop/shirts/adidas/something
.
Why does the presence of a top-level optional catch-all route affect how Vercel handles fallback: false
in nested routes? How can I maintain consistent behavior across environments?
Edit) This also seems to apply when using the App Router under a similar route setup