Shipping Social Cards with Satori (and GPT-5 Codex)
I’ve always loved how iMessage or WhatsApp automatically turn a shared link into a little postcard. Until this week, that magic felt like a black box. I suspected there had to be an open standard behind it, but I’d never hooked it up myself. Cue a pairing session with GPT-5 Codex, and about two hours later, the 11ty Subspace Builder can mint its own branded previews for every post.
Here is an example card for this blog post.
Table of Contents
Figuring Out the Spec
The “standards” are mostly Open Graph (Facebook/Meta) and Twitter card meta tags.
Scrapers look for <meta property="og:image">
and friends, and if you supply an
absolute 1200×630-ish image, messaging apps happily show it. Easy in theory; the hard
part is actually generating those images so they look intentional.
I scoped the options:
- Render HTML in a headless browser (Puppeteer/Playwright) and take screenshots.
- Call a third-party API like Cloudinary or Bannerbear.
- Draw rectangles with Canvas/Sharp.
- Use Satori + Resvg to turn a React-ish component into a PNG.
Static generation during the build made the most sense for a small blog. No runtime costs, predictable branding, and we already rebuild any time content changes. GPT-5 Codex nudged me toward Satori + Resvg: pure Node, fast startup, and expressive enough for the layout I wanted.
Designing the Pipeline
We planned out a few pieces before typing anything:
- Shared excerpt helper. The site already had an Eleventy
excerpt
filter. To stay DRY, we extracted it intolib/excerpt.js
so both Eleventy and the generator use the same logic. - Generator script. A new
scripts/generate-og-images.js
reads every Markdown post, renders the markdown with Markdown-It, pulls out the title and excerpt, and feeds that into a Satori HTML template. Resvg converts the SVG into a crisp PNG. - Fonts and branding. We picked Lexend for headings and Inter for body text via
@fontsource
. The template applies the Sun theme palette—warm yellow gradient with amber accents—to keep the cards on-brand. - Caching. Each post’s title + excerpt + template version hashes into a
manifest. If nothing changed, we skip regeneration. There’s also a
--force
flag (orOG_FORCE=true
) to nuke the cache when we tweak the design. - Where to store output. PNGs land in
assets/og/
, and a manifest JSON lives under_data/ogImages.json
. Eleventy treats that as global data we can reference later.
Adaptive Typography with Satori
The first draft looked great until a long title shoved everything off the canvas. Satori doesn’t auto-scale text, so we added a small fitting helper: several font-size tiers, each with a max character threshold, and a final truncation if the title still won’t cooperate. Excerpts follow the same pattern. It’s not AI, just a tidy switch statement, but it keeps the layout neat.
We also sanitize whitespace-only text nodes before handing markup to Satori—
otherwise it counts blank nodes as extra children and throws “Expected <div>
to have
display:flex” errors.
Hooking into Eleventy
With images in place, we needed Eleventy to know about them. A tiny posts/ posts.11tydata.js
computed value looks up the relevant entry in _data/ogImages.json
and injects ogImage
into the page data. Any post can override it manually, but the
generated path is there by default.
We also wired the generator into the build pipeline: eleventy.config.js
runs
generateOgImages()
inside the eleventy.before
event. On Cloudflare Pages (our CI),
setting OG_FORCE=true
guarantees a full refresh.
For better dev ergonomics we added:
eleventyConfig.addWatchTarget("assets/og/")
and.cache/og/
so changes to the PNGs kick Eleventy’s watcher.- A dedicated npm script (
npm run og
) if I want to regenerate cards without running the full site build. - A friendly
README
section explaining the workflow, including the Satori vs. Puppeteer decision.
Updating the Head Tags
It turns out you need more than just images. The base Nunjucks layout now assembles a
proper <head>
:
- Titles combine the post title with the site title (“Post No. 1! · 11ty Subspace Builder”).
- Descriptions prefer front matter, then the excerpt, then the site description.
- Canonical URLs and
og:url
/twitter:url
derive from the Eleventypage.url
plus the site base. og:type
flips toarticle
for anything living under/posts/
.- Twitter cards upgrade to
summary_large_image
whenever a PNG exists.
The result is consistent metadata across the site, even for pages that don’t have generated images.
Working with GPT-5 Codex
I wouldn’t have shipped this so quickly without Codex. I basically dumped my hunches
—“messaging apps must use OG tags”—and Codex methodically walked through the
architecture, wrote the first version of the script, and even caught the Satori
whitespace edge case before I fully understood the error message. Every time I asked
“why don’t I see og:image
in the output?”, we traced it together until it clicked.
Having an AI pair who knows the 11ty ecosystem and Node nuance sped everything up
immeasurably.
What’s Next
Now that social cards are in place, I want to explore:
- Pulling theme colors dynamically from
_data/themes.yaml
so different posts could showcase different palettes. - Generating previews for the home page and tag archives.
- Surfacing the OG image in the CMS authoring experience so I can preview cards before publishing.
If you’re hacking on 11ty and want branded social previews without a heavy runtime, the Satori + Resvg combo is a great place to start—especially if you have GPT-5 Codex in your corner.
Let me know when you’re ready to slot it into the repo, and I’ll help with placement or front matter tweaks.