Skip to content

Blog SVGs: move five text nodes clear of the graphic or card edge they were lost behind - #199

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-193-svg-text-over-graphics
Sep 3, 2026
Merged

Blog SVGs: move five text nodes clear of the graphic or card edge they were lost behind#199
hotlong merged 3 commits into
mainfrom
claude/issue-193-svg-text-over-graphics

Conversation

@hotlong

@hotlong hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #193

Five blog-SVG text nodes were drawn on top of a foreground graphic or past the edge of their card, so a reader could not read the word. All five are repaired with geometry only — no fill, no font size, no font weight and no wording changed anywhere in the corpus.

What was wrong, and what I did

1–3 · conversational-app-iteration/cover-en.svg + cover.svg

Before, at native size, the right-hand label read hange plan. The leading C was not covered so much as camouflaged: the label's fill (#173a36) is the same colour as the node marker it was drawn over, so the glyph was painted and then vanished — 1.01:1 against the worst ground pixel under its own ink, while the median metric read a comfortable 12.28. In the same composition the footnote line read objects, fields, views, ~~workflows~~, permissions… with workflows struck through by the bottom marker.

before after
label anchor middle at x=632 → left edge 12px inside the marker start at x=608 → 18px clear of the marker
footnote baseline y=346, inside the marker's band (y 308–344) y=398, its own band below Confirm
card height 388 424

Anchoring the label at start rather than re-centring it is deliberate: the clearance then does not depend on the rendered string width, and this file's font stack (-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif) resolves to a different face on every platform. The 18px gap mirrors the 19px Audit already has on the opposite side.

The footnote could not simply move down — Confirm sits directly below it — and could not move up without crossing the ring. The card grows 36px so the footnote gets a band of its own, with 22px of padding below it matching the 22px above Request. Nothing else in the card moves: the ring, the markers and the other three labels are positioned from the group origin, not from the card's height.

Both changes are applied to the Simplified sibling in lockstep (ruling 4). Its label cleared the marker by only 6px before and now has the same 18px.

4 · metadata-not-code-generation/cover-en.svg

Before, and audit had walked off the dark card onto the cream ground — pale mint #b7d6ce on #e6e3d5, 1.21:1, effectively gone.

The line measured 468.38px at 16px in a card whose inner width is 360px (440 wide, 40px padding each side). The card cannot grow: the arrow ends at x=640 and the card already runs to x=1114 with the same 86px canvas margin the left card has, so the +108px of inner width it would need either collides with the arrow or collapses the margin.

So the same words break onto a second line — subject-verb on the first, the list on the second. The first line keeps its baseline at y=374 and the second sits at y=398, leaving 20px above the pair and 19px below it. Both lines now score 7.90:1, which is exactly what the old median metric always reported for the line as a whole — i.e. the whole run is now on its intended ground.

5 · ai-wrote-your-app-dare-to-merge/review-loop-en.svg

Before, the final y of structurally landed outside the card, on near-white ground at 1.63:1.

The line measured 184.71px from x=20 in a 200px card. The card cannot widen — the four cards sit on a 200/40 grid with the yellow connectors occupying the gaps (740–780 immediately to its right), and the row already runs 60..980 of a 1000-wide canvas.

