Public: anyone can read this; the crew can change it.

A pure SVG card renderer: src/web/og/card.ts turns {kind, title, lines, stat} into a 1200×630 card in the studio's tokens #1498

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

This adds a pure SVG card renderer, `renderCard({kind, title, lines, stat})` in `src/web/og/card.ts`. It returns a 1200×630 SVG string and has no dependencies. In the sandbox, `bun test test/og-card.test.ts test/check-list.test.ts` passes (17 tests) and `bun x tsc --noEmit` is clean.

shipped production is serving it deploy d5155e2

opened this

This adds a pure SVG card renderer, renderCard({kind, title, lines, stat}) in src/web/og/card.ts. It returns a 1200×630 SVG string and has no dependencies. In the sandbox, bun test test/og-card.test.ts test/check-list.test.ts passes (17 tests) and bun x tsc --noEmit is clean.

  • Look: colours come from THEMES.dark.tokens in src/design/themes.ts. Type uses the same system and mono stacks as src/web/style/base.ts. The accent colour changes with the kind.
  • Kinds: I chose repo | issue | pull | app | page; the issue didn't name them, so say if the designs (#454, #455) need others. An unknown kind falls back to page.
  • Text safety: every string has XML-forbidden control characters removed and whitespace folded, is clamped with an ellipsis, and is then escaped.
  • Layout: the title wraps to at most 2 lines and lines is capped at 3. The stat sits in the footer.
  • Tests: test/og-card.test.ts checks that each kind renders well-formed XML at 1200×630. It also checks that a hostile title (<script>, &, 500 characters) comes out escaped and clamped, that no text exceeds the inner width, and that control characters and a bad kind are handled. test/og-card.test.ts is now in GATE in scripts/gate.ts. forge.json is untouched and there is no route.

Not done:

  • Width is estimated at 0.62 em per character (0.68 for bold), so some text will be clamped a little early.
  • There is no word-aware wrapping for lines; each line is a single clamped row.
  • The .bash_profile, .claude/ and similar files in the working tree are untracked and not in the commit.

Try it: bun -e 'import {renderCard} from "./src/web/og/card.ts"; console.log(renderCard({kind:"pull",title:"Hello",lines:["a"],stat:"3 open"}))' > card.svg

🤖 Generated with Claude Code

Closes #1470

Approved

A pure, dependency-free renderer that does what the issue asked: it strips what XML cannot carry, clamps to width, then escapes, and falls back to the page kind. The tests cover well-formedness, hostile input and unknown kinds, and the new test is in the gate. It adds no route, so there is nothing to see in the preview, which fits the stated scope. Two things I would do next: (1) give the SVG a <title> and aria-label built from the title, because role=img with no name is silent for a screen reader and for a link unfurler that shows alt text. (2) Tighten the fit test: it measures with a 0.5 em factor against 1040px, but the renderer assumes 0.62 and 0.68 em, so a regression in the bold estimate would still pass. Assert with the renderer own constants.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs