1. Get a key
Grab a free key from the homepage and export the credentials in your build environment:
OGSMITH_KEY_ID=ab12cd34
OGSMITH_SIGNING_SECRET=…
2. Add a shortcode
// eleventy.config.js
import { createHmac } from "node:crypto";
const enc = (s) =>
encodeURIComponent(s).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
export default function (eleventyConfig) {
eleventyConfig.addShortcode("ogImage", (title, description = "") => {
const p = {
title, description,
site: "yoursite.dev",
template: "article",
kid: process.env.OGSMITH_KEY_ID,
};
const canonical = Object.keys(p).sort().map((k) => enc(k) + "=" + enc(p[k])).join("&");
const sig = createHmac("sha256", process.env.OGSMITH_SIGNING_SECRET)
.update(canonical).digest("hex");
return "https://api.ogsmith.dev/v1/og?" + canonical + "&sig=" + sig;
});
}
3. Use it in your base layout
{# _includes/base.njk #}
<meta property="og:image" content="{% ogImage title, description %}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{% ogImage title, description %}">
Builds stay fast because nothing is rendered at build time — the URL is just a string. The PNG renders on first crawl and is CDN-cached forever after.
Frequently asked questions
Won't calling the shortcode twice render the image twice?
No — identical parameters produce an identical URL, which renders once and then serves from CloudFront's edge cache. Cache hits don't count against your quota.
Why signed URLs instead of an API key?
The og:image URL ends up in your public HTML, where anyone can read it. A signed URL exposes no secret: the HMAC signature covers every parameter, so changing the title (or anything else) invalidates it. A leaked signed URL can only ever render that exact image.
Does every page view burn my quota?
No. Images are served through CloudFront with immutable caching, and cache hits don't count against your quota. A given og:image URL renders once; every crawler and preview after that is a free edge hit.
Templates available: basic, gradient, split, article, minimal, code — try them in the playground. Full parameter list in the docs.