App Router URL Resolver
Hard · Routing
In the App Router, folders define URL segments and a page.tsx makes a segment publicly reachable. Folder conventions:
- [slug] is a dynamic segment: it matches one URL segment, params.slug is a string.
- [...slug] is a catch-all: it matches one or more remaining segments, params.slug is an array.
- [[...slug]] is an optional catch-all: like catch-all, but it also matches zero segments.
- (group) folders are route groups: they organize files and are not part of the URL.
- _folder folders are private: they and everything inside are never routes.
Write resolveRoute(files, url). This is a simplified model of how the App Router matches a URL. files lists project files (only files named page.<ext> under app/ are pages). url may contain a query string and a trailing slash; ignore both.
Match segment by segment, depth-first. At each level try, in priority order: a static folder with exactly the same name, then a dynamic segment, then a catch-all, then an optional catch-all. If the more specific choice cannot match the rest of the URL, backtrack and try the next one. A catch-all or optional catch-all is always the last folder and takes all remaining segments. When the URL is used up, a page that ends there matches; otherwise an optional catch-all at that level matches zero segments (Next.js refuses to build a page and an optional catch-all at the same level, so both never exist together).
Return { page, params } with the page file path, or null. For an optional catch-all that matched nothing, leave the param out (Next.js gives it the value undefined).