Skip to content

fix(routing): rank overlapping routes segment by segment - #706

Merged
cpojer merged 1 commit into
voidzero-dev:mainfrom
yangshun:fix/route-specificity
Nov 4, 2026
Merged

cpojer merged 1 commit into
voidzero-dev:mainfrom
yangshun:fix/route-specificity

Conversation

@yangshun

@yangshun yangshun commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

When several routes match a URL, the most specific one now handles it, the same way in pages/, routes/ and WebSocket routes. Before, each scanner ranked overlapping routes differently, and some URLs went to the wrong file without a warning:

URL Files Was Now
/users/about pages/[lang]/about.tsx, pages/users/[id].tsx [lang]/about users/[id]
/users/new/edit pages/users/[id]/edit.tsx, pages/users/new/[tab].tsx users/[id]/edit users/new/[tab]
/fr/about pages/[lang]/about.tsx, pages/[lang]/[slug].tsx [lang]/[slug] [lang]/about
/fr/about routes/[lang]/about.ts, routes/[category]/[item].ts [category]/[item] [lang]/about
/chat/lobby routes/chat/[room].ws.ts, routes/chat/lobby.ws.ts chat/[room] chat/lobby

Pages compared whole pattern strings, where : sorts before letters, so any [param] beat a static segment at the same depth. This hit translated sites hardest: under pages/[lang]/ every page is dynamic, so [lang]/[slug] shadowed every static page beside it. WebSocket routes used plain string order. routes/ already compared segment by segment, but let parameter names break ties.

All three now share one comparator: catch-alls last, fully static routes first, then the first differing segment decides (static, then [param], then catch-all). Parameter names never decide. The Pages client matcher classifies segments with the same helper, so its speculative match agrees with the server.

Unchanged: catch-all routes still rank after every other route, so /docs/intro goes to [lang]/[slug] before docs/[...path].

Docs: a short "Overlapping routes" section in the server routing guide, which the pages guide points to.

Tests: a comparator test that checks each pair really overlaps (using the client regex) and that the order doesn't depend on input order, plus one regression test each for the pages scan (through the generated matchRoute), the routes/ scan and the WebSocket scan. All three scan tests fail on main.

🤖 Generated with Claude Code

Pages sorted dynamic patterns by comparing the whole pattern string, so
`[lang]/about` (/:lang/about) registered ahead of `users/[id]` and won
/users/about, and `[lang]/[slug]` shadowed every static sibling under
`[lang]/`. WebSocket routes sorted by plain string order, so
`chat/[room]` shadowed `chat/lobby`. The routes/ comparator ranked by
segment but let parameter names break ties, so `[category]/[item]` won
/fr/about over `[lang]/about`.

Pages, routes/ and WebSocket routes now share one comparator: catch-alls
last, fully static routes first, then the first differing segment
decides (static, then param, then catch-all). Parameter names never
decide. The Pages client matcher classifies segments with the same
helper, so ranking and matching agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it.

@cpojer
cpojer merged commit bc9c05e into voidzero-dev:main Nov 4, 2026
18 checks passed
@yangshun
yangshun deleted the fix/route-specificity branch November 4, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants