JavaScript 6 min read

Build a Dynamic OG Image Generator for Your Blog (Next.js + Screenshot API)

Stop manually creating social preview images. Learn how to automatically generate unique Open Graph images for every blog post using Next.js and a screenshot API.

MR

Moshiour Rahman

Advertisement

Every time you share a blog post on Twitter or LinkedIn, you need an eye-catching Open Graph image. Creating these manually for every post is tedious. What if they could generate themselves?

In this tutorial, we’ll build a system that automatically creates unique, branded OG images for every blog post—no design tools required.

What We’re Building

FeatureDescription
Dynamic generationEach blog post gets a unique image
Branded designConsistent styling across all images
Zero manual workImages generate on-demand
Cached performanceSame URL = instant response

Here’s the flow:

  1. Blog post has metadata (title, author, date)
  2. API renders a styled HTML template with that data
  3. Screenshot captures it as an image
  4. OG meta tags point to the generated image

Prerequisites

Before we start, you’ll need:

  • Next.js 13+ project (App Router or Pages Router)
  • Node.js 18+
  • Screenshot API key (we’ll use SnapForge - 50 free/day)

Step 1: Create the OG Template Page

First, we need an HTML page that displays our OG image design. This page will be screenshotted to become the actual image.

// app/og-template/page.tsx (App Router)
// or pages/og-template.tsx (Pages Router)

export default function OGTemplate({
  searchParams,
}: {
  searchParams: { title?: string; author?: string; date?: string };
}) {
  const title = searchParams.title || 'Untitled Post';
  const author = searchParams.author || 'Anonymous';
  const date = searchParams.date || new Date().toLocaleDateString();

  return (
    <html>
      <body style={{ margin: 0, padding: 0 }}>
        <div
          style={{
            width: '1200px',
            height: '630px',
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            alignItems: 'center',
            background: 'linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%)',
            fontFamily: 'system-ui, -apple-system, sans-serif',
            padding: '60px',
            boxSizing: 'border-box',
          }}
        >
          {/* Logo/Brand */}
          <div
            style={{
              position: 'absolute',
              top: '40px',
              left: '60px',
              display: 'flex',
              alignItems: 'center',
              gap: '12px',
            }}
          >
            <div
              style={{
                width: '48px',
                height: '48px',
                borderRadius: '12px',
                background: 'linear-gradient(135deg, #6366f1, #10b981)',
              }}
            />
            <span style={{ color: 'white', fontSize: '24px', fontWeight: 600 }}>
              YourBlog
            </span>
          </div>

          {/* Title */}
          <h1
            style={{
              color: 'white',
              fontSize: title.length > 60 ? '48px' : '64px',
              fontWeight: 800,
              textAlign: 'center',
              margin: 0,
              lineHeight: 1.2,
              maxWidth: '1000px',
            }}
          >
            {title}
          </h1>

          {/* Author & Date */}
          <div
            style={{
              position: 'absolute',
              bottom: '40px',
              display: 'flex',
              alignItems: 'center',
              gap: '20px',
              color: '#94a3b8',
              fontSize: '20px',
            }}
          >
            <span>By {author}</span>
            <span>•</span>
            <span>{date}</span>
          </div>
        </div>
      </body>
    </html>
  );
}

Visit /og-template?title=Hello%20World&author=John to see your template.

Step 2: Create the Screenshot API Route

Now we create an API endpoint that screenshots our template and returns the image.

// app/api/og/route.ts (App Router)

import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const title = searchParams.get('title') || 'Untitled';
  const author = searchParams.get('author') || 'Anonymous';
  const date = searchParams.get('date') || '';

  // Build the template URL
  const templateUrl = new URL('/og-template', request.url);
  templateUrl.searchParams.set('title', title);
  templateUrl.searchParams.set('author', author);
  templateUrl.searchParams.set('date', date);

  try {
    // Screenshot the template using SnapForge
    const screenshotUrl = new URL('https://api.snapforge.techyowls.io/v1/screenshot');
    screenshotUrl.searchParams.set('url', templateUrl.toString());
    screenshotUrl.searchParams.set('width', '1200');
    screenshotUrl.searchParams.set('height', '630');
    screenshotUrl.searchParams.set('format', 'png');

    const response = await fetch(screenshotUrl.toString(), {
      headers: {
        'X-API-Key': process.env.SNAPFORGE_API_KEY!,
      },
    });

    if (!response.ok) {
      throw new Error(`Screenshot failed: ${response.status}`);
    }

    const imageBuffer = await response.arrayBuffer();

    return new NextResponse(imageBuffer, {
      headers: {
        'Content-Type': 'image/png',
        'Cache-Control': 'public, max-age=86400, s-maxage=86400',
      },
    });
  } catch (error) {
    console.error('OG image generation failed:', error);

    // Return a fallback or error image
    return new NextResponse('Failed to generate image', { status: 500 });
  }
}

For Pages Router, use:

// pages/api/og.ts

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { title = 'Untitled', author = 'Anonymous', date = '' } = req.query;

  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
  const templateUrl = `${baseUrl}/og-template?title=${encodeURIComponent(
    String(title)
  )}&author=${encodeURIComponent(String(author))}&date=${encodeURIComponent(
    String(date)
  )}`;

  try {
    const response = await fetch(
      `https://api.snapforge.techyowls.io/v1/screenshot?` +
        `url=${encodeURIComponent(templateUrl)}&width=1200&height=630`,
      {
        headers: {
          'X-API-Key': process.env.SNAPFORGE_API_KEY!,
        },
      }
    );

    const imageBuffer = Buffer.from(await response.arrayBuffer());

    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Cache-Control', 'public, max-age=86400');
    res.send(imageBuffer);
  } catch (error) {
    res.status(500).send('Failed to generate image');
  }
}

Step 3: Add Environment Variables

Create or update your .env.local:

SNAPFORGE_API_KEY=sk_live_your_key_here
NEXT_PUBLIC_BASE_URL=https://yourdomain.com

Get your free API key at app.snapforge.techyowls.io.

Step 4: Use in Your Blog Post Layout

Now use the dynamic OG image in your blog post metadata:

// app/blog/[slug]/page.tsx

import { Metadata } from 'next';

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug); // Your data fetching

  const ogImageUrl = new URL('/api/og', process.env.NEXT_PUBLIC_BASE_URL);
  ogImageUrl.searchParams.set('title', post.title);
  ogImageUrl.searchParams.set('author', post.author);
  ogImageUrl.searchParams.set('date', post.date);

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      images: [
        {
          url: ogImageUrl.toString(),
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
      images: [ogImageUrl.toString()],
    },
  };
}

Step 5: Test Your OG Images

  1. Visit your blog post
  2. Use the Facebook Sharing Debugger or Twitter Card Validator
  3. Paste your blog post URL
  4. See your dynamically generated OG image

Performance Optimization

The screenshot API caches responses automatically. The same URL parameters will return a cached image in ~50ms instead of generating a new one.

Request TypeResponse Time
First request (cache miss)~2-3 seconds
Subsequent requests (cache hit)~50ms

You can verify caching by checking the X-Cache header:

  • X-Cache: MISS - First time, screenshot was generated
  • X-Cache: HIT - Cached, instant response

Advanced: Custom Fonts

To use custom fonts, load them in your template via Google Fonts:

// In your og-template page
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@600;800&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <div style={{ fontFamily: "'Inter', sans-serif" }}>
      {/* Your template */}
    </div>
  </body>
</html>

Add a delay parameter to ensure fonts load:

screenshotUrl.searchParams.set('delay', '500'); // Wait 500ms for fonts

Advanced: Different Templates for Categories

You can create different designs per category:

// Determine template based on category
const templatePath = post.category === 'tutorial'
  ? '/og-template/tutorial'
  : '/og-template/default';

const templateUrl = `${baseUrl}${templatePath}?title=${encodeURIComponent(post.title)}`;

Complete Example Repository

Here’s a working example with all the code:

git clone https://github.com/techyowls/nextjs-og-generator-example
cd nextjs-og-generator-example
npm install
cp .env.example .env.local
# Add your SNAPFORGE_API_KEY
npm run dev

Summary

What We BuiltHow It Works
HTML templateDisplays blog metadata with custom styling
API endpointScreenshots template, returns image
OG meta tagsPoint to dynamic image URL
CachingSame post = instant cached image

No more Figma templates. No more manual exports. Every blog post automatically gets a unique, branded social preview image.

What’s Next?

  • Add gradients or images based on post category
  • Include reading time or tags in the design
  • Create video thumbnails using the same approach

This tutorial uses SnapForge for screenshot capture. The free tier includes 50 screenshots/day—plenty for a blog with moderate traffic.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.