You've built your Next.js app. It's fast, it looks great. Then you share a link on Twitter and... a grey box appears where the preview image should be. Every developer has been here.
Open Graph images are the difference between a link that gets ignored and one that gets clicked. This tutorial will walk you through generating dynamic, per-page OG images for your Next.js app using Imago API — with real, copy-paste code for both the App Router and the Pages Router.
By the end of this tutorial, every page in your app will have a unique, branded OG image generated from the page's title and description. No Figma, no templates, no design work.
We'll build a helper that takes any page's title and description, calls the Imago API, and returns the URL of a generated OG image. Then we'll wire it up to Next.js metadata so every page gets its own preview image automatically.
The result: when someone shares any page from your app, they'll see a clean, branded card with the page title and description — generated on-the-fly, no manual image creation required.
Sign up at imagoapi.com/pricing
and grab your API key. The free tier gives you 100 images per month — more than enough for testing
and small projects. Add it to your .env.local:
IMAGO_API_KEY=imago_your_api_key_here
Create a utility file that wraps the Imago API. We'll use this from both route handlers and server components:
// lib/og-image.ts
export interface OGImageOptions {
title: string;
description?: string;
brandColor?: string;
template?: 'gradient' | 'solid' | 'minimal';
}
export async function generateOGImage(options: OGImageOptions): Promise<string | null> {
const apiKey = process.env.IMAGO_API_KEY;
if (!apiKey) {
console.warn('IMAGO_API_KEY not set — skipping OG image generation');
return null;
}
try {
const res = await fetch('https://imagoapi.com/api/og/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: apiKey,
title: options.title,
description: options.description ?? '',
brand_color: options.brandColor ?? '#6366F1',
template: options.template ?? 'gradient',
}),
// Cache for 24h — don't regenerate the same image every request
next: { revalidate: 86400 },
});
if (!res.ok) return null;
const data = await res.json();
return data.image?.url ?? null;
} catch {
return null;
}
}
next: { revalidate: 86400 } fetch
option so the same title+description combo only generates a new image once per day. This
keeps your API usage low and your pages fast.
In Next.js 13+ with the App Router, you export a generateMetadata function
from your page. This runs on the server and can call async APIs:
import { generateOGImage } from '@/lib/og-image';
import type { Metadata } from 'next';
interface Props {
params: { slug: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Fetch your post data (replace with your actual data source)
const post = await getPost(params.slug);
const ogImageUrl = await generateOGImage({
title: post.title,
description: post.excerpt,
brandColor: '#6366F1',
});
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: ogImageUrl ? [{ url: ogImageUrl, width: 1200, height: 630 }] : [],
type: 'article',
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: ogImageUrl ? [ogImageUrl] : [],
},
};
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
return <article>{/* your post content */}</article>;
}
If you're on the Pages Router, generate the OG image URL inside getStaticProps
or getServerSideProps and pass it as a prop:
import Head from 'next/head';
import { generateOGImage } from '@/lib/og-image';
import type { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await getPost(params!.slug as string);
const ogImageUrl = await generateOGImage({
title: post.title,
description: post.excerpt,
});
return { props: { post, ogImageUrl }, revalidate: 86400 };
};
export default function BlogPost({ post, ogImageUrl }) {
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
{ogImageUrl && (
<>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImageUrl} />
</>
)}
</Head>
<article>{/* your post content */}</article>
</>
);
}
Run your app and check the generated HTML:
npm run dev
curl http://localhost:3000/blog/my-post | grep og:image
You should see something like:
<meta property="og:image" content="https://imagoapi.com/generated/abc123.png" />
To verify the social card preview, use the Twitter Card Validator or opengraph.xyz.
A few things worth knowing before you ship:
revalidate: 86400, Next.js regenerates the image at most once a day.generateMetadata is server-side only
and happens before HTML is sent, so it's safe — just keep the cache warm.null on error.
Add a static fallback image for when the API is unavailable.process.env.IMAGO_API_KEY.For production, add a static fallback image so your OG tags are never empty:
const FALLBACK_OG_IMAGE = 'https://imagoapi.com/og/default.png'; // your static fallback
export async function getOGImage(options: OGImageOptions): Promise<string> {
const generated = await generateOGImage(options);
return generated ?? FALLBACK_OG_IMAGE;
}
Every page in your Next.js app now gets a unique, branded OG image — generated from the page title and description, no design work required. When you publish a new blog post or add a new product, the image is generated automatically.
The total integration time for this is under 15 minutes. Compare that to maintaining a Figma template, exporting images manually, or uploading custom images for every page.
Get 100 free images per month. No credit card required.
Get Your Free API Key →