You launch a blog. The first ten posts get hand-designed OG images in Figma — beautiful, on-brand, exactly 1200 × 630. Then post eleven needs to ship in twenty minutes and you skip it. Now half your archive shares as a generic gray fallback on Slack and LinkedIn, and your social CTR for those posts is roughly nothing.
The fix is to stop designing OG images and start generating them. Pick a template, define the variables (title, author, tag, accent color), and have your build pipeline or a CDN URL produce the final 1200 × 630 PNG every time you publish. This is a 2026 walkthrough of the four approaches that actually work, when each one wins, and copy-paste code for the two most common stacks.
Every auto-OG setup falls into one of four buckets. Pick by stack and traffic, not by what's trendy.
| Approach | Best for | Setup time | Per-image cost |
|---|---|---|---|
| @vercel/og (Satori) | Next.js, Astro, Remix, Nuxt sites already on Vercel/Cloudflare | ~30 min | $0 (within free edge limits) |
| Cloudinary URL transforms | Anything with a CDN already and no custom rendering needs | ~15 min | $0–$0.005 |
| Bannerbear / Placid API | Brand-heavy designs, non-developer teams, multi-template per post | ~1 hour | $0.01–$0.05 |
| Template + spreadsheet loop | Static blogs, archives, no infra appetite | ~2 hours once | $0 |
If you're on a JavaScript framework that deploys to an edge platform, @vercel/og is almost always the right answer. If you're on WordPress or Ghost, Cloudinary URL transforms are the path of least resistance. If you have a designer on the team, Bannerbear lets them own the template and developers just call an API. If you have neither and just need to backfill an archive once, the template-loop approach is the cheapest hack.
@vercel/og is a small library that renders React/JSX to a PNG via Satori, Vercel's HTML-to-SVG renderer. It runs on the edge runtime, returns an image in 50–200ms, and ships with most modern Next.js starter templates.
The complete setup for a Next.js 15 App Router blog:
// app/og/route.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title')?.slice(0, 100) ?? 'Untitled';
const tag = searchParams.get('tag') ?? 'Blog';
return new ImageResponse(
(
<div style={{
height: '100%', width: '100%', display: 'flex', flexDirection: 'column',
background: 'linear-gradient(135deg, #0a0a0f 0%, #15151f 100%)',
padding: '80px', justifyContent: 'space-between', color: 'white',
fontFamily: 'Inter, sans-serif',
}}>
<div style={{ fontSize: 24, color: '#06b6d4', fontWeight: 600 }}>
{tag.toUpperCase()}
</div>
<div style={{
fontSize: 72, fontWeight: 800, lineHeight: 1.1,
letterSpacing: '-0.02em', maxWidth: '90%',
}}>
{title}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ fontSize: 28, fontWeight: 700 }}>yourblog.com</div>
<div style={{ fontSize: 22, color: '#a0a0b8' }}>@yourbrand</div>
</div>
</div>
),
{ width: 1200, height: 630 }
);
}
Then in your post layout:
<meta property="og:image"
content={`https://yourblog.com/og?title=${encodeURIComponent(post.title)}&tag=${post.category}`} />
That's the entire integration. Every post URL gets a unique OG image, generated on-demand at the edge, cached by Vercel's CDN forever (or until the URL params change). You'll want to add a custom font for production — Satori uses a default font that's serviceable but not beautiful. Loading Inter from Google Fonts adds about 30 lines and another 50ms to the cold start.
twitter:image as a separate tag. X bots sometimes refuse to follow query-stringed URLs. Set both og:image and twitter:image to the same fully-qualified URL.If you're on a CMS that doesn't run JavaScript on the edge, Cloudinary's URL-based image transformations let you compose an OG image from a base template plus text overlays, all encoded in the URL. No build step, no API call, no library.
Upload a base template once (a 1200 × 630 PNG with your brand bar, logo, and an empty headline area). Then any URL like:
https://res.cloudinary.com/yourcloud/image/upload/
w_1100,c_fit,co_white,g_west,x_50,y_-50,
l_text:Inter_72_bold:Why%20Static%20Sites%20Win%20in%202026/
v1/og-template.png
...renders the template with that headline burned in. Cloudinary caches at the URL level, so every post becomes effectively static after first render.
The pattern in a WordPress functions.php:
function dynamic_og_image() {
if (!is_single()) return;
$title = rawurlencode(get_the_title());
$url = "https://res.cloudinary.com/yourcloud/image/upload/" .
"w_1100,c_fit,co_white,g_west,x_50,y_-50," .
"l_text:Inter_72_bold:{$title}/v1/og-template.png";
echo "<meta property='og:image' content='{$url}' />";
}
add_action('wp_head', 'dynamic_og_image');
Ten minutes of work, every post gets a unique OG image, costs land in Cloudinary's free tier for any blog under ~25k monthly shares.
For brands that want pixel-perfect, on-brand OG images that change layout per content type (interview cards look different from how-to cards look different from changelog cards), an API service like Bannerbear or Placid lets your designer build templates in a visual editor and your developer hits a JSON endpoint:
POST https://api.bannerbear.com/v2/images
{
"template": "abc123",
"modifications": [
{ "name": "headline", "text": post.title },
{ "name": "author", "text": post.author },
{ "name": "category", "text": post.category, "color": colorMap[post.category] }
]
}
The response includes a CDN URL you store in the post's frontmatter. Generation runs ~1–3 seconds on first request and is cached after. Cost is $0.01–$0.05 per image at typical plans, which is irrelevant for blogs but adds up if you're generating thousands per day.
Worth it when you have a designer who wants ownership of the OG visual identity without learning React. Overkill if you're a one-person blog.
Got 80 old posts that ship without OG images? Don't engineer a system. Open a spreadsheet, list every post slug + title, paste those into a free OG image template once each, download the PNGs, and upload them to your media folder. Then update each post's OG meta tag.
This is the right move when:
For volumes above ~150 posts, the spreadsheet approach starts to chafe and one of the first three approaches will pay back its setup cost within an afternoon.
Eight pre-tested OG templates at exactly 1200 × 630, safe-zone padding built in, every color palette tested for contrast on Slack white and LinkedIn blue. Free, no signup, downloads as PNG. Use it as your base template for any of the approaches above.
Open the OG Image Generator →On a JS framework already deploying to Vercel or Cloudflare? Use @vercel/og — it'll be working before lunch and will scale forever for free. On WordPress, Ghost, or any CMS? Use Cloudinary URL transforms — same idea, no infra. Have a designer who needs to own the visual? Buy Bannerbear, save the API call, ship faster than building it yourself. Backfilling an archive once with no infra appetite? Open a generator, run a spreadsheet loop, move on. The wrong move is hand-designing every post in Figma — that's the system that quietly breaks the moment publishing volume goes up.
og:image:width and og:image:height explicitly. Saves the platform a round trip and unlocks the large card preview.The compounding benefit isn't per-post CTR — it's the fact that every old post becomes shareable retroactively. A blog that ships 50 posts a year without OG images leaves probably 60–80% of its potential social traffic on the table. Setting up auto-generation once captures that traffic on every future post AND on every backfilled archive post in the same afternoon. There aren't many wins in 2026 SEO that take an afternoon and pay back forever — this is one of them.
Pick the approach that matches your stack, ship the integration today, then come back tomorrow and backfill your archive. The whole project is a weekend at most. The traffic uplift compounds for years.
Use it as the static base for Cloudinary, the design reference for @vercel/og, or the template inside Bannerbear. 1200 × 630, safe zones built in, downloads as PNG.
Try it free →