We found this bug the boring way: generating real og:image assets for our own marketing pages. Every short title rendered fine. Every title long enough to wrap to a second line came back with both lines painted in the same place — a smeared, double-exposed mess sitting on top of each other. Not clipped, not truncated. Overlapping.
If you're rendering OG images with Satori — directly, via @vercel/og, or via a Cloudflare-flavored wrapper like workers-og — and you've had this dependency pinned for a while, you may be shipping this right now without knowing it, because short titles hide it completely.
Why it happens
Satori's job is to take a flexbox-like layout tree and lay out text runs inside it, including wrapping a long string across multiple lines when it doesn't fit the available width. Line-wrapping in a from-scratch layout engine is genuinely hard: it has to measure glyph widths, decide break points, and — critically — advance the vertical cursor for each wrapped line so line 2 starts below line 1 instead of on top of it.
Early Satori versions had bugs in that vertical-advance step for wrapped text inside flex containers. The container's height was computed correctly, but each line's y offset wasn't, so every line rendered starting at the same baseline. Single-line text — which never triggers the wrap path — was completely unaffected, which is exactly why this kind of bug survives in production for so long: it only shows up on realistic content, not on the two-word placeholder title you tested with.
The fix was one dependency bump
We had workers-og pinned to 0.0.14, which bundles a Satori build from around mid-2023. Bumping to 0.0.27 (bundling Satori 0.15.2) fixed the overlap immediately — no code changes, no layout tweaks, nothing to work around. The whole diff was a version bump in package.json and a regenerated lockfile.
The lesson isn't "upgrade your dependencies," which is too generic to act on. It's narrower: if your OG image renderer is built on Satori and you haven't touched the version in months, go generate an image with a genuinely long, realistic title today — not the short placeholder you built the template with. If you see stacked text, this is almost certainly why.
The harder problem: making sure it doesn't come back
A visual bug like this doesn't show up in a normal unit test — the render succeeds, returns a valid PNG, and every function-level assertion passes. You need to look at the pixels. Here's the technique we ended up with, adapted from our regression suite for SnapOG:
- Render the template with a title long enough to force a wrap, decode the output PNG.
- Pick a narrow horizontal band (x-range) that contains title text but excludes chrome — accent bars, dividers, watermark — that would otherwise pollute the signal.
- For every row in that x-range, check whether any pixel differs from the background color by more than a threshold. That gives you a boolean "this row has ink" for every row of the image.
- Group consecutive ink rows into bands, allowing a 1-row gap for anti-aliasing.
Two cleanly wrapped lines produce two distinct bands with real vertical space between them. The overlap bug collapses them into one band — or one band with no gap — because both lines are painted in the same rows. The assertion is just "count the bands, and check the gap between them," which is far more robust than pixel-exact snapshot comparison (which breaks on every font hinting or anti-aliasing change across environments).
function groupRowsIntoBands(rowHasInk: boolean[]): InkBand[] {
const bands: InkBand[] = [];
let current: InkBand | null = null;
let gapRun = 0;
for (let row = 0; row < rowHasInk.length; row++) {
if (rowHasInk[row]) {
current = current ?? { startRow: row, endRow: row };
current.endRow = row;
gapRun = 0;
} else if (current !== null) {
gapRun++;
if (gapRun > 1) {
bands.push(current);
current = null;
}
}
}
if (current !== null) bands.push(current);
return bands;
}
This isn't SnapOG-specific — the technique works for any image-rendering pipeline where "did two things visually overlap" is the property under test and pixel-perfect snapshots are too brittle to maintain. Decode real pixels, reduce to a 1D signal along the axis you care about, assert on the shape of that signal instead of the raw bytes.
Where we ended up
Every OG template in SnapOG — including the two-line title/description layouts — now has a pixel-content regression test using this method, so a future Satori upgrade (or a template change) that reintroduces overlapping text fails CI instead of shipping silently to production. If you're maintaining your own Satori-based renderer, the harder-won lesson here transfers directly even if you never touch our code: test the actual rendered pixels of a realistic, multi-line input, not just that the function returns a 200.
Curious what the templates and tests look like end to end?