Build a Dynamic OG Image Service
Open Graph (OG) and Twitter Card images boost CTR and social sharing. Instead of running your own headless renderer, point Supacrawler’s Screenshots API at a hosted HTML template and generate images on demand.
Architecture
- A hosted HTML/CSS template (e.g.,
/og-template
) that reads query params (title, author, theme) - An API route that calls the Screenshots API to capture the template URL
- A cache layer (filesystem, KV, or object storage)
1) Minimal HTML template
Host a responsive template in your app (Next.js, static site, etc.).
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { margin: 0; font-family: Inter, system-ui; background: #0b0f19; color: #fff; }
.wrap { display: flex; flex-direction: column; justify-content: center; height: 100vh; padding: 64px; }
h1 { font-size: 72px; line-height: 1.05; margin: 0 0 16px 0; }
p { font-size: 28px; opacity: 0.9; margin: 0; }
</style>
</head>
<body>
<div class="wrap">
<h1 id="title"></h1>
<p id="meta"></p>
</div>
<script>
const q = new URLSearchParams(location.search)
document.getElementById('title').textContent = q.get('title') || 'Hello, OG!'
document.getElementById('meta').textContent = q.get('meta') || 'By Supacrawler'
document.body.style.background = q.get('bg') || '#0b0f19'
</script>
</body>
</html>
2) Next.js API route to generate OG images
Render the template at a fixed viewport and return a binary image.
// app/api/og/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { SupacrawlerClient, ScreenshotCreateRequest } from '@supacrawler/js'
const client = new SupacrawlerClient({ apiKey: process.env.SUPACRAWLER_API_KEY! })
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const title = searchParams.get('title') || 'Untitled'
const meta = searchParams.get('meta') || 'By Supacrawler'
const bg = searchParams.get('bg') || '#0b0f19'
const templateUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/og-template?title=${encodeURIComponent(title)}&meta=${encodeURIComponent(meta)}&bg=${encodeURIComponent(bg)}`
const job = await client.createScreenshotJob({
url: templateUrl,
device: ScreenshotCreateRequest.device.CUSTOM,
width: 1200,
height: 630,
format: ScreenshotCreateRequest.format.PNG,
wait_until: 'domcontentloaded',
block_ads: true,
})
const res = await client.waitForScreenshot(job.job_id!)
const img = await fetch(res.screenshot).then(r => r.arrayBuffer())
return new NextResponse(Buffer.from(img), { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=31536000, immutable' } })
}
3) Caching and storage
- Derive a cache key from the query params (e.g., SHA-1 of
title|meta|bg
) - Store renders in object storage (S3, R2) and respond with a redirect when hit
- For static content, pre‑render popular combinations at build time
Rendering tips
- Use
CUSTOM 1200x630
to match OG dimensions domcontentloaded
is enough for static templates; add a smalldelay
if using web fonts- Prefer PNG for text‑heavy cards; WebP if file size matters
That’s it—dynamic social images with clean templates, stable rendering, and no headless infra. Wire the route into your <meta property="og:image" />
and ship.