Skip to content

refactor(renderers): migrate SVG generation to satori #44

@evirunurm

Description

@evirunurm

Background

All three card renderers (renderStatCard, renderLanguageCard, renderErrorCard) build SVG output by manually constructing XML strings with inline coordinate math. This makes layout changes fragile — positions are calculated from CARD_WIDTH/CARD_HEIGHT ratios, text x/y values are derived from index arithmetic, and the TextAttr/CardAttr interfaces exist solely to thread positioning state through helper functions.

Satori renders a JSX/React element tree to an SVG string, replacing coordinate math with CSS flexbox. It runs in Node/Edge environments and returns a plain string, matching the existing return type.

Goal

Replace the string-template approach in all renderers with satori-based JSX components, eliminating manual layout math while keeping the public API surface (function signatures and SVG output) identical.

Migration plan

Phase 1 — Setup · #45

  • Add satori and react/react-dom + their @types as dependencies
  • Enable jsx in tsconfig.json ("jsx": "react" or "react-jsx")
  • Bundle a font file (e.g. Inter or Noto Sans) under scripts/assets/fonts/ — satori requires explicit font data; system fonts are unavailable at render time
  • Update jest config to handle .tsx files if not already

Phase 2 — Migrate renderErrorCard · #46

Start here because it is the simplest renderer (no dynamic data, no icons, no variants).

  • Create scripts/renderers/ErrorCard.tsx as a pure JSX component
  • Replace renderErrorCard.ts body with a satori(...) call wrapping the new component
  • Make the function async, update the call site in api/stats.ts
  • Update renderErrorCard.test.ts to use await

Phase 3 — Migrate renderStatCard · #47

  • Create scripts/renderers/StatCard.tsx
  • Replace TextAttr/CardAttr interfaces and mountText/mountIcons imperative loops with flexbox column layout
  • Embed SVG icon assets using satori's img or inline <svg> — satori supports rendering inline SVGs as children
  • Handle the peng/color variants via props
  • Make renderStatCard async, update api/stats.ts
  • Update or add tests

Phase 4 — Migrate renderLanguageCard · #48

Most complex due to the pie chart (stroke-dasharray circles).

  • Create scripts/renderers/LanguageCard.tsx
  • Verify satori supports stroke-dasharray on <circle> — if not, replace the pie chart with a conic-gradient <div> or pre-rendered SVG fragment passed as a raw string child
  • Replace calcPercentages usage (already unit-tested separately — keep that logic as-is, only change rendering)
  • Make renderLanguageCard async

Phase 5 — Cleanup · #49

  • Delete TextAttr, CardAttr, createText, createIcon, mountText, mountIcons from old files
  • Remove the DIVIDER_Y / ERROR_DIVIDER_Y constants if no longer referenced
  • Confirm CI (typecheck, test) passes

Constraints

  • Satori only supports a subset of CSS — absolute positioning and stroke/stroke-width on rects are not supported; layout must use flexbox
  • Satori is async — all three renderer functions will become async, and their callers must await them
  • Font files must be bundled explicitly — no fallback to host system fonts
  • The pie chart's stroke-dasharray on <circle> elements may not be supported by satori and may require a workaround in Phase 4

Out of scope

  • Visual redesign of the cards
  • New card types
  • Changes to the GitHub data-fetching layer

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions