Developer typing alt text descriptions on a laptop screen

Open Graph images for Eleventy without paying the build-time tax

house Jul 16, 2026

Share a link with no og:image and you get the dead gray card. On X, in Slack, in Discord, your post sits next to ones with real preview images and loses. So you decide to generate one per post. Reasonable. Then you look at how Eleventy sites usually do it.

The two standard approaches, and what they cost

Approach one: render at build time with Satori (this is what most eleventy-plugin-og-* packages do). Satori is fast and doesn't need a browser, but it renders a subset of CSS, not real CSS. Flexbox mostly works; plenty of other things don't. You also now own a font pipeline: font files checked into the repo or downloaded in CI, emoji handled separately.

Approach two: render at build time with headless Chrome. Real CSS now, but you've put Chromium inside your build. That means installing it in CI on every run, roughly a hundred milliseconds to a few seconds per page, and a new way for deploys to fail that has nothing to do with your content. A 500-post site renders 500 images every full rebuild, including the 490 posts nobody will share this year.

Both approaches share an assumption I stopped believing: that the image has to exist when the build finishes.

The pattern: sign now, render on first share

An og:image has one consumer: social crawlers. Nothing fetches that URL until the page is shared. So the image doesn't need to exist at build time. Only the URL does.

That suggests a different shape:

  1. At build time, construct an image URL from the post's data (title, author, tag) and sign it with an HMAC so nobody else can render on your account. One hash, microseconds, no network call.
  2. Put that URL in the meta tag. It points at nothing that exists yet.
  3. The first time a crawler fetches it, a rendering service draws the image with real Chrome, caches it permanently, and serves it. Every later fetch is a cache hit.

Build cost per page: one HMAC. Pages nobody shares never render at all. And if the rendering service is down during your deploy, nothing happens, because your build never talks to it.

Wiring it up

Disclosure before the code: I built the service this plugin talks to (Shotpipe), so read this section as the author showing their own tool, not a neutral survey. The pattern above stands regardless of whose renderer you use.

npm i eleventy-plugin-shotpipe
// eleventy.config.js
module.exports = (eleventyConfig) => {
  eleventyConfig.addPlugin(require('eleventy-plugin-shotpipe'), {
    key: process.env.SHOTPIPE_KEY,
    secret: process.env.SHOTPIPE_SECRET,
    template: 'terminal',
    accent: '#5ca9ff',
  })
}
{# base layout #}
<meta property="og:image" content="https://shotpipe.io/og?title=Open+Graph+images+for+Eleventy+without+paying+the+build-time+tax&key=k_44c2ef414a82&template=terminal&author=Your+Name&tag=blog&accent=%2339A5E7&logo=3d99f5724fc8736da25e08ed62c1fccf74d653875f11f1177f3965f2ce113b4f&sig=d5c196694542a4942271d8df591b13027e48278e11061b3da64bd8b76d763611">

What ships in the built HTML is just a long URL:

<meta property="og:image"
      content="https://shotpipe.io/og?template=terminal&title=Hello&key=k_you&sig=8f2c...">

The templates are hosted server-side and parameterized by query string, so there's no template HTML to build, host, or version yourself. You pass a title and pick an accent color. A logo can be uploaded once and referenced by content hash.

I timed the difference on this blog: adding the plugin changed the build time by nothing measurable. That's not an optimization claim, it's just what "one HMAC per page" means in practice.

What actually happens on first share

Paste your post into Slack. Slack's crawler requests the signed URL. The service verifies the signature, renders the template in headless Chrome, stores the PNG, and responds. That first fetch takes a couple of seconds; the crawler waits, it's what crawlers do. Every fetch after that, from any platform, is served from a CDN cache in milliseconds, forever. The render happens exactly once per unique title.

Tradeoffs, honestly

  • It's a hosted service. Your images render on someone else's infrastructure and you need an API key (there's a free tier, no card). If you want zero external dependencies, this is not your pattern; use Satori and accept the CSS subset.
  • You use their templates. Four of them, parameterized. If your brand needs a fully bespoke design, a build-time approach with your own HTML gives you more control.
  • The URL is public. The signature means nobody can render arbitrary images on your key, but anyone can re-fetch an image you've already signed. For OG images that's fine; they're public by definition.

If you'd rather stay self-contained

Fair choice. eleventy-plugin-og-image renders with Satori at build time and is well maintained. @vercel/og does the same rendering trick at the edge if you're on Vercel. The build-time tax is real but modest on small sites; it mostly bites on large sites and slow CI.

But if you've ever watched a deploy fail because Chromium didn't download, or waited on 400 images that nobody asked for yet, it's worth questioning the assumption underneath: the image doesn't have to exist until someone looks.