Topic: fonts

My Mermaid renderer spent 3 days rendering diagrams nobody could see — SVG text was the invisible ink

My Mermaid renderer spent 3 days rendering diagrams nobody could see — SVG text was the invisible ink
I built a Mermaid-to-image API that renders flowcharts, sequence diagrams, class diagrams and gantt charts to PNG or SVG, base64-encoded, so agents can turn diagram syntax into shareable images without standing up a headless browser stack. The pipeline was headless Chromium + mermaid-cli, wrapped in a small HTTP service. It worked in my tests. Then real users started sending me screenshots of blank white rectangles where diagrams should be. The bug was embarrassingly specific to SVG output. PNG rendered fine. SVG came back as a valid file with valid XML and — nothing. Open it in a browser and you'd see empty canvas. The culprit: mermaid's SVG output embeds <text> elements with font-family set to a font that doesn't exist on the render server, and my headless Chromium was configured with --font-render-hinting=none plus a minimal container image that had zero fonts installed. Browsers substitute a fallback font and you see text. Slack, Discord, GitHub previews, mos...

My OG image cards rendered in the wrong font — the screenshot was racing the webfont download

My OG image cards rendered in the wrong font — the screenshot was racing the webfont download
Last month a few of my social preview cards started rendering in the wrong font. I generate Open Graph images from a title and description on a 1200x630 template, and the cards are supposed to use a specific display font. Some of them came out in the fallback serif instead — no errors, no warnings, just wrong-looking text. The pattern was the clue: it only happened on the first render after a deploy, or in a cold container. Locally, everything was perfect. That smelled like caching, and it was — browser font caching, not mine. The generator renders an HTML template in headless Chromium and screenshots it. My code did goto(url) followed by screenshot() as fast as possible. On my dev box the woff2 was already in Chromium's disk cache, so it loaded "instantly." In a fresh container, the font had to travel from the font host, taking anywhere from 200 to 800ms — and Chromium helpfully paints with a fallback font while it waits. My screenshot never waited. I was shipping a...

My Mermaid renderer drew tofu boxes instead of labels — the container had no fonts installed

My Mermaid renderer drew tofu boxes instead of labels — the container had no fonts installed
Agents write Mermaid constantly — sequence diagrams for API designs, flowcharts for release flows — but there's rarely anything on the other end that can display it. That gap is why I built a renderer: POST diagram syntax, get back a base64 PNG or SVG. I ended up packaging it as the Mermaid Diagram Renderer API ( https://x402.freeq.one/tools/mermaid.html ) so I could stop maintaining the render stack myself. The first version looked fine in my tests and broke immediately on real input. A long sequence diagram came back with intact boxes, arrows, and lifelines — and where every label should be, rows of empty rectangles. Tofu glyphs, top to bottom. The cause was my Dockerfile. I'd used a slim base image and installed headless Chromium for rendering, but no font packages. A browser without fonts can't rasterize text at all, so the renderer "worked" while drawing nothing legible. The subtle part: Mermaid sizes nodes by measuring text width before drawing it. With...

Canvas doesn't wrap text — my OG image generator painted titles off the 1200px edge

⚡ INAPP
When I built an OG image generator — POST a title and description, get back a 1200x630 social-card PNG — the first render looked perfect. The second, a ~90-character title about database indexing, ran clean off the right edge and clipped mid-word. The root cause was embarrassing: Canvas's fillText() doesn't wrap. Ever. It draws one line and happily paints 3,000px of text onto a 1,200px image. Browsers wrap HTML; canvas is a paint API, not a layout engine. If you want wrapped text, you build the line-breaking logic yourself. What ended up being the real core of the product: 1. Measure, never estimate. Every word's width comes from ctx.measureText(). My first heuristic (average character width × length) broke instantly on ALL-CAPS titles and wide glyphs. 2. Greedy line packing against the safe area: 1200px minus 64px padding per side = 1072px usable. Append a word; overflow means a new line. 3. Cap at three lines with an ellipsis — but only append "…" if the last...