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].tsxusesfallback: falsewith a hardcoded array of paths:/shop/shirts/adidas/original-tee/shop/shirts/adidas/performance-tee
[[...catchallslug]].tsxusesfallback: '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