Back to Blog
og-imageopen-graphapidynamic-images

OG Image Generator API: Create Dynamic Social Images at Scale

ST
ScreenURL Team
8 min read

OG Image Generator API: Create Dynamic Social Images at Scale

When someone shares your link on Twitter, LinkedIn, or Slack, the preview image can make or break that click. A compelling OG image increases engagement by up to 40%. But generating unique images for thousands of pages? That's where most developers hit a wall.

This guide shows you how to build a dynamic OG image system using a screenshot API—no image manipulation libraries, no serverless cold starts, no design skills required.

What Are OG Images?

Open Graph (OG) images are the preview images that appear when you share a URL on social platforms. They're defined by the og:image meta tag:

<meta property="og:image" content="https://example.com/og/my-page.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

The standard dimensions are 1200×630 pixels—optimized for Twitter cards, LinkedIn posts, Facebook shares, and Slack unfurls.

Why OG Images Matter

  • 40% higher click-through rates on posts with custom images vs. generic ones
  • Brand recognition across every share
  • Context at a glance—users know what they're clicking before they click
  • SEO signals—social engagement feeds back into search rankings

The problem? Creating unique OG images for a blog with 500 posts, an e-commerce site with 10,000 products, or a SaaS with user-generated content isn't feasible by hand.

The Dynamic OG Image Challenge

Static OG images work fine for homepages. But modern applications need dynamic images that include:

  • Blog posts: Title, author, reading time, category
  • Product pages: Product image, price, rating
  • User profiles: Avatar, name, bio, stats
  • Dashboard shares: Charts, metrics, data visualizations

Generating these dynamically means building an image generation pipeline. Most teams try one of three approaches—each with significant tradeoffs.

DIY Approaches (And Why They're Painful)

1. Canvas/Sharp Libraries

Generate images server-side using Node.js libraries like node-canvas or sharp.

// Example with node-canvas
const { createCanvas, loadImage } = require('canvas');

async function generateOG(title, author) {
  const canvas = createCanvas(1200, 630);
  const ctx = canvas.getContext('2d');
  
  // Background
  ctx.fillStyle = '#1a1a2e';
  ctx.fillRect(0, 0, 1200, 630);
  
  // Title
  ctx.font = 'bold 48px Inter';
  ctx.fillStyle = '#ffffff';
  ctx.fillText(title, 60, 300);
  
  return canvas.toBuffer('image/png');
}

Pros: Full control, no external dependencies
Cons: Complex text wrapping, no CSS styling, font management nightmare, manual positioning of every element

2. Puppeteer/Playwright

Render HTML templates and screenshot them.

const puppeteer = require('puppeteer');

async function generateOG(title) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 630 });
  await page.setContent(`
    <div style="width:1200px;height:630px;background:#1a1a2e;padding:60px;">
      <h1 style="color:white;font-size:48px;">${title}</h1>
    </div>
  `);
  const buffer = await page.screenshot({ type: 'png' });
  await browser.close();
  return buffer;
}

Pros: Use HTML/CSS for layouts, familiar tooling
Cons: 2-5 second cold starts, 200MB+ memory per instance, browser management overhead, scaling nightmares

3. Serverless Image Generation (Vercel OG)

Vercel's @vercel/og library uses Satori to convert JSX to SVG, then to PNG.

// app/api/og/route.tsx
import { ImageResponse } from '@vercel/og';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title');
  
  return new ImageResponse(
    <div style={{ display: 'flex', background: '#1a1a2e' }}>
      <h1 style={{ color: 'white' }}>{title}</h1>
    </div>,
    { width: 1200, height: 630 }
  );
}

Pros: Edge-deployed, fast after warm-up
Cons: Limited CSS support (no grid, limited flexbox), custom fonts require base64 encoding, complex layouts break, vendor lock-in

The Screenshot API Approach

What if you could design your OG images as regular web pages—with full CSS, any fonts, animations, gradients—and just screenshot them?

That's where ScreenURL comes in. Instead of fighting with image libraries:

  1. Design a template page using your existing frontend stack
  2. Pass dynamic data via URL parameters
  3. Screenshot it via API call
  4. Cache aggressively at the CDN level
https://screenurl.com/api/screenshot
  ?url=https://yoursite.com/og-template?title=Hello+World
  &width=1200
  &height=630
  &format=png

Why this works better:

  • Full CSS support: Flexbox, Grid, animations, gradients, backdrop-blur
  • Any fonts: Google Fonts, custom typefaces, icon fonts
  • Your existing stack: React, Vue, Svelte, plain HTML
  • No cold starts: API handles browser pooling
  • Predictable scaling: Pay per screenshot, not per compute second

Implementation Examples

Blog Post OG Images

Create a template page that accepts URL parameters:

<!-- /og-template/blog.html -->
<!DOCTYPE html>
<html>
<head>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      width: 1200px;
      height: 630px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      font-family: 'Inter', sans-serif;
      display: flex;
      flex-direction: column;
      justify-content: center;
      padding: 60px;
    }
    .title {
      color: white;
      font-size: 56px;
      font-weight: 700;
      line-height: 1.2;
      margin-bottom: 24px;
    }
    .meta {
      color: rgba(255,255,255,0.8);
      font-size: 24px;
      display: flex;
      gap: 16px;
    }
    .logo {
      position: absolute;
      bottom: 40px;
      right: 60px;
      height: 48px;
    }
  </style>
</head>
<body>
  <h1 class="title" id="title"></h1>
  <div class="meta">
    <span id="author"></span>
    <span></span>
    <span id="readTime"></span>
  </div>
  <img src="/logo-white.svg" class="logo" alt="">
  
  <script>
    const params = new URLSearchParams(window.location.search);
    document.getElementById('title').textContent = params.get('title') || 'Untitled';
    document.getElementById('author').textContent = params.get('author') || 'Anonymous';
    document.getElementById('readTime').textContent = params.get('readTime') || '5 min read';
  </script>
</body>
</html>

Generate the OG image:

const ogUrl = `https://screenurl.com/api/screenshot?` + new URLSearchParams({
  url: `https://yoursite.com/og-template/blog.html?title=${encodeURIComponent(post.title)}&author=${post.author}&readTime=${post.readTime}`,
  width: '1200',
  height: '630',
  format: 'png',
  apiKey: process.env.SCREENURL_API_KEY
});

Product Page OG Images

<body>
  <div class="product-card">
    <img class="product-image" id="image" src="" alt="">
    <div class="details">
      <h1 class="name" id="name"></h1>
      <div class="price" id="price"></div>
      <div class="rating" id="rating"></div>
    </div>
  </div>
</body>

User Profile Cards

<body>
  <div class="profile">
    <img class="avatar" id="avatar" src="" alt="">
    <h1 class="name" id="name"></h1>
    <p class="bio" id="bio"></p>
    <div class="stats">
      <div><strong id="followers">0</strong> followers</div>
      <div><strong id="posts">0</strong> posts</div>
    </div>
  </div>
</body>

Template Design Tips

Great OG images follow these principles:

1. Hierarchy Matters

The title should be readable in a thumbnail. Use 48-64px font sizes for primary text.

2. Contrast is King

Test your image at 300px width—can you still read it? High contrast (white on dark, dark on light) wins.

3. Brand Consistently

Include your logo, use brand colors, maintain visual identity across all generated images.

4. Leave Safe Zones

Social platforms may crop edges. Keep critical content within an inner 1100×550 area.

5. Limit Text

3-4 lines maximum. OG images are glanceable, not readable.

/* Safe zone helper */
body {
  padding: 60px 50px;
}
.title {
  max-width: 900px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

Performance & Caching

OG images don't need to be generated in real-time. Cache them aggressively.

URL Pattern Strategy

Use deterministic URLs based on content:

// Generate a hash from content
const contentHash = crypto
  .createHash('md5')
  .update(post.title + post.updatedAt)
  .digest('hex')
  .slice(0, 8);

const ogImageUrl = `https://yourcdn.com/og/${post.slug}-${contentHash}.png`;

CDN Caching Headers

Set long cache times—regenerate only when content changes:

// In your OG image endpoint
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.setHeader('CDN-Cache-Control', 'public, max-age=31536000');

Pre-generation vs. On-Demand

Approach Best For
Pre-generate (build time) Static sites, limited pages (<1000)
On-demand (first request) Dynamic content, user-generated pages
Hybrid (pre-gen popular, on-demand rest) Large sites with traffic distribution

Code Examples

Next.js App Router

// app/api/og/[slug]/route.ts
import { NextResponse } from 'next/server';

export async function GET(
  request: Request,
  { params }: { params: { slug: string } }
) {
  const post = await getPost(params.slug);
  
  const screenshotUrl = new URL('https://screenurl.com/api/screenshot');
  screenshotUrl.searchParams.set('url', 
    `${process.env.NEXT_PUBLIC_URL}/og-template?title=${encodeURIComponent(post.title)}`
  );
  screenshotUrl.searchParams.set('width', '1200');
  screenshotUrl.searchParams.set('height', '630');
  screenshotUrl.searchParams.set('format', 'png');
  screenshotUrl.searchParams.set('apiKey', process.env.SCREENURL_API_KEY!);
  
  const response = await fetch(screenshotUrl);
  const imageBuffer = await response.arrayBuffer();
  
  return new NextResponse(imageBuffer, {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=86400, s-maxage=31536000',
    },
  });
}

Cloudflare Workers

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const title = url.searchParams.get('title') || 'Default Title';
    
    // Check cache first
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;
    let response = await cache.match(cacheKey);
    
    if (!response) {
      const screenshotUrl = `https://screenurl.com/api/screenshot?` +
        `url=${encodeURIComponent(`https://yoursite.com/og?title=${title}`)}` +
        `&width=1200&height=630&format=png&apiKey=${env.SCREENURL_API_KEY}`;
      
      response = await fetch(screenshotUrl);
      response = new Response(response.body, {
        headers: {
          'Content-Type': 'image/png',
          'Cache-Control': 'public, max-age=31536000',
        },
      });
      
      await cache.put(cacheKey, response.clone());
    }
    
    return response;
  },
};

Vercel Edge Function

// pages/api/og.ts
import type { NextRequest } from 'next/server';

export const config = { runtime: 'edge' };

export default async function handler(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get('title');
  
  const imageUrl = `https://screenurl.com/api/screenshot?` +
    new URLSearchParams({
      url: `https://yoursite.com/og-template?title=${encodeURIComponent(title || '')}`,
      width: '1200',
      height: '630',
      format: 'png',
      apiKey: process.env.SCREENURL_API_KEY!,
    });
  
  const imageResponse = await fetch(imageUrl);
  
  return new Response(imageResponse.body, {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, s-maxage=31536000, stale-while-revalidate',
    },
  });
}

Wrapping Up

Dynamic OG images don't have to be complicated. Instead of wrestling with Canvas APIs or managing Puppeteer instances:

  1. Design your OG templates as web pages
  2. Pass dynamic data via URL parameters
  3. Screenshot with ScreenURL's API
  4. Cache at the CDN layer

You get full CSS support, any fonts you want, and images that scale from 100 to 100,000 pages without infrastructure headaches.

Ready to generate your first dynamic OG image? Get started with ScreenURL's free tier—100 screenshots per month, no credit card required.