N-012
Dynamic OG Images on Next.js and Cloudflare Workers
12 Oct 2026 · nextjs, cloudflare
This note as an agent skill: og-images in phucbm/skills.
Every page on this site, perxel.com and OpenRace has its own share card: a 1200×630 PNG drawn from the page's data with next/og. All three run on Cloudflare Workers through OpenNext, which has no filesystem, so the usual examples that read a font with fs break there. This note keeps only what surprised me; the step by step version is an agent skill.
The cards

This site: a note's card, drawn at build.

perxel.com: an insight's card, with its category and the tech it covers.

OpenRace: a race's card, drawn on request from live data.
What broke
- A
force-staticroute still can't read from disk. OpenNext can run a "static" route module again on a cache miss, so areadFilethat works innext buildthrows in production. Perxel embeds its fonts and logos as base64 in.tsfiles. This site reads its fonts withfsonly because a script copies every card into the static assets after the build, so the Worker never draws one. - Satori can't read the fonts every site ships. No WOFF2, no variable fonts: the card needs one static TTF or WOFF per weight. Google Fonts sends WOFF if you ask with an old Safari user agent; for my own files I cut instances with fonttools.
- WebP crashes Satori. A hard error, not a missing image. Perxel keeps PNG copies of its tech logos just for the cards.
- A file-based
opengraph-imageoverridesgenerateMetadata. So an entry could never have its own hand-made share image. Perxel moved its detail cards to a plain[slug]/og/route.tsx, and the page picks between the entry's image and the card. - A page's
openGraphreplaces the layout's whole object. Not field by field. Perxel's pages had lostog:typeandog:site_namebefore every page started spreading a shared base.
Three ways to serve them
| phucbm.com | perxel.com | OpenRace | |
|---|---|---|---|
| Route | one app/og/[...path]/route.tsx | one opengraph-image per page, plain routes for details | one opengraph-image per page |
| Drawn | at build | at build | on request, with its page's revalidate |
| Served from | static assets, as plain files | OpenNext's cache | OpenNext's cache |
| Fonts | TTF files, read at build only | base64 in .ts | base64 in .ts |
This site and Perxel draw every card at build, because all their content is in the repo. OpenRace's races, series and organizers change all the time, so each card revalidates with its page and falls back to the page's own title when the fetch fails: a card route must always return an image.