Next.js gives you three ways to add an OG image in the App Router. Use a static file for a site with few pages. Use opengraph-image.tsx to render one image for each route. Use a route handler when many pages share one template.
Put an image named opengraph-image.png in the app folder, or in any route folder. Next.js adds the tags for you.
app/
opengraph-image.png → all pages
opengraph-image.alt.txt → alt text for the image
pricing/
opengraph-image.png → only /pricing
The output in the head of the page:
<meta property="og:image" content="https://example.com/opengraph-image.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
The file can be JPG, PNG, or GIF. Use 1200×630 pixels. See the OG image size guide.
To give each blog post its own image, add opengraph-image.tsx next to the page. The file exports a function that returns an ImageResponse. Next.js renders JSX to a PNG with Satori.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPost } from '@/lib/posts'
export const alt = 'Blog post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: 72,
background: '#0a0a0a',
color: 'white',
}}
>
<div style={{ fontSize: 72, fontWeight: 800 }}>{post.title}</div>
<div style={{ fontSize: 32, opacity: 0.7 }}>example.com</div>
</div>
),
size,
)
}
In Next.js 16, params is a promise. Await it before you read the slug.
A route handler is a normal URL that returns an image. The text comes from the query string. Each page then points og:image at that URL. One template serves the full site, and other sites can use it too.
// app/og/route.tsx
import { ImageResponse } from 'next/og'
import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const title = request.nextUrl.searchParams.get('title') ?? 'Acme'
return new ImageResponse(
(
<div tw="flex h-full w-full items-center justify-center bg-black p-20 text-7xl font-bold text-white">
{title}
</div>
),
{
width: 1200,
height: 630,
headers: { 'Cache-Control': 'public, max-age=3600, immutable' },
},
)
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
openGraph: {
images: [`/og?title=${encodeURIComponent(post.title)}`],
},
twitter: { card: 'summary_large_image' },
}
}
The ogimage.org templates use this pattern. There are nine routes: headline, blog post, screenshot, emoji, and more. The source is on GitHub under the MIT license.
Platforms need an absolute image URL. Set metadataBase in the root layout, and Next.js adds the domain to each relative path.
// app/layout.tsx
export const metadata = {
metadataBase: new URL('https://example.com'),
}
display: grid does not work. An element with more than one child needs display: flex.tw prop, not through className.Open the image URL in the browser first. Then deploy and run the page through the OG image checker. It reads the tags as a crawler does and measures the image.
If you only need one image, the OG image generator is faster than code. For ideas, see the developer tool examples in the gallery.