fix: support static metadata URL resolver (fillStaticMetadataSegment)#946
Conversation
…cloudflare#861) Port fillStaticMetadataSegment from Next.js (commit 7873aea) to correctly resolve URLs for static metadata files under dynamic parent segments. - Add fillStaticMetadataSegment to compute static URLs with "-" placeholders for dynamic/catch-all parent segments - Update scanMetadataFiles to normalize dynamic parents in servedUrl for static metadata files (e.g. blog/[slug]/opengraph-image.png → /blog/-/opengraph-image) - Strip route groups and parallel route slots from URL paths, applying djb2 hash suffixes for collision avoidance - Keep dynamic metadata files on the existing urlPrefix path for patternParts-based runtime matching Ported from Next.js: test/e2e/app-dir/metadata-static-file/
commit: |
|
/bigbonk review |
There was a problem hiding this comment.
Good work porting fillStaticMetadataSegment from Next.js. The core logic is correct: dynamic segments get - placeholders, route groups and parallel slots are stripped from URLs, and djb2 hash suffixes disambiguate collisions from invisible parents. Tests are thorough and well-structured.
A few issues worth addressing — one correctness nit, one code-health concern, and a couple of minor observations.
|
|
||
| function getMetadataRouteSuffix(page: string): string { | ||
| const lastSlash = page.lastIndexOf("/"); | ||
| const parentPathname = lastSlash > 0 ? page.slice(0, lastSlash) : lastSlash === -1 ? "" : ""; |
There was a problem hiding this comment.
Both branches of the inner ternary return "", making it a no-op:
lastSlash === -1 ? "" : ""This reads like one branch was meant to be something else (maybe the full page string?), or it's just a simplification artifact. Either way it should be collapsed:
| const parentPathname = lastSlash > 0 ? page.slice(0, lastSlash) : lastSlash === -1 ? "" : ""; | |
| const parentPathname = lastSlash > 0 ? page.slice(0, lastSlash) : ""; |
There was a problem hiding this comment.
Fixed in b8d8bb1 — collapsed the inner ternary since both branches returned empty string.
| // icon, apple-icon, etc.). Dynamic files keep the existing urlPrefix | ||
| // behavior because the runtime uses patternParts for matching. | ||
| const appDirPath = parentSegments.length > 0 ? `/${parentSegments.join("/")}` : ""; | ||
| const servedUrl = isStatic |
There was a problem hiding this comment.
Nit: the inline IIFE duplicates logic that fillStaticMetadataSegment and its helpers already encapsulate — getStaticMetadataRoute, getMetadataRouteSuffix, suffix application. The only difference is that scanMetadataFiles uses config.urlPath (extensionless, e.g. /icon) while fillStaticMetadataSegment uses the actual filename with extension.
This means:
fillStaticMetadataSegmentis exported but not used byscanMetadataFiles— it's only exercised in tests.getMetadataRouteFilenameis only called byfillStaticMetadataSegment, so it's also dead code in production.
Consider one of:
- Refactor so
scanMetadataFilesactually callsfillStaticMetadataSegment(or a shared helper), passingconfig.urlPathinstead of the real filename. This eliminates the duplication and ensures the exported function is the one tested and used. - If
fillStaticMetadataSegmentis intended for future use outside of scanning (e.g., runtime resolution), document that intent and add a comment inscanMetadataFilesexplaining why it doesn't delegate.
As-is, the inline code and the exported function could drift apart silently.
There was a problem hiding this comment.
Fixed in b8d8bb1 — extracted a shared computeMetadataRouteSuffix helper that both scanMetadataFiles and fillStaticMetadataSegment/`getMetadataRouteFilename` now use. Eliminates the duplicated route+suffix computation.
| function getMetadataRouteSuffix(page: string): string { | ||
| const lastSlash = page.lastIndexOf("/"); | ||
| const parentPathname = lastSlash > 0 ? page.slice(0, lastSlash) : lastSlash === -1 ? "" : ""; | ||
| if (page.endsWith("/sitemap") || page.endsWith("/sitemap.xml")) return ""; |
There was a problem hiding this comment.
Minor inconsistency: getMetadataRouteSuffix only exempts sitemap from hash suffixes (line 399), while the existing metadataRouteSuffix (line 454) exempts sitemap, robots, and manifest.
This is inconsequential today because robots and manifest are nestable: false (so they only appear at root, where there are no invisible parents). But if nestable ever changed, the two functions would disagree. Worth either:
- Adding the same exemptions here for defensive consistency, or
- Adding a brief comment noting why only sitemap is exempted (because this function is ported from Next.js where robots/manifest aren't relevant).
There was a problem hiding this comment.
Filed as follow-up issue #963. The inconsistency is theoretically latent since robots/manifest are nestable: false, but worth tracking.
| } | ||
|
|
||
| function isParallelRouteSegment(segment: string): boolean { | ||
| return segment.startsWith("@") && segment !== "@children"; |
There was a problem hiding this comment.
Just confirming: isParallelRouteSegment excludes @children (so it doesn't trigger hash suffixes), while getStaticMetadataRoute at line 390 uses seg.startsWith("@") (strips all @-prefixed segments including @children from the URL). The comment at 387-389 explains this well. The asymmetry is correct and intentional — @children is invisible in URLs but doesn't need disambiguation.
This is fine as-is; just flagging that the two checks look different for a reason.
| }); | ||
|
|
||
| it("strips route groups and applies hash suffix", () => { | ||
| const result = fillStaticMetadataSegment("/(group)/group", "icon.png"); |
There was a problem hiding this comment.
Nice — regex-matching the hash suffix rather than hardcoding a specific hash value is the right call. Makes the tests resilient to hash algorithm changes while still asserting the structural invariant.
|
Review posted successfully. Here's a summary of the feedback: Overall: The PR correctly ports Key comments posted:
|
- Fix redundant ternary: both branches of `lastSlash === -1 ? "" : ""` returned empty string - Extract `computeMetadataRouteSuffix` shared helper to eliminate code duplication between `scanMetadataFiles` inline IIFE and `fillStaticMetadataSegment`/`getMetadataRouteFilename` Co-Authored-By: ask-bonk[bot] <noreply@github.com>
|
All three review issues addressed:
|
|
/bigbonk review |
There was a problem hiding this comment.
All three issues from the previous review are addressed:
- Redundant ternary — collapsed to
lastSlash > 0 ? page.slice(0, lastSlash) : "". - Duplicated logic — extracted
computeMetadataRouteSuffixas a shared helper used by bothscanMetadataFiles(static branch) andfillStaticMetadataSegment/getMetadataRouteFilename. The core route+suffix computation is no longer duplicated. - Sitemap-only exemption — tracked in #963.
Code is clean, tests pass (69/69), vp check is green. The implementation correctly ports the Next.js fillStaticMetadataSegment behavior: dynamic segments become - placeholders, route groups and parallel slots are stripped from URLs, and djb2 hash suffixes disambiguate collisions from invisible parents.
One minor observation (not blocking): fillStaticMetadataSegment and getMetadataRouteFilename are still only exercised in tests — scanMetadataFiles uses computeMetadataRouteSuffix directly rather than delegating through fillStaticMetadataSegment. This is fine since the shared helper eliminates the drift risk, and fillStaticMetadataSegment is presumably intended for future runtime use. The dynamic branch in scanMetadataFiles still uses the old metadataRouteSuffix function, which produces identical hashes for the same input — verified by inspection.
|
Review posted. All three issues from the previous review have been addressed:
Tests pass (69/69), |
Fixes #861
Summary
fillStaticMetadataSegmentfrom Next.js (commit 7873aea) to correctly resolve URLs for static metadata files under dynamic parent segmentsscanMetadataFilesto normalize dynamic segments to "-" placeholders in servedUrl for static metadata filesurlPrefixpath for patternParts-based runtime matchingfillStaticMetadataSegmentand integration tests for static metadata scanning under dynamic parentsTest plan
tests/metadata-routes.test.ts— 69 tests (8 new forfillStaticMetadataSegment, 3 new for scan with dynamic parents)tests/app-router.test.ts— metadata routes section (20 tests, all passing)