fix: resolve client preview images against the article canonical - #128
Merged
LunaStev merged 1 commit intoSep 26, 2026
Merged
Conversation
internal/web/seo.go resolves the first Markdown preview image against the article canonical, while frontend/src/services/seo.ts resolved it against window.location.origin. For  in /blog/compiler-notes the server rendered .../blog/images/pipeline.png and the client then overwrote og:image and twitter:image with .../images/pipeline.png. absoluteURL and firstMarkdownImage now take the document base explicitly, applyPageSEO resolves image metadata against its own canonical, and BlogPage.vue passes the canonical it already computes, so a release article reached through its legacy /blog/<slug> URL resolves against /releases/<slug> like the server does. The image pattern and value handling mirror the server parser: the whitespace class is spelled out because JS \s also matches Unicode spaces, and the value is trimmed and rejected when it contains a control character, as net/url does. Without those a client-only match failure drops a preview image the server rendered. frontend/tests/seo-image.test.ts mirrors the fixtures in internal/web/seo.go, and make test now runs them next to the existing frontend tests.
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.
The client resolved the first Markdown preview image against the site origin while
internal/web/seo.goresolves it against the article canonical. Forin/blog/compiler-notesthe server renderedhttps://wave.example/blog/images/pipeline.pngand the client then replacedog:image/twitter:imagewithhttps://wave.example/images/pipeline.png.absoluteURLandfirstMarkdownImagenow take the document base explicitly instead ofwindow.location.origin,applyPageSEOresolves image metadata against its own canonical, andBlogPage.vuepasses the canonical it already computes. A release article reached through its legacy/blog/<slug>URL therefore resolves against/releases/<slug>, matching the server canonical and its 308 redirect.The Markdown image pattern and value handling now mirror the server parser too: the whitespace class is spelled out because JS
\salso matches Unicode spaces such asU+00A0andU+3000, and the captured value is trimmed and rejected when it contains a control character, asnet/urldoes. Without them a pattern that matches on only one side lets the client drop a preview image the server rendered.Verification
frontend/tests/seo-image.test.tsfixtures mirrormarkdownImageininternal/web/seo.go: relative, root-relative, absolute HTTP(S), protocol-relative,../, Markdown titles, angle-bracket URLs, Unicode spaces, trimmed destinations, rejected schemes, rejected control characters, and no fall-through to a later valid image.markdownImagefor 20 realistic fixtures plus 28 whitespace and control-character probes: identical results, 0 differences.https://wave.example/images/pipeline.pngandhttps://wave.example/images/graph.png; the new tests fail on that revision and pass on this one.GET /blog/v0.2.2rendersrel="canonical" href=".../releases/v0.2.2"withog:image/twitter:image.../releases/images/graph.png, which is the base the client now uses.make test:go test ./...,vue-tsc --noEmit,npm run test:docs(2 tests) andnpm run test:seo(5 tests) all pass. The Makefile now runs the frontend unit tests, which were previously unwired.Notes
BlogPage.vueis the only producer of preview images; every otherapplyPageSEOcall site passes no image, so no other page changes.U+0085) or BOM (U+FEFF) directly at the start or end of an image destination is trimmed differently, becauseString.prototype.trim()andstrings.TrimSpace()define whitespace differently. It only changes the resulting URL string, never whether an image exists, and interior occurrences match.Closes #72.