You never think about OG images until you share a link and see that sad gray rectangle where a preview should be. For a blogging platform, that first visual impression matters — it's the difference between someone clicking your link on Twitter or scrolling past it.
We knew early on that Postlark needed auto-generated OG images. Asking bloggers to create a 1200×630 pixel card for every single post is unrealistic, especially when half our users publish daily through automation. So we built a system that generates them on the fly.
The Problem with Static OG Images
Most blog platforms handle this one of two ways: they don't (you get the platform's default logo), or they force you to upload one. Both options fall apart if you're running a blog that pushes out content frequently.
We wanted something that worked without any input from the blogger but still looked intentional. Each preview card should reflect the actual post — its title, tags, and the blog it belongs to. Not a generic brand card that tells you nothing about what you're about to read.
Satori and the display:flex Surprise
The core of our OG worker uses Satori, the same engine Vercel built for their OG image generation. Satori takes HTML-like markup and converts it to SVG, which then gets rasterized to PNG. We specifically use workers-og, a wrapper that makes Satori play nicely at the edge.
The template itself is straightforward: a gradient background in Postlark's indigo palette, the post title in large type, up to three tag chips at the bottom, and the blog name in the footer. Clean, readable, branded. Nothing fancy.
What wasn't straightforward was discovering that every single <div> in your template needs display:flex. Not just the container. Not just the layout wrappers. Every div, including the ones that just hold a line of text.
Satori silently ignores elements without this property. No error, no warning — just a blank space where your content should be. Our first version rendered white rectangles for about a third of posts, and there was absolutely nothing in the logs to point us toward the cause.
I spent an afternoon staring at blank PNGs before finding a GitHub issue buried deep in Satori's repo that mentioned the flex requirement in passing. The fix was tedious but trivial: go through every element in the template and slap display:flex on it. Tag chips that were <span> elements? Swapped to <div style="display:flex;...">. The title wrapper, the footer label, the "postlark.ai" watermark — all of them. Five minutes to fix once you know the trick, but hours of confusion before that.
If you're building with Satori today, save yourself the headache: treat display:flex as mandatory on every element, not optional.
Korean Text Rendering
Postlark has a significant Korean user base. Korean text rendering on OG images is one of those things that's trivially easy to get wrong. Ship the image generator with Inter or any Latin-only font, and Korean titles render as a row of empty boxes — tofu, as typographers call them. We've seen this happen on other platforms and it always looks broken, because it is.
Our approach loads Noto Sans KR as the primary font, fetched from Google Fonts at generation time. If the fetch fails for any reason, we fall back to Inter. A Latin title with Inter still looks fine, and the failure mode stays graceful rather than catastrophic.
The font file is roughly 1MB for the Korean weight-700 subset. On the first request this adds real latency — noticeable enough that you'd think something is wrong. But we cache aggressively (more on that in a moment), so repeat requests for the same post skip font loading entirely and return in milliseconds.
One design choice worth calling out: the title font size adjusts based on character count. Titles over 40 characters drop from 52px to 40px. Korean titles tend to be shorter in raw character count but wider per glyph, so this heuristic works reasonably well for both languages without us needing separate rendering paths. It's not perfect — a 39-character Korean title can still overflow — but it covers the 95% case, and we'd rather ship a pragmatic solution than chase pixel-perfect layout logic for edge cases.
Four Levels of Fallback
Not every post ends up using the generated image. We built a resolution chain with four levels:
User-specified image — Provide an
og_imagevia the API, MCP server, or CLI, and that wins. Full control when you want it.First image from post body — If the post contains an embedded image, we extract it from the rendered HTML automatically. A photo-heavy post probably has a better visual than any generated text card.
Blog cover image — Upload a default cover through the dashboard. This acts as a branded fallback for blogs that prefer visual consistency across all their posts.
Dynamic OG worker — The text-based generated image. The safety net that guarantees every post has a meaningful preview, no matter what.
The chain evaluates top to bottom and stops at the first match. In practice, most automated posts land on level 4, while human-written posts with photos tend to get caught at level 2. Bloggers who care deeply about their social cards use level 1. The cover image at level 3 sits there for everyone else — a set-it-and-forget-it default that beats a generic placeholder.
Cache Strategy
Generated images get stored in object storage after their first render. Every subsequent request for the same post hits the stored copy directly — no re-rendering, no font download, no template execution. CDN edges cache them too, with long TTLs.
The cache key is deterministic: blog slug plus post slug. Simple and predictable. There's one known gap here — if someone updates a post title after the image was generated, the cached version shows the old title. We don't bust the cache on title changes yet. The workaround is straightforward: set a custom og_image to override the generated one, or wait for the CDN TTL to expire.
It's the kind of trade-off you make when you're a small team. Perfect cache invalidation for a feature that matters at the margins isn't worth the complexity right now.
What We Skipped
We considered more customization early on: letting bloggers pick gradient colors, upload logo overlays, choose from a font menu. We decided against all of it. The generated cards look good enough out of the box, and the four-level fallback chain gives users escape hatches when they want full control.
OG images are an invisible feature when they work. Nobody tweets "love the OG card on that post." But when the preview is missing or broken, everyone notices — and some people won't click. That's the nature of these infrastructure-level details. You build them, you cache them, you move on to the next thing.