Generate Open Graph Images from HTML Templates
No Figma. No SaaS. Just HTML and a self-hosted screenshot API.
POST /api/convert/snippet on a self-hosted Openkova instance. Get back a 1200×630 PNG. Cache it. Done.Why generate OG images from HTML?
Open Graph images are the preview cards that appear when your link is shared on Twitter, LinkedIn, Slack, or iMessage. A good OG image dramatically increases click-through rates. The problem is generating them at scale.
The common alternatives all have trade-offs:
- Static images — design one, use it everywhere. Fast, but every page looks the same.
- Vercel OG (Satori) — generates images at the edge, but requires a JSX-to-SVG pipeline that doesn't support all CSS.
- Bannerbear / Placid — SaaS tools with templates and APIs. Monthly fee, usage limits, and your content goes through their servers.
HTML templates rendered by a real browser give you the full CSS feature set — Grid, custom properties, backdrop-filter, Google Fonts — with no extra tooling.
The setup
git clone https://github.com/scnix-git/openkova
cd openkova
docker compose upOpenkova is now listening on port 3000.
Write your OG template
Create an HTML file at 1200×630 px. Use whatever CSS you like — Grid, custom properties, web fonts. Here's a minimal example:
<!-- og-template.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; margin: 0; }
body {
width: 1200px; height: 630px;
background: #0f0f0f;
color: #fff;
font-family: sans-serif;
display: grid;
grid-template-rows: 1fr auto;
padding: 64px;
}
h1 { font-size: 3rem; line-height: 1.2; max-width: 800px; }
.meta { font-size: 1.2rem; color: #7c6af7; }
</style>
</head>
<body>
<h1>{{TITLE}}</h1>
<span class="meta">openkova.dev</span>
</body>
</html>Render it with Openkova
Replace the {{TITLE}} placeholder in your backend, then send the HTML:
// Node.js example
const fs = require('fs');
async function generateOgImage(title) {
const template = fs.readFileSync('./og-template.html', 'utf8');
const html = template.replace('{{TITLE}}', escapeHtml(title));
const res = await fetch('http://localhost:3000/api/convert/snippet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html }),
});
// Read SSE stream to completion
const reader = res.body.getReader();
let filePath = null;
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (line.startsWith('data: ')) {
const event = JSON.parse(line.slice(6));
if (event.type === 'done') filePath = event.filePath;
}
}
}
return filePath;
}Caching the output
OG images don't change unless your content changes. Cache them by content hash or post slug. A simple pattern with Next.js:
// app/api/og/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { readFileSync } from 'fs';
export async function GET(req: NextRequest) {
const title = req.nextUrl.searchParams.get('title') ?? 'Openkova';
const png = await generateOgImage(title); // calls Openkova
return new NextResponse(readFileSync(png), {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=604800, immutable',
},
});
}Set the og:image meta tag to /api/og?title=Your+Post+Title. The first request generates and caches the PNG; subsequent requests are served from the CDN edge.
Tips for better OG images
- Fix the viewport — always set
width: 1200px; height: 630pxonbodyand useoverflow: hiddento prevent scroll - Embed fonts — network-loaded fonts can time out in CI or serverless environments; use a
@font-facewith a base64 data URI - Truncate long titles — clamp at ~60 characters client-side before sending to Openkova
- Use CSS variables — one template with
--brand-colorand--bg-colorinjected viastyleattribute covers many themes
Frequently asked questions
What size should Open Graph images be?
1200×630 px (1.91:1 ratio). Twitter summary_large_image and most link preview scrapers use this size. Keep critical content in the center to survive crops.
Can I generate OG images without Vercel or a cloud service?
Yes. Openkova runs on any Docker host. Send HTML to POST /api/convert/snippet; get back a PNG. No SaaS account, no API key.
How do I pass dynamic data into an HTML OG image template?
Build the HTML string server-side (Node.js template literal, Python f-string, or any templating engine) before posting it to Openkova. Sanitize user input to prevent HTML injection.
Does Openkova support web fonts in OG images?
Yes. Headless Chromium loads Google Fonts and other CDN fonts normally. For reliability, embed fonts as base64 @font-face data URIs so there is no external network dependency at render time.
Next: Screenshot API for AI agents — or deploy Openkova first with the Docker setup guide.