BlogTutorial

Generate Open Graph Images from HTML Templates

No Figma. No SaaS. Just HTML and a self-hosted screenshot API.

The idea: Write your OG image as an HTML file. Hit 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:

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 up

Openkova 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

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.