Your Next.js OG Image Works Locally and Dies on Vercel. Here's Why.
August 8, 2026 · 8 мин чтения
Our build had been green for a week. Then one deploy stopped at 223 of 235 pages with a message that made no sense: ETIMEDOUT while prerendering /en/tools/ai/opengraph-image. No code had changed in that route. Running npm run build on the laptop finished in ninety seconds, every page generated, zero errors.
That gap between local and Vercel is the whole story. It took us longer than it should have to find, so here it is written down.
The short answer: an emoji
next/og renders your JSX with Satori. Satori draws text with real font files, and emoji are not in those font files. When it hits one, it goes and downloads the graphic from a CDN at render time. That request happens during your build.
So the build machine needs working outbound network to a font CDN, at the exact moment it prerenders each card. Your laptop always has that. A CI runner sometimes does not. Ours had it most days, which is the worst possible failure mode, because the build passes often enough that you blame something else.
We had a small emoji badge in the corner of each tool card. Cute. It cost us three failed deploys. The replacement was the first letter of the category rendered in a rounded square, which takes the same space, needs no network, and honestly looks more deliberate.
How to confirm it's the emoji and not something else
- Read the failing route name in the build log. If it ends in
opengraph-imageortwitter-image, you are in Satori, not in your page code. - Search that route's JSX for anything outside the basic Latin range. Emoji, arrows like ➜, checkmarks, box-drawing characters. All of them trigger the same lookup.
- Grep the whole OG folder, not just the failing file. Shared components get pulled into every card.
- If the log says ETIMEDOUT, ECONNRESET or fetch failed, it is a network problem, not a layout problem. Layout mistakes throw different errors.
A regex that catches most of it: grep -rP "[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}]" src/components/og src/app. If that prints anything, you found your build failure.
The other four things that break OG images
Once the build passes, the image can still fail to appear. These are the ones we hit, roughly in order of how often they bite.
Divs with two children and no display: flex. Satori implements a subset of CSS and it is strict about this one. A <div> holding more than one child must set display: "flex" explicitly. Browsers forgive you. Satori throws. Every layout wrapper in our OG components sets it, and there is a comment above them explaining why, because we forgot twice.
No metadataBase. Open Graph tags need absolute URLs. If you never set metadataBase in your root metadata, Next falls back to localhost, and every scraper gets a URL it cannot reach. The build prints a warning about it, buried in a wall of other output, and it is easy to scroll past.
One image for the entire site. This is the quiet one. A single opengraph-image.tsx at the root applies everywhere below it, so every article and every tool page shares one picture. Nothing errors. Nothing warns. Your links just all look identical when shared, which defeats the point of having the image at all. Drop an opengraph-image.tsx next to each dynamic route and give it generateStaticParams.
Aggressive caching on the other side. Facebook, LinkedIn and Slack cache what they scraped the first time, sometimes for weeks. You fix the image, reshare the link, and see the old one. That is not your bug. Use each platform's own debugger to force a re-scrape, and do it after the deploy is live rather than before.
What a card should actually contain
1200×630 is still the size to build for. It fits the 1.91:1 box that most platforms crop to, and it downscales cleanly on phones.
- The page title, large, and truncated rather than wrapped into five lines. We cut at 82 characters and add an ellipsis.
- One supporting line: a tagline, a reading time, a category. Not a paragraph.
- Your brand somewhere small and consistent, so a reader who sees three of your links in a feed recognises the third one.
- High contrast. Half these images get viewed at 300px wide inside a chat app.
- No text within about 40px of the edge, because different platforms crop slightly differently.
Resist the pull toward putting the whole meta description on the card. At feed size it is unreadable, and it makes the title compete with body text for attention.
Check the tags before you check the image
Half the time the image is fine and the tags are wrong: a missing og:image, a twitter:card that was never set, a description that got truncated at 160 characters by something upstream. You can paste a URL into our Meta Tag Generator to see what a scraper reads back, and preview how the card renders before you ship it.
While you are in there, the Favicon Checker covers the neighbouring problem. Icons and OG images break for the same reason, which is that both live in the head, neither one is visible on the page, and nothing tells you they are wrong until someone shares your link.
The part worth remembering
Build-time image generation feels like a static step. It is not. It renders text, it may reach for the network, and it runs in an environment that is less forgiving than your machine. Keep the cards to plain text and shapes, set metadataBase once, give each route its own image, and the whole category of problem disappears.
We went back through every OG component after this and removed a total of six emoji. The builds have been green since.