Generate OG Images from HTML Templates
Turn any HTML template into a 1200×630 Open Graph image using real headless Chromium. Full CSS support. Free, self-hosted, no API key, no design tools required.
What is OG image generation?
OG image generation is the process of automatically creating Open Graph preview images — the 1200×630 cards that appear when a URL is shared on Twitter/X, LinkedIn, Slack, iMessage, and other platforms. Pages with custom OG images receive 2–3× more clicks from social shares than pages displaying a generic screenshot or no image.
The standard approach: write an HTML template with your brand, title, author, and date; render it at 1200×630 using headless Chromium; save the PNG alongside your page. No design tools needed — if you can write HTML and CSS, you can generate OG images.
Why HTML templates beat design tools
Hand-designing a unique OG image for every blog post doesn't scale. HTML templates let you:
- Generate images at build time, on-demand, or in CI/CD pipelines automatically
- Maintain brand consistency without a designer touching every post
- Update the template once and regenerate all images in seconds
- Include dynamic content — views, date, reading time, author — without re-opening a design file
The Openkova approach
Openkova accepts a raw HTML string and returns a PNG — that's the entire workflow. Because it uses real headless Chromium (not a custom renderer), any CSS that works in Chrome works in your OG images.
curl -X POST https://your-openkova-instance/api/screenshot \
-H "Content-Type: application/json" \
-d '{
"html": "<html><body style=\"margin:0;background:#0f0f0f;display:flex;align-items:center;justify-content:center;height:630px;font-family:sans-serif\"><div style=\"color:#e8e8e8;font-size:48px;font-weight:700;padding:60px\">My Blog Post Title</div></body></html>",
"width": 1200,
"height": 630,
"format": "png"
}' \
--output og-image.pngFor a reusable template, keep the HTML separate and interpolate dynamic values:
// generate-og.ts
import fs from 'fs';
async function generateOGImage(title: string, author: string, date: string) {
const template = fs.readFileSync('./og-template.html', 'utf-8')
.replace('{{title}}', title)
.replace('{{author}}', author)
.replace('{{date}}', date);
const res = await fetch('https://your-openkova-instance/api/screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: template, width: 1200, height: 630, format: 'png' }),
});
return res.arrayBuffer();
}Comparison: OG image generation approaches
| Approach | CSS support | Cost | Self-hosted | Build-time | On-demand |
|---|---|---|---|---|---|
| Openkova | Full (Chromium) | Free | ✓ | ✓ | ✓ |
| @vercel/og (Satori) | Limited subset | Free (Vercel only) | ✗ | ✗ | ✓ |
| Cloudinary | Limited overlays | $0.05–$0.12/image | ✗ | ✗ | ✓ |
| Custom Puppeteer | Full (Chromium) | Server cost | ✓ | ✓ | ✓ |
| html-to-image (browser) | Partial (Canvas) | Free | ✓ | ✗ | Client only |
CSS features that work in Openkova OG images
Because Openkova uses real Chromium, all CSS that Chrome supports works in your OG images. No limitations, no undocumented quirks:
- CSS Grid and Flexbox — full layout support including
gap,grid-template-areas, subgrid - CSS custom properties —
var(--color-accent)works exactly as in your design system - Custom web fonts — load via
@font-facewith a URL or base64-encoded font data - CSS
clip-pathandmask— shapes, gradients, and decorative effects - CSS
backdrop-filter— blur, brightness, and saturation on overlapping layers - CSS
text-shadowandbox-shadow— full shadow support including multiple shadows
@vercel/og limitations
Satori (the renderer behind @vercel/og) reimplements a CSS subset in JavaScript — it does not use a real browser. Known limitations as of 2026:
- No CSS Grid — only Flexbox
- No CSS pseudo-elements (
::before,::after) - No
calc()in certain contexts - Custom fonts must be loaded manually as
ArrayBuffer - No SVG animations or CSS animations
- Locked to Vercel infrastructure — cannot self-host
Build-time OG image generation
For static sites (Next.js static export, Astro, Eleventy), generate all OG images at build time. This is the fastest approach — images are static files, no server required at runtime.
// scripts/generate-og-images.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const POSTS_DIR = './content/blog';
const OUTPUT_DIR = './public/og';
const posts = fs.readdirSync(POSTS_DIR)
.filter(f => f.endsWith('.mdx'));
for (const file of posts) {
const { data } = matter(fs.readFileSync(path.join(POSTS_DIR, file), 'utf-8'));
const slug = file.replace('.mdx', '');
const html = `
<html>
<head>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 1200px; height: 630px;
background: #0f0f0f;
font-family: 'Inter', sans-serif;
display: flex; align-items: center;
padding: 80px;
}
.title { color: #e8e8e8; font-size: 52px; font-weight: 700; line-height: 1.2; }
.tag { color: #7c6af7; font-size: 18px; margin-bottom: 24px; text-transform: uppercase; letter-spacing: 0.1em; }
</style>
</head>
<body>
<div>
<div class="tag">${data.tag ?? 'Blog'}</div>
<div class="title">${data.title}</div>
</div>
</body>
</html>
`;
const res = await fetch('https://your-openkova-instance/api/screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html, width: 1200, height: 630, format: 'png' }),
});
const buffer = await res.arrayBuffer();
fs.writeFileSync(path.join(OUTPUT_DIR, `${slug}.png`), Buffer.from(buffer));
console.log(`Generated: ${slug}.png`);
}On-demand OG image generation (Next.js route handler)
For dynamic content, generate OG images on-demand in a Next.js route handler. Cache the response to avoid regenerating on every request.
// app/api/og/route.ts
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const title = searchParams.get('title') ?? 'Untitled';
const tag = searchParams.get('tag') ?? 'Blog';
const html = `
<html><head>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { width: 1200px; height: 630px; background: #0f0f0f;
font-family: sans-serif; display: flex; align-items: center; padding: 80px; }
.tag { color: #7c6af7; font-size: 18px; margin-bottom: 20px;
text-transform: uppercase; letter-spacing: 0.1em; display: block; }
.title { color: #e8e8e8; font-size: 52px; font-weight: 700; line-height: 1.2; }
</style>
</head><body><div>
<span class="tag">${tag}</span>
<div class="title">${title}</div>
</div></body></html>
`;
const res = await fetch(process.env.OPENKOVA_URL + '/api/screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html, width: 1200, height: 630, format: 'png' }),
});
const image = await res.arrayBuffer();
return new NextResponse(image, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
}Reference the route in your page metadata:
export const metadata: Metadata = {
openGraph: {
images: [{ url: `/api/og?title=${encodeURIComponent(title)}&tag=${tag}`, width: 1200, height: 630 }],
},
};OG image generation in CI/CD
Generate OG images as part of your build pipeline using @openkova/cli — no running server or Docker container required. The CLI uses your local Chromium installation directly.
# .github/workflows/og-images.yml
name: Generate OG Images
on: [push]
jobs:
og:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install @openkova/cli
run: npm install -g @openkova/cli
- name: Generate OG images
run: |
for file in public/og-templates/*.html; do
slug=$(basename "$file" .html)
kova screenshot "$file" --output "public/og/$slug.png" --width 1200 --height 630
done
- name: Commit OG images
run: |
git add public/og/
git diff --staged --quiet || git commit -m "chore: regenerate OG images"Deploy Openkova for OG image generation
Openkova ships as a Docker image with bundled Chromium. One command and it's running:
docker run -p 3000:3000 ghcr.io/scnix-git/openkova:latestFor production, use Docker Compose with a persistent volume:
# docker-compose.yml
services:
openkova:
image: ghcr.io/scnix-git/openkova:latest
ports:
- "3000:3000"
restart: unless-stopped
volumes:
- ./data:/app/dataFor cloud deployments (Railway, Fly.io, Render), see the full documentation. The Docker deployment guide covers production configuration and storage.
Frequently asked questions
What is OG image generation?
OG image generation is the automated creation of Open Graph preview images — the 1200×630 cards shown when a URL is shared on social platforms. Pages with OG images get 2–3× more clicks on social shares than pages without them.
How do I generate OG images from HTML?
Pass your HTML template as a string to Openkova with width: 1200, height: 630, and format: "png". It renders the HTML using headless Chromium and returns the PNG. You can do this at build time, on-demand in a server route, or in CI/CD using @openkova/cli.
What is the difference between @vercel/og and Openkova?
@vercel/og uses Satori, a JavaScript JSX-to-SVG renderer with limited CSS support (no Grid, no pseudo-elements, no calc() in some cases). Openkova uses real headless Chromium — any CSS that works in Chrome works in your OG images. Trade-off: @vercel/og is zero-setup on Vercel; Openkova requires self-hosting but has no CSS constraints.
Can I use custom fonts in OG images?
Yes. In your HTML template, include a @font-facedeclaration pointing to a font URL or embed the font as a base64 data URI. Openkova's Chromium instance will load and render the font exactly as Chrome does — no manual font buffer loading required.
Can I generate OG images in GitHub Actions?
Yes. Install @openkova/cli in your workflow and run kova screenshot template.html --width 1200 --height 630 --output og.png. No server required — the CLI uses the local Chromium that GitHub Actions includes.
How much does OG image generation cost with Openkova?
Openkova is free — MIT-licensed, self-hosted, no per-image charges. A $20/mo VPS handles thousands of OG images per day. Cloudinary charges $0.05–$0.12 per transformation on paid plans; at 10,000 images that's $500–$1,200/month versus $20/month for a self-hosted Openkova instance.