Render non-Latin scripts in OG cards instead of "NO GLYPH" boxes - #20
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/ogdrew 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
og-svg.tshard-codedfont-family: Interfor the token text and ignored the family the line actually asks for. Even with the correct font loaded, the card requested Inter.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: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 linksummary 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/ogand the MCP preview had duplicated the resvg call. They now sharerenderOgPng, 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