ogimage.org
Guide

Next.js OG Image: Static and Dynamic Open Graph Images

Three ways to add an OG image in the Next.js App Router: a static file, an opengraph-image.tsx file, and a route handler with ImageResponse. With code you can copy.

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.

1. A static file

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.

2. A dynamic image with opengraph-image.tsx

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.

3. A route handler that many pages share

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.

Set metadataBase

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'),
}

Limits of ImageResponse

  • Flexbox only. display: grid does not work. An element with more than one child needs display: flex.
  • A subset of CSS. Satori supports the common properties. It does not support all of them.
  • Fonts must be TTF, OTF, or WOFF. WOFF2 does not work.
  • The bundle limit is 500 KB, with fonts and images included. Fetch large assets at runtime.
  • Tailwind works through the tw prop, not through className.

Test the result

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.