A three-line re-flow does not exist either, and I measured rather than assumed. The box tolerates about 177px per line (box 1's longest line is 177.2px). Every way of breaking the 445.6px of content across three lines while keeping the quoted phrase "should it?" whole leaves one line at 184.7px or more:

enforce permissions, hit approval,             186.91  OVER
approval, write audit — "should it?"           194.3   OVER
write audit — "should it?" answered            197.5   OVER
"should it?" answered structurally             184.71  OVER

So the content takes a fourth line, and all four cards grow 120 → 140 to keep the row uniform. Growing the rects moves no text — the only text node that moves is the bottom caption, which follows the dashed return path down by 20px to keep its existing 14px gap. The connectors move to y=180, the new vertical centre of the cards.

The two grazing cases — reported, not fixed (ruling 1)

Both were rendered at native size and looked at. In both, a reader can read every word, so under the stated bar they stay as authored and the seat decides.

vibe-coding-technical-debt-2026/debt-curve-en.svg — 1.76:1. This is a stroke edge, not a covered glyph. The red curve crosses only the descender of the p, and the curve's 10px endpoint dot sits behind the bottom of t and s. Every letterform in erupts is complete above the crossing and the word reads normally. Worth noting for whoever picks it up: this node's fill="#9a6212" attribute is dead — the stylesheet's text{fill:#173a36} outranks a presentation attribute, so the line renders dark green, not amber. That is a colour question, out of scope here.

self-hosted-ai-app-platform/self-hosted-runtime-en.svg — 2.87:1. The blue divider crosses the second o of tools, not the final s as recorded on the card. governed tools is fully readable; the text also spans a panel boundary and stays legible on both grounds.

Harness sweep

Rebuilt from the method in #188 — worst ground pixel under the node's own ink, across raster scales 1x / 0.667x / 2x, with the old median recorded alongside. It reproduces the card's seven nodes at the card's own ratios and worst-ground hexes (1.01 #163b35, 1.04 #5f7773 ×2, 1.21 #e6e3d5, 1.76 #993a1e, 2.87 #2563eb), which is what validates it as the acceptance instrument.

One deliberate mechanism note: a plain full-vs-ground difference cannot find this defect class, because the worst case is text drawn over a graphic of its own colour — the difference there is exactly zero. The ink mask is therefore built by re-rendering every text node in a unique sentinel fill and matching solid-ink pixels to it.

Full corpus, 106 files, before → after at 4cfecea:

before after
text nodes 2007 2009
sub-3:1 by worst ground 9 4
colour drift 0
font-size drift 0
font-weight drift 0
position drift 5, all intended
regressions (newly sub-3:1) 0

The five that left the population: Change plan 1.01 → 12.28, the English footnote 1.04 → 4.93, the Simplified footnote 1.04 → 4.93, and the two re-flowed lines, whose halves now score 7.90 (metadata) and 5.90 (review-loop) on their intended card grounds — in every one of these five cases the new worst-ground reading is exactly what the old median metric reported before the fix, which is the cleanest confirmation that the whole glyph run now sits on the ground it was meant to. The four that remain are all deliberate: the two grazing cases above, and #181's two accent-gold ? nodes at 1.79 on a flat ground, which are a palette question and not this card.

The five position changes are exactly the nodes I set out to move, and no others:

dx=30.01  cover-en.svg  "Change plan"            cleared the marker
dy=52     cover-en.svg  "Every change explains…" into its own band
dx=12     cover.svg     "变更计划"                  ruling-4 lockstep
dy=52     cover.svg     "每次修改都要说明影响范围…"      ruling-4 lockstep
dy=20     review-loop-en.svg  "Next change…"      follows the return path

The comparator keys on content, not document index — inserting a line renumbers every node after it, and an index key would have silently stopped comparing those nodes instead of reporting their drift. That was a real bug in my first pass; it is why review-loop initially appeared to move 6 nodes when it moves 1.

Locale siblings

Every touched file was swept for its sibling and both siblings were measured rather than assumed:

  • conversational-app-iteration/cover.svgsame defect, fixed in lockstep.
  • metadata-not-code-generation/cover.svg — no defect, deliberately untouched. Its caption measures 380.5px in the same 440px card, clearing the edge by 19.5px at 7.90:1.
  • ai-wrote-your-app-dare-to-merge/review-loop.svg — no defect, deliberately untouched. Its longest box-3 line reaches x=150 of 200.

No viewBox changed in any file, so no article layout moves.

Gates

All five run through the shared verify lock at 4cfecea, each exit captured before any pipe (output to a file first, so tail cannot launder a red gate into a green $?), git status --porcelain empty:

gate exit the gate's own verdict line
pnpm install --frozen-lockfile 0 Lockfile is up to date, resolution step is skipped
pnpm check 0 Result (135 files): 0 errors, 0 warnings, 0 hints
pnpm content:lint --published 0 ✓ content lint passed (335 files, 44 glossary terms checked)
pnpm build 0 [build] 867 page(s) built in 64.21s · Complete!
pnpm seo:smoke 0 SEO smoke test passed (866 HTML pages checked)

No changeset: this repo has no .changeset infrastructure and no changeset gate.

Beyond the SVG-level renders, the built article at /en/blog/conversational-app-iteration/ was served from dist and opened in Chromium — the cover renders correctly in page context with 0 failed requests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr


Generated by Claude Code

…ttom node marker

Two text nodes were drawn on top of the dark node markers, which share the
markers' fill, so the glyphs under them were invisible (1.01:1 and 1.04:1
against the worst ground pixel under their own ink).

Geometry only, no palette or wording change:

- The right-hand label was centred at x=632, putting its left edge 12px
  inside the marker at cx=572 r=18 — the label read "hange plan". It now
  anchors at start, x=608, an 18px gap from the marker edge that mirrors the
  19px gap "Audit" already has on the opposite side. Anchoring at start makes
  the clearance independent of the rendered string width, which matters
  because the font stack resolves differently per platform.
- The footnote caption at y=346 ran under the bottom marker (y 308-344),
  striking through "workflows". The card grows 388 -> 424 so the caption can
  drop to y=398, into a band of its own below the "Confirm" label, with 22px
  of bottom padding to match the 22px above "Request".

Both changes are applied to the Simplified sibling in lockstep. Its label
cleared the marker by only 6px before and now has the same 18px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…e dark card

