📖 Table of Contents
Prerequisites
- Next.js 12+ (works with 13, 14, and 15)
- Node.js 18+
- An OG Image API key (get one free)
Get Your API Key
Sign up at ogimageapi.io to get your free API key. The free tier includes 25 images per month — plenty for development and small sites.
Your API key will look like: og_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
Set Up Environment Variables
Add your API key to your environment variables:
# OG Image API
OG_IMAGE_API_KEY=og_your_api_key_here
# Your site URL (for absolute image URLs)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
.env.local to your .gitignore.
Create a Utility Function
Create a reusable function for generating OG image URLs:
interface OGImageParams {
title: string;
subtitle?: string;
theme?: 'dark' | 'light';
template?: string;
}
/**
* Generate an OG image URL with the given parameters
*/
export function getOGImageUrl(params: OGImageParams): string {
const url = new URL('https://ogimageapi.io/api/generate');
url.searchParams.set('title', params.title);
if (params.subtitle) {
url.searchParams.set('subtitle', params.subtitle);
}
url.searchParams.set('theme', params.theme || 'dark');
if (params.template) {
url.searchParams.set('template', params.template);
}
return url.toString();
}
/**
* Generate an OG image and return as buffer (for caching)
*/
export async function generateOGImage(params: OGImageParams): Promise<ArrayBuffer> {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY!
},
body: JSON.stringify(params)
});
if (!response.ok) {
throw new Error(`OG Image generation failed: ${response.status}`);
}
return response.arrayBuffer();
}
App Router (Next.js 13+)
The App Router uses the generateMetadata function for dynamic meta tags.
Basic Usage
import { Metadata } from 'next';
import { getOGImageUrl } from '@/lib/og-image';
// Fetch your blog post data
async function getPost(slug: string) {
// Your data fetching logic
return { title: 'Post Title', excerpt: 'Post excerpt...', author: 'Author' };
}
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug);
const ogImage = getOGImageUrl({
title: post.title,
subtitle: `By ${post.author}`,
theme: 'dark'
});
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
images: [{
url: ogImage,
width: 1200,
height: 630,
alt: post.title
}]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [ogImage]
}
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
{/* ... */}
</article>
);
}
Pages Router (Next.js 12)
For the Pages Router, use getStaticProps or getServerSideProps combined with the next/head component.
import Head from 'next/head';
import { getOGImageUrl } from '@/lib/og-image';
interface Post {
title: string;
excerpt: string;
author: string;
}
interface Props {
post: Post;
ogImage: string;
}
export async function getStaticProps({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
const ogImage = getOGImageUrl({
title: post.title,
subtitle: `By ${post.author}`,
theme: 'dark'
});
return {
props: { post, ogImage },
revalidate: 3600 // Revalidate every hour
};
}
export async function getStaticPaths() {
// Your paths logic
return { paths: [], fallback: 'blocking' };
}
export default function BlogPost({ post, ogImage }: Props) {
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
{/* Open Graph */}
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.title} />
<meta name="twitter:description" content={post.excerpt} />
<meta name="twitter:image" content={ogImage} />
</Head>
<article>
<h1>{post.title}</h1>
{/* ... */}
</article>
</>
);
}
Advanced: Caching & Optimization
For high-traffic sites, cache generated images on your own domain:
import { NextRequest, NextResponse } from 'next/server';
import { generateOGImage } from '@/lib/og-image';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title') || 'Untitled';
const subtitle = searchParams.get('subtitle') || undefined;
const theme = (searchParams.get('theme') || 'dark') as 'dark' | 'light';
try {
const imageBuffer = await generateOGImage({
title,
subtitle,
theme
});
return new NextResponse(imageBuffer, {
headers: {
'Content-Type': 'image/png',
// Cache for 1 year (images are deterministic)
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to generate image' },
{ status: 500 }
);
}
}
Then use your own endpoint:
const ogImage = `${process.env.NEXT_PUBLIC_SITE_URL}/api/og?title=${encodeURIComponent(title)}`;
Testing Your Images
Use these tools to verify your OG images work correctly:
- Twitter Card Validator
- Facebook Sharing Debugger
- LinkedIn Post Inspector
- OpenGraph.xyz — Preview on all platforms
Ready to Add Dynamic OG Images?
Get started with 25 free images per month. No credit card required.
Get Your API Key →