How it works
- Ghost fires a
post.publishedwebhook. - A tiny handler (Cloudflare Worker, Lambda, or any small server) renders the card via the ogsmith API using your API key.
- It uploads the PNG through the Ghost Admin API and sets it as the post's social image.
The handler
// ghost-og-hook.mjs — deploy anywhere that runs Node ≥ 20
import GhostAdminAPI from "@tryghost/admin-api";
const api = new GhostAdminAPI({
url: process.env.GHOST_URL,
key: process.env.GHOST_ADMIN_KEY,
version: "v5.0",
});
export async function handlePostPublished(webhookBody) {
const post = webhookBody.post.current;
const og = "https://api.ogsmith.dev/v1/og?" + new URLSearchParams({
title: post.title,
description: post.custom_excerpt ?? "",
site: new URL(process.env.GHOST_URL).host,
template: "article",
key: process.env.OGSMITH_API_KEY, // server-side only — never in theme HTML
});
const png = await fetch(og);
if (!png.ok) throw new Error("ogsmith render failed: " + png.status);
const upload = await api.images.upload({
file: Buffer.from(await png.arrayBuffer()),
purpose: "image",
ref: post.slug + "-og.png",
});
await api.posts.edit({
id: post.id,
updated_at: post.updated_at,
og_image: upload.url,
twitter_image: upload.url,
});
}
Wire up the webhook
In Ghost Admin → Settings → Integrations → Add custom integration: copy the Admin API key, then add a webhook for the Post published event pointing at your handler's URL.
From then on every published post gets a designed social card automatically — and because Ghost stores the file, your site keeps working even if you cancel ogsmith.
Frequently asked questions
Why not put the image URL directly in the theme?
Ghost's Handlebars themes can't compute the HMAC signature that makes public URLs safe, and embedding your raw API key in HTML would let anyone burn your quota. Publish-time generation sidesteps both problems and is actually more robust — the PNG lives in your Ghost media storage.
What about posts published before I set this up?
Run the same function over the Admin API's post list once (api.posts.browse with a loop). 250 free images a month covers a decent backlog; the Hobby plan ($9/mo) covers 10,000.
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.