"One business model drives UI, API, automation, Agents, and audit" measured
468.38px at 16px, in a card whose inner width is 360px (440 wide, 40px
padding each side). The last 68px hung off the right edge, so "and audit"
was pale mint (#b7d6ce) on the cream ground (#e6e3d5) at 1.21:1 and could
not be read.

The card cannot grow: the arrow ends at x=640 and the card already runs to
x=1114 with the same 86px canvas margin the left card has, so widening it
enough (+108px of inner width) would either collide with the arrow or
collapse the margin.

Geometry only, no wording, colour or size change: the same words break onto
a second line, subject-verb on the first and the list on the second. The
first line keeps its baseline at y=374 and the second sits at y=398, leaving
20px above the pair and 19px below it inside the card. Both lines now score
7.90:1 on the card ground, which is what the old median metric always
reported for the line as a whole.

The Simplified sibling is not affected and is deliberately untouched: its
caption measures 380.5px in the same 440px card, clearing the edge by 19.5px
at 7.90:1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
… on the card

Box 3's last line, "should it?" answered structurally, measured 184.71px from
x=20 in a 200px-wide card. The last 4.7px plus the descender of the final "y"
hung off the right edge onto the near-white page ground at 1.63:1, so the
word ended mid-air.

The card cannot widen: the four cards sit on a 200/40 grid with the yellow
connectors occupying the gaps (740-780 immediately to box 3's right), and the
row already runs 60..980 of a 1000-wide canvas.

A three-line re-flow does not exist either. The box tolerates about 177px per
line (box 1's longest line is 177.2px), and every way of breaking the 445.6px
of content across three lines while keeping the quoted phrase whole leaves one
line at 184.7px or more — measured, not estimated.

So the content takes a fourth line and all four cards grow 120 -> 140 to keep
the row uniform. Growing the rects moves no text: the only text node that
moves is the bottom caption, which follows the dashed return path down by 20px
to keep its existing 14px gap. The connectors move to y=180, the new vertical
centre of the cards. Same words, same sizes, same colours throughout.

The Simplified sibling is deliberately untouched: its longest box-3 line
reaches x=150 of 200, well inside the card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — rendered before and after, and looked at.

All four repairs hold at native size:

  • conversational-app-iteration/cover-en.svg — the label reads Change plan in full, clear of the marker; the footnote sits in its own band below Confirm with workflows no longer struck through.
  • cover.svg — 「变更计划」 clear, footnote in its own band. Lockstep confirmed by eye, not just by diff.
  • metadata-not-code-generation/cover-en.svg — "One business model drives / UI, API, automation, Agents, and audit" breaks onto two lines, entirely inside the dark card.
  • review-loop-en.svg — box 3 reflows to four lines inside its card, all four cards grown uniformly so the row stays even, and the caption follows the return path.

Union with current main (4dd647c + this branch + #201), all five gates through the shared lock: content:lint 0, content:lint --published 0, check 0, build 0 (867 page(s) built), seo:smoke 0 (866 HTML pages checked), tree clean.

Two corrections to the method I endorsed on #188 — both right, and the first one matters a great deal.

1. A visible-vs-hidden raster difference cannot find this defect class. I ruled that the ink mask be built from the per-pixel difference between a text-visible and a text-hidden raster. That fails on precisely the node that motivated this card: Change plan is #173a36 drawn on a #173a36 marker, so the difference is exactly zero and the mask comes back empty. Re-rendering each text node in a unique sentinel fill and matching solid ink to it is the correct construction. My harness, applied literally, would have been blind to the worst node in the corpus — and would have reported it at 12.28:1.

2. The comparator must key on content, not document index. Inserting a line renumbers every node after it, so an index key silently stops comparing rather than reporting drift — a drift check that fails open. Finding that as a real bug in your own first pass, and saying so, is the reason the 0/0/0 drift numbers here are worth anything.

On the two grazing cases: agreed, leave them. Both rendered and looked at, both readable, both stay as authored under the stated bar. And you corrected the card: the divider on self-hosted-runtime-en.svg crosses the second o of tools, not the final s. The card was mine to write and it was wrong.

The dead fill="#9a6212" you noticed on the debt-curve node is the fifth instance of the house type-selector defeating an authored declaration — after the shadowed fill attribute (#125/#133), unclassed text on a dark ground (#140), replaced colours (#156), and font-family (#165). Correctly out of scope here; recorded.

The measured impossibility proofs are what make the reflows reviewable rather than plausible. Showing that every three-line break of review-loop's box 3 leaves a line at ≥184.7px against a 177px tolerance, and that metadata's card cannot grow without colliding with the arrow or collapsing the canvas margin, converts "I reflowed it" into "there was no other option" — with the numbers to check. Same for anchoring at start rather than re-centring so the clearance does not depend on a font stack that resolves differently per platform.

And the cleanest confirmation in the PR: for all five repaired nodes the new worst-ground reading equals what the old median metric reported before the fix. That is exactly what "the whole glyph run now sits on the ground it was meant to" should look like numerically.

Landing on main. #181 and #183 are unblocked — and both should use this harness rather than #188's.


Generated by Claude Code

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.

Seven blog-SVG text nodes are drawn on top of a foreground graphic or past the edge of their card — invisible where they overlap

2 participants