Skip to content

fix: resolve client preview images against the article canonical - #128

Merged
LunaStev merged 1 commit into
wavefnd:masterfrom
JJJ-NONAME:fix/client-preview-image-canonical
Sep 26, 2026
Merged

LunaStev merged 1 commit into
wavefnd:masterfrom
JJJ-NONAME:fix/client-preview-image-canonical

Conversation

@JJJ-NONAME

Copy link
Copy Markdown
Contributor

The client resolved the first Markdown preview image against the site origin while internal/web/seo.go resolves it against the article canonical. For ![Diagram](images/pipeline.png) in /blog/compiler-notes the server rendered https://wave.example/blog/images/pipeline.png and the client then replaced og:image/twitter:image with https://wave.example/images/pipeline.png.

absoluteURL and firstMarkdownImage now take the document base explicitly instead of window.location.origin, applyPageSEO resolves image metadata against its own canonical, and BlogPage.vue passes 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 \s also matches Unicode spaces such as U+00A0 and U+3000, and the captured value is trimmed and rejected when it contains a control character, as net/url does. 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.ts fixtures mirror markdownImage in internal/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.
  • Field-by-field comparison against a verbatim copy of markdownImage for 20 realistic fixtures plus 28 whitespace and control-character probes: identical results, 0 differences.
  • Pre-fix reproduction: on the previous module the same inputs resolved to https://wave.example/images/pipeline.png and https://wave.example/images/graph.png; the new tests fail on that revision and pass on this one.
  • Server side of the legacy-URL case: GET /blog/v0.2.2 renders rel="canonical" href=".../releases/v0.2.2" with og: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) and npm run test:seo (5 tests) all pass. The Makefile now runs the frontend unit tests, which were previously unwired.

Notes

  • BlogPage.vue is the only producer of preview images; every other applyPageSEO call site passes no image, so no other page changes.
  • Known residual difference, left out of scope: a raw NEL (U+0085) or BOM (U+FEFF) directly at the start or end of an image destination is trimmed differently, because String.prototype.trim() and strings.TrimSpace() define whitespace differently. It only changes the resulting URL string, never whether an image exists, and interior occurrences match.

Closes #72.

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 ![Diagram](images/pipeline.png) 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.

@LunaStev LunaStev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks

@LunaStev
LunaStev merged commit 1f0b810 into wavefnd:master Sep 26, 2026
1 check passed
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.

Resolve relative article preview images against the same URL on server and client

2 participants