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 %}">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 build | one HMAC (microseconds) |
| CI requirements | Chromium install, or font files for Satori | none — node:crypto only |
| CSS support | Satori: subset, no real CSS; Puppeteer: full | full (real Chrome, server-side) |
| Fonts & emoji | your problem | maintained server-side |
| Pages nobody shares | rendered and paid for anyway | never rendered at all |
| Renderer down during deploy | build fails | build unaffected — URLs sign offline |
| Dependencies | Satori/sharp/Puppeteer trees | zero |
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.
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.
Hosted server-side, parameterized by query string. These previews are live renders from the API:
terminal |
gradient |
minimal |
default |
Every template takes title, author, tag, an accent color, and an optional logo.
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.pngThe 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.
- At build time the plugin HMAC-signs the template parameters into a URL. No network call.
- The URL sits in your
<meta og:image>tag doing nothing. - The first time a crawler (Twitter, Slack, Discord…) fetches it, Shotpipe renders the template and caches the PNG.
- 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.
- Shotpipe — the API behind the plugin (docs, pricing)
- Get a free API key — 100 renders/month, no card, cached hits never count