Cambridge II Algebraic Topology

Illustration glyphs: namespaced IDs shipped, stale client caches can still show collisions

#27
Open
Assignee
No one assigned
Labels
No labels
Milestone
No milestone
Aadmin
admin @admin
4/24/2026, 8:05:33 PM

Symptom

On pages with multiple illustrations (e.g. page 1274, "The Rose with Two Petals"), point labels and other text glyphs rendered incorrectly in the browser — letters from one illustration appeared inside a different illustration, or labels went missing entirely.

Root cause

TikZ/dvisvgm emits glyph path definitions with IDs like glyph-0-0, glyph-1-0, clip-0. These IDs are only unique within a single SVG document. When two illustrations are inlined into the same HTML page, both declare id="glyph-0-0" in their defs, and every xlink:href="#glyph-0-0" resolves to whichever definition the browser found first. Result: one illustration's letters are drawn from another's font table, and references to IDs that do not exist in the winning defs render as blank.

The pipeline fix was to namespace every emitted ID with the illustration's slug — glyph-0-0 becomes glyph-rose-two-petals-0-0, clip-0 becomes clip-rose-two-petals-0, and every xlink:href rewritten to match. This was applied in the TikZ compile step and the DB content was updated.

Investigation (and the MCP detour)

I confirmed the server is serving the correct, namespaced SVG:

  • Fetching /api/illustrations/rose-two-petals returns IDs glyph-rose-two-petals-0-0 through glyph-rose-two-petals-2-1 and matching use refs.
  • Diff against the local fixture file shows only a trailing-newline difference (1 byte).
  • The batch endpoint /api/illustrations/batch?slugs=rose-two-petals returns the same namespaced content.

I then wanted to verify what the browser actually renders on the authenticated wiki page, since the user was still seeing broken labels. I ran into:

  1. Requesting /pages/1274 returns the Demo Mode landing page to anonymous visitors, so a plain Playwright run (via the repo's node_modules) could not reach the rendered illustration.
  2. I assumed the claude-in-chrome MCP tools would inherit the user's login session from their real Chrome. I spent time trying the /chrome command, /reload-plugins, and asking for a session cookie — believing a dedicated browser MCP was necessary to inspect the rendered DOM.
  3. Eventually I realised I did not need an authenticated session to prove the SVG itself is correct. The /api/illustrations/* endpoints are public (per the demo-mode middleware), so I could fetch the SVG anonymously and render it into a DOM directly.

Verification

With the browser MCP connected (but unauthenticated), I fetched the SVG from the public API and injected it into document.body on a demo-mode page, then queried the rendered DOM:

  • 6 IDs defined in defs, all namespaced: glyph-rose-two-petals-{0-0, 1-0, 2-0, 2-1}, clip-rose-two-petals-{0, 1}.
  • 4 use elements with xlink:href="#glyph-rose-two-petals-..." — every one resolves to a real definition.
  • use.getBBox() returns non-zero width/height for every label (e.g. {x: 62.27, y: 63.12, w: 4.95, h: 4.52}), confirming the browser is actually rasterising each glyph.

So end-to-end: pipeline, DB, HTTP response, SVG DOM, and rasterised glyphs all check out.

Why the user was still seeing breakage

Since the freshly-fetched SVG renders correctly, the broken labels the user was seeing in their own browser are almost certainly served from a stale cache — either:

  • HTTP cache on the illustration endpoints (hard refresh / Cmd+Shift+R will bust it), or
  • A service worker cached the old non-namespaced response.

Suggested follow-ups

  • Add a cache-busting query param (e.g. ?v=<content_hash>) or ETag / Last-Modified based on the SVG content when the inline image / fetch path is rendered, so republishing an illustration invalidates stale client caches automatically.
  • Add a pipeline assertion: after writing namespaced SVG to DB, sanity-check that no unnamespaced id="glyph-<digits>-<digits>" remains in any stored SVG — catches regressions of this class cheaply.
  • Consider a dev-only route that serves an illustration with Cache-Control: no-store for debugging.

0 Comments

Add a comment