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

  1. A hosted HTML/CSS template (e.g., /og-template) that reads query params (title, author, theme)
  2. An API route that calls the Screenshots API to capture the template URL
  3. 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 small delay 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.

Was this page helpful?