DEV Community

Cover image for Open Graph images for Eleventy without paying the build-time tax
Stevie g
Stevie g

Posted on • Originally published at shotpipe.io

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

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
Enter fullscreen mode Exit fullscreen mode
// 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',
  })
}
Enter fullscreen mode Exit fullscreen mode
{# base layout #}
<meta property="og:image" content="{% ogImageUrl title, 'Your Name', 'blog' %}">
Enter fullscreen mode Exit fullscreen mode

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...">
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)