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.
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
| Feature | Description |
|---|---|
| Dynamic generation | Each blog post gets a unique image |
| Branded design | Consistent styling across all images |
| Zero manual work | Images generate on-demand |
| Cached performance | Same URL = instant response |
Here’s the flow:
- Blog post has metadata (title, author, date)
- API renders a styled HTML template with that data
- Screenshot captures it as an image
- 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
- Visit your blog post
- Use the Facebook Sharing Debugger or Twitter Card Validator
- Paste your blog post URL
- 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 Type | Response 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 generatedX-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 Built | How It Works |
|---|---|
| HTML template | Displays blog metadata with custom styling |
| API endpoint | Screenshots template, returns image |
| OG meta tags | Point to dynamic image URL |
| Caching | Same 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
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
Turborepo Advanced Patterns: Production-Ready Monorepo Architecture in 2025
Master production Turborepo patterns with real-world examples. Migration guide, CI/CD pipelines, remote caching, testing strategies, and framework-specific configs.
JavaScripttRPC: End-to-End Type-Safe APIs for TypeScript
Master tRPC for full-stack TypeScript applications. Learn procedures, routers, React Query integration, and build type-safe APIs without schemas.
JavaScriptGraphQL API Development: Complete Guide with Node.js
Master GraphQL API development from scratch. Learn schema design, resolvers, queries, mutations, subscriptions, and authentication best practices.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.