Skip to content

Render non-Latin scripts in OG cards instead of "NO GLYPH" boxes - #20

Merged
dani-polani merged 1 commit into
mainfrom
fix/og-fonts-non-latin
Aug 8, 2026
Merged

Render non-Latin scripts in OG cards instead of "NO GLYPH" boxes#20
dani-polani merged 1 commit into
mainfrom
fix/og-fonts-non-latin

Conversation

@dani-polani

Copy link
Copy Markdown
Member

Found while wiring vertical writing into the API (#19). It predates that work: a horizontal Japanese project breaks identically, so this is not a vertical-writing bug.

The bug

/api/og drew visible "NO GLYPH" boxes for any non-Latin script. resvg ships no fonts of its own, reads only files from disk, the deploy image (node:22-bookworm-slim) has none, and only Inter was bundled.

It reached two places: the social preview of a shared ?data= link (what Telegram, Slack, Discord, X, and Facebook draw) and the inline PNG the MCP tool hands back to an agent. Normal PNG/SVG/PDF export is unaffected — that path runs in the browser with real fonts and never touches resvg.

Two causes

  1. og-svg.ts hard-coded font-family: Inter for the token text and ignored the family the line actually asks for. Even with the correct font loaded, the card requested Inter.
  2. No font for the script existed anywhere.

The fix

Google's CSS API returns TrueType — the only format resvg accepts — when the User-Agent looks old enough to predate woff2, and text= subsets the file to exactly the characters we are about to draw:

Script Subset size
Noto Serif JP (7 chars) 8.5 KB
Noto Sans Mongolian 11 KB
Noto Sans Arabic 4.6 KB
Noto Sans Hebrew 1.9 KB

So the font is fetched per card and cached in-process by family and text. Measured 243ms cold, 19ms warm. Nothing is added to the bundle or the image, and coverage is exact for scripts nobody anticipated.

A line that cannot be covered — uploaded custom font, unknown family, no network — degrades to a neutral 2 lines · 1 link summary instead of boxes. That decision is per line, so a Japanese/English pair keeps its English. Whether Inter can render a string is decided by asking the bundled font for a glyph through opentype.js (already a dependency) rather than guessing at Unicode ranges. A fetch failure is caught and cannot turn the card into a 500; there is a 3s timeout so a hung request cannot hold the route open.

/api/og and the MCP preview had duplicated the resvg call. They now share renderOgPng, so the font handling cannot drift between them.

Verification

Rendered live for Japanese, Mongolian, Hebrew, a deliberately bogus font family, and plain Latin. Japanese, Mongolian, and Hebrew now render their scripts; the bogus family falls back to the summary while its English line still renders; Latin is unchanged. 196 tests green, 7 added over the pure parts (family emission, subset text after truncation, per-line fallback, pluralization, empty-project placeholders).

Independent of #19 — no overlapping changes beyond different regions of mcp/server.ts.

🤖 Generated with Claude Code

resvg ships no fonts and reads only files from disk; the deploy image has
none and only Inter was bundled. Every non-Latin OG card therefore rendered
as visible "NO GLYPH" boxes. This predates vertical writing: a horizontal
Japanese project broke the same way. It reached the social preview of any
shared `?data=` link and the inline PNG the MCP tool returns.

Two causes, both fixed:

- `og-svg.ts` hard-coded `font-family: Inter` for the token text and ignored
  the family the line actually asks for. Even with the right font loaded, the
  card requested Inter.
- No font for the script was available at all. Google's CSS API returns
  TrueType (the only thing resvg accepts) for an old-style User-Agent, and
  `text=` subsets it to the characters we are about to draw. A whole CJK
  sentence comes back around 8 KB, so this costs nothing next to bundling
  fonts or installing them in the image. Cached in-process by family and
  text: measured 243ms cold, 19ms warm.

A line we cannot cover — an uploaded custom font, an unknown family, no
network — degrades to a neutral "2 lines · 1 link" summary rather than
boxes. That is per line, so a Japanese/English pair keeps its English.
Coverage is decided by asking the bundled Inter for a glyph via opentype.js
(already a dependency) rather than guessing at script ranges, and a font
fetch failure is caught so it cannot turn the card into a 500.

`/api/og` and the MCP preview now share `renderOgPng`, which is where all of
the above lives; they had duplicated the resvg call and would have drifted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dani-polani
dani-polani merged commit 15281d1 into main Aug 8, 2026
2 checks passed
@dani-polani
dani-polani deleted the fix/og-fonts-non-latin branch August 8, 2026 23:50
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.

1 participant