Nicholas Clooney

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.

Example social card generated by the Subspace Builder

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:

  1. Shared excerpt helper. The site already had an Eleventy excerpt filter. To stay DRY, we extracted it into lib/excerpt.js so both Eleventy and the generator use the same logic.
  2. 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.
  3. 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.
  4. Caching. Each post’s title + excerpt + template version hashes into a manifest. If nothing changed, we skip regeneration. There’s also a --force flag (or OG_FORCE=true) to nuke the cache when we tweak the design.
  5. 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 Eleventy page.url plus the site base.
  • og:type flips to article 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.