Skip to content

fix(site-import): resolve cache-busted asset URLs - #539

Open
spcxai2025 wants to merge 1 commit into
CoreBunch:mainfrom
spcxai2025:fix/import-cache-busted-asset-urls
Open

spcxai2025 wants to merge 1 commit into
CoreBunch:mainfrom
spcxai2025:fix/import-cache-busted-asset-urls

Conversation

@spcxai2025

Copy link
Copy Markdown

Summary

A query string or fragment was never stripped before looking a path up in the FileMap, so assets/site.css?v=8f3a1c resolved to a key that cannot exist. The reference was reported as missing-stylesheet / missing-script while the file sat in the archive, and every style rule, colour token and font in that sheet was dropped from the import.

Static site generators commonly emit a content hash on stylesheet, script and media URLs, so this affects a whole class of input. Measured on one real 6-page export:

with ?v= same site, queries removed
style rules 401 659
colour tokens 18 26
font families 0 1
warnings 12 0

A 39% and 31% loss, surfaced only as warnings a user can click past.

The handling was inconsistent rather than absent, which is probably why it survived: linkRewrite stripped the query itself before calling resolveHref, so internal page links resolved correctly while stylesheets did not — and assetPlan stripped one for MIME sniffing but not for resolution.

The change

Fixed at both resolution primitives:

  • resolveHref<link rel=stylesheet>, <script src>, CSS @import, and page links via linkRewrite
  • resolveRelativePath — CSS url(), node src / href / srcset

and removed the now-redundant strip in linkRewrite. Its fragment split stays — the fragment is re-attached to the page ref by makePageRef and is not addressing.

This cannot affect the rewrite step. Node props are replaced by the resolved FileMap key and rewriteProps keys on that key; CSS passes the original rawUrl to replaceRawUrlInValue as a separate argument. Stripping changes only which entry is found.

Tests

  • Unit (htmlPagePlan.test.ts) — query, fragment, both, empty-after-strip, and that a query does not change traversal handling.
  • End-to-end (cacheBustedUrls.test.ts) — a cache-busted site produces a plan identical to the same site without queries.

The end-to-end shape matters here: the unit returned a plausible-looking key and every consumer dutifully failed to find it, so the defect was only ever visible downstream as warnings. I verified the new tests fail without the source change (11 failures) and pass with it.

One note on the traversal assertion — I initially wrote it expecting ../../x.css to be rejected, then found joinPaths clamps an escaping .. to the root by design. The test now pins the property this change is actually responsible for: a query string must not alter that handling.

Verification

  • bun run build
  • bun test — 7067 pass, 0 fail
  • bun run lint
  • Docker/deployment check, if relevant — n/a

Checklist

  • Tests cover behavior changes.
  • Docs were updated (docs/features/site-import.md: the missing-stylesheet row plus a short "Cache-busted URLs" note).
  • No compatibility shim was added for old pre-release behavior — the redundant strip in linkRewrite was removed rather than left in place.
  • No secrets, local databases, uploads, or generated artifacts are included.

Happy to split the linkRewrite cleanup into its own commit, or adjust the stripping to ? only if you'd rather leave fragments alone on asset URLs.

A query string or fragment was never stripped before looking a path up in the
FileMap, so `assets/site.css?v=8f3a1c` resolved to a key that cannot exist. The
reference was reported as `missing-stylesheet` / `missing-script` while the file
sat in the archive, and every style rule, colour token and font in that sheet was
silently dropped from the import.

Static site generators commonly emit a content hash on stylesheet, script and
media URLs, so this affects a whole class of input. Measured on one real 6-page
export: 401 style rules and 18 colour tokens imported, against 659 and 26 for the
byte-identical site without the queries — a 39% and 31% loss, reported only as
warnings a user can click past.

The handling was inconsistent rather than absent, which is why it survived:
`linkRewrite` stripped the query itself before calling `resolveHref`, so internal
page links resolved correctly while stylesheets did not, and `assetPlan` stripped
one for MIME sniffing but not for resolution.

Fixed at both resolution primitives:

  - `resolveHref` (stylesheets, scripts, CSS @import, and page links via
    linkRewrite)
  - `resolveRelativePath` (CSS url(), node src/href/srcset)

and removed the now-redundant strip in `linkRewrite`. Its fragment split stays —
the fragment is re-attached to the page ref and is not addressing.

This cannot affect the rewrite step. Node props are replaced by the resolved
FileMap key and `rewriteProps` keys on that key, while CSS passes the original
`rawUrl` to `replaceRawUrlInValue` as a separate argument, so stripping changes
only which entry is found.

Tests: a unit group on `resolveHref` covering query, fragment, both, and
empty-after-strip, plus an assertion that a query does not change traversal
handling; and an end-to-end group over `buildImportPlan` asserting that a
cache-busted site produces a plan identical to the same site without queries.
The end-to-end shape matters here — the unit returned a plausible-looking key and
every consumer dutifully failed to find it, so the defect was only ever visible
downstream. Verified the new tests fail without the source change (11 failures)
and pass with it.

bun run build, bun test (7067), bun run lint all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@spcxai2025
spcxai2025 force-pushed the fix/import-cache-busted-asset-urls branch from 9bad9a4 to 6a64675 Compare September 17, 2026 20:35
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.

1 participant