eleventy-plugin-shotpipe

July 15, 2026 · View on GitHub

Open Graph images for Eleventy — with zero build-time cost.

An og:image plugin for 11ty that signs image URLs locally at build time (one HMAC, microseconds per page) and renders the actual image lazily on the first social-crawler hit, via Shotpipe. No headless Chrome in your build. Nothing new in CI. A thousand-page site adds nothing measurable to the build.

<meta property="og:image" content="{% ogImageUrl title, author, tag %}">

Why not render at build time?

Most Eleventy og-image plugins run Satori or headless Chrome inside your build. That works — until it's the slowest thing in your deploy.

Build-time plugins (Satori / Puppeteer)eleventy-plugin-shotpipe
Build cost per page~100ms–seconds, every page, every buildone HMAC (microseconds)
CI requirementsChromium install, or font files for Satorinone — node:crypto only
CSS supportSatori: subset, no real CSS; Puppeteer: fullfull (real Chrome, server-side)
Fonts & emojiyour problemmaintained server-side
Pages nobody sharesrendered and paid for anywaynever rendered at all
Renderer down during deploybuild failsbuild unaffected — URLs sign offline
DependenciesSatori/sharp/Puppeteer treeszero

The trade: images render on Shotpipe's servers, so you need an API key (free tier, no card). URLs are HMAC-signed, so nobody can render on your key.

Quick start

npm i eleventy-plugin-shotpipe
// .eleventy.js
module.exports = (eleventyConfig) => {
  eleventyConfig.addPlugin(require('eleventy-plugin-shotpipe'), {
    key: process.env.SHOTPIPE_KEY,
    secret: process.env.SHOTPIPE_SECRET,
  })
}
{# in your base layout #}
<meta property="og:image" content="{% ogImageUrl title, 'Your Name', 'blog' %}">

That's it. Every page gets a social card; none of them render until someone shares the page.

Templates

Hosted server-side, parameterized by query string. These previews are live renders from the API:

terminal template terminalgradient template gradient
minimal template minimaldefault template default

Every template takes title, author, tag, an accent color, and an optional logo.

Options

eleventyConfig.addPlugin(require('eleventy-plugin-shotpipe'), {
  key: process.env.SHOTPIPE_KEY,       // required — get one at shotpipe.io/signup
  secret: process.env.SHOTPIPE_SECRET, // required
  template: 'terminal',                // default template for all pages
  accent: '#0ea5a4',                   // brand color, recolors every template
  logo: './src/assets/logo.png',       // see "Your logo" below
  baseUrl: 'https://shotpipe.io',      // only for self-hosted instances
})

Any option can be overridden per page by passing an object to the shortcode or to signedOgUrl:

{% ogImageUrl title %}
{% ogImageUrl title, author, tag %}
// in a JS shortcode / computed data — full control
const { signedOgUrl } = require('eleventy-plugin-shotpipe')
signedOgUrl({ title, author, tag, template: 'terminal', accent: '#ff6600', w: 1200, h: 630 }, opts)

Point the plugin at a local PNG/JPG (see options above), then upload it once — and again only when it changes:

SHOTPIPE_KEY=... SHOTPIPE_SECRET=... npx shotpipe-upload-logo ./src/assets/logo.png

The plugin references the logo by its content hash, computed locally — builds stay network-free — and the server embeds it at render time (never fetched). The logo is per-site, so one key can back many sites, each with its own.

How it works

  1. At build time the plugin HMAC-signs the template parameters into a URL. No network call.
  2. The URL sits in your <meta og:image> tag doing nothing.
  3. The first time a crawler (Twitter, Slack, Discord…) fetches it, Shotpipe renders the template and caches the PNG.
  4. Every fetch after that is a cache hit served from the edge.

Unsigned or tampered URLs are rejected, so nobody can render on your key. If the API is unreachable during your build, nothing happens — the build never talks to it.

License

MIT