Dynamic OG Images for Next.js

Complete step-by-step guide to adding dynamic Open Graph images to your Next.js application. Works with both App Router (Next.js 13+) and Pages Router.

Get API Key → Full Docs

Prerequisites

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:

.env.local
# 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
💡 Tip: Never commit your API key to version control. Add .env.local to your .gitignore.

Create a Utility Function

Create a reusable function for generating OG image URLs:

lib/og-image.ts
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

app/blog/[slug]/page.tsx
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.

pages/blog/[slug].tsx
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:

app/api/og/route.ts
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:

⚠️ Note: Social platforms cache OG images aggressively. Use the debugger tools to force a refresh when testing changes.

Ready to Add Dynamic OG Images?

Get started with 25 free images per month. No credit card required.

Get Your API Key →

More Integration Guides