Repository navigation
fix(browser): CSS url() self-references are fragment-only, and a canonical verdict reports the canonical it declared; v1.40.0 - #239
Merged
Conversation
…shot is correct at every URL it is served at; v1.40.0 A snapshot is served at more URLs than the one it was rendered at: every query-string variant of a route whose key drops the query, and (plugin entityServe, #237) every other spelling of the same entity. A script-built `fill="url(<page URL>#id)"` (a reviews widget's star fills) names a different document at any of those URLs, so the reference becomes external. The serialized snapshot now rewrites CSS `url(<this page's URL>#id)` -- absolute, scheme-relative or path-absolute, any quoting, raw or with `&` serialized as `&` -- to `url(#id)`. A fragment-only url() always refers to the current document whatever its URL or <base>, so the rewrite is equivalent at the render URL. hrefs are left alone (they resolve against <base>). A page without such a reference pays two substring scans. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to rewrite absolute self-references in serialized snapshots to fragment-only URLs (e.g., converting url(#id) to url(#id)), ensuring CSS references remain valid when snapshots are served at different URLs. The changes include the core rewriting logic in selfReferences.ts, integration into the renderer's post-processing step, updated documentation, and comprehensive tests. I have no feedback to provide as the implementation is robust and well-tested.
This was referenced Oct 2, 2026
…ther URL the rewritten stars fill and the absolute ones do not; v1.40.0 The PR claimed an absolute self-reference loses its fill at another URL as a hypothesis. Measured with Chrome 148: at a second same-origin URL, `url(<render URL>#id)` and the path-absolute spelling paint nothing (an external paint server), the browser fetches the render URL trying to resolve it, and `url(#id)` paints. The new test renders a page, serves the snapshot at another URL and reads the pixel; a control with the absolute reference restored must read unfilled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…red (declaredCanonical), so the plugin learns where the product lives now; v1.40.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… canonical verdict, posts with a stored sitemap render too, http(s) and keyable only, reset on every verdict; v1.40.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two changes, both for serving one entity's render at its other spellings (#237):
url(<this page's URL>#id)tourl(#id), so a reference a script built fromlocationstill points into the same document when the snapshot is served at a URL other than the one it was rendered at. This is the "known difference to fix first".declaredCanonical. The plugin's entity registry records it (#243): after a re-slug, the render of the old spelling is often the first fetch from the origin to see the new canonical, and the result used to carry only the verdict.Why
queryParams: []). With the plugin's entity serve (#237) it is also served at every other spelling of the same entity.fill="url(<page URL>#rating_star_filled)"(observed). Every product link in those snapshots was the canonical, and there were no share links.url(<absolute first URL>#id)url(<first path>#id)(path-absolute)url(#id)What
src/selfReferences.ts(new):fragmentOnlySelfReferences(html, pageUrl)matches a CSSurl(in any case, with optional whitespace and an optional",'or"quote, followed by this page's URL and#. The URL can be absolute, scheme-relative or path-absolute, and is matched exactly with its query, both raw and with&serialized as&inside attributes.href. Anhrefresolves against<base>, so a fragment-only href is not the same reference.src/renderer.ts: applied to the post-processed HTML usingpage.url(), inside the timedpostProcessphase.url(#id)refers to the current document whatever its URL and whatever<base>says (CSS Values 4, fragment-only URLs).README.md: a section under the post-processing docs.declaredCanonicalsrc/renderer.ts:indexVerdictreturnsdeclaredCanonicalfor a CANONICAL verdict only (canonical-mismatch, or a strictcanonical-variant), never for anoindexpage, whose canonical is not something to adopt a target from. It is resolved against the rendered URL exactly ascanonicalVerdictresolved it to reach the verdict, with the fragment dropped, and only for an http(s) URL of at most 2,048 characters (past ~1,978 bytes no Harper key could hold it). Both verdict paths, the pre-settle bail and the post-settle check, assign it on every verdict, so an earlier attempt's never outlives a later one's.src/RenderJob.ts: posted with or without content. A sitemap target is serialized even when its canonical names another URL, and that re-slugged listed URL is exactly the one whose new canonical the plugin wants. It is absent, not null, when there is nothing to say, so an older plugin never sees the key.src/renderOnce.tssurfaces it too.Verification
test/selfReferences.test.ts: every quoting the serializer produces, the scheme-relative and path-absolute spellings, the query (raw and&), a hash on the page URL, and the cases that must not change (another document, a prefix-sharing path, another scheme,href,<use href>, plain text).renderOnceof a page whose script buildsfill="url(" + location.href + "#star_filled)": the stored HTML carriesurl(#star_filled)and no absolute self-reference. With the rewrite disabled, that test fails.declaredCanonical:test/earlySettleSkip.test.ts:indexVerdictresolves an absolute or a path-absolute canonical, drops a fragment, and reports a strict variant.test/jobResult.test.ts: it is on the wire for a canonical verdict without content, and for a sitemap target's stored render with content, and absent from an indexable render.Render parity
Stored HTML changes only on pages that carry an absolute or path-absolute
url(<self>#…). On those, the only bytes that change are the URL inside each such reference.structuredOffers,pageFacts, outcome, status and indexability are read before this step and are unaffected.Verified on live product pages. A fresh-context agent ran the deployed render config through render-service's
render-onceagainst one production origin, 2026-10-02. It rendered three product pages with a reviews widget, on desktop and mobile, with the released 1.38.0 and with this build, back to back:url(#rose by 50 per document. Nourl(naming the page's own URL or path with a fragment remains.isIndexable,reason,redirectedTo,structuredOffersandpageFactsare JSON-equal on all 8 pairs.declaredCanonical:canonical-mismatch, and 1.40.0 reports the product's real canonical, which renders indexable. 1.38.0 has no such key.declaredCanonical. It now does. Its storedpageFacts.canonicalcarries the same URL as well, and the plugin reads either (#243).Not in this PR
<use href="<self>#id">and otherhrefself-references are not rewritten (see above).Release: browser 1.40.0 (1.39.0 is reserved by #228). No config change. render-service picks it up with its normal tarball bump after the release.
🤖 Generated with Claude Code