ScreenshotNeo

BlogHow-to

How to Generate Open Graph Images in Next.js

Generate route-specific Open Graph images in Next.js with static files or `ImageResponse`. See runnable App Router examples, metadata options, troubleshooting, and ways to inspect the result.

By the ScreenshotNeo team29 September 202611 min read

How to Generate Open Graph Images in Next.js

To generate Open Graph images in Next.js App Router, add an opengraph-image.tsx file to the route segment and return an ImageResponse from next/og. Use a static opengraph-image.jpg when the same finished image should represent every page in a segment. Use a generated image when its title, label, or design depends on route data. Next.js emits the corresponding image metadata for these file conventions. Next.js file convention documentation.

This guide focuses on the App Router. The examples use current promise-based route parameters; verify the signature against the Next.js version installed in your project, especially if you use Next.js 16, which changed image generation props to promises.

1. Choose a static image or a generated image

Both approaches attach an Open Graph image to a route through its location in the app directory. Choose based on whether the image needs to vary by page.

A parent segment can supply a shared image, while a nested route can define a more specific one.
A parent segment can supply a shared image, while a nested route can define a more specific one.
Need Use Example location
One existing image for a whole route segment Static image file app/blog/opengraph-image.jpg
An image composed from a post slug or page data Generated image route app/blog/[slug]/opengraph-image.tsx
Several generated image variants for a route generateImageMetadata and an image generator that receives an ID See the Next.js API reference and version notes

A child route can provide a more specific image than its parent. For example, a blog-level image can act as a default while a post folder generates a post-specific image. Next.js documents file-based metadata as taking precedence over values in metadata or generateMetadata, so check for convention files when an explicitly configured image does not appear as expected. See the metadata and OG images guide and generateMetadata reference.

2. Add a static Open Graph image

For a fixed design, place a supported image in the segment. Next.js recognizes formats including JPG, JPEG, PNG, and GIF. For example:

app/
  blog/
    opengraph-image.jpg
    page.tsx
    first-post/
      page.tsx
      opengraph-image.png

The image at app/blog/opengraph-image.jpg is associated with the blog segment. The file inside first-post is more specific for that route. You do not need to manually construct a metadata image URL for these convention files.

You can supply descriptive alt text for a static image with a sibling text file named opengraph-image.alt.txt. Keep the description concise and relevant to the image rather than repeating page metadata. The file convention reference documents the supported metadata and image size limit: an Open Graph image file must not exceed 8 MB or the build fails. Twitter image files have a separate 5 MB limit.

3. Generate a dynamic image with ImageResponse

Create opengraph-image.tsx in the segment for the pages that need generated output. Import ImageResponse from next/og, export the image metadata, and return JSX with inline styles. This example makes the image depend on a blog post slug.

A generated image can use route data to compose a page-specific cover.
A generated image can use route data to compose a page-specific cover.
import { ImageResponse } from 'next/og'

export const alt = 'A blog post cover image'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

type Props = {
  params: Promise<{ slug: string }>
}

export default async function Image({ params }: Props) {
  const { slug } = await params
  const title = slug.replaceAll('-', ' ')

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'space-between',
        width: '100%',
        height: '100%',
        padding: '64px',
        background: '#f4f1e9',
        color: '#172321',
        fontSize: 56,
        fontWeight: 700,
      }}
    >
      <div style={{ fontSize: 24, color: '#43645c' }}>Engineering blog</div>
      <div>{title}</div>
      <div style={{ fontSize: 24, fontWeight: 400 }}>Example site</div>
    </div>,
    { ...size }
  )
}

The route for app/blog/[slug]/opengraph-image.tsx receives the slug corresponding to the post route. This example only formats the slug; a production application can look up a title and other data using its own content layer. If the post is missing, decide how your application should handle that case, such as providing a generic title or returning the framework’s appropriate not-found response. Do not assume every URL slug maps to a valid post.

The alt, size, and contentType exports describe the image for metadata generation. Keep the returned dimensions consistent with size. The official examples use 1200 × 630 pixels. The image response option is spread from the exported dimensions in the example so the declared size and rendered response configuration stay aligned.

Use actual page data

When the image should show a post title, load the same canonical data used by the page. Keep the data lookup deterministic and ensure it can run in the image route’s execution context. A simplified shape is:

import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'

export const alt = 'Article cover image'
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 getPostBySlug(slug)
  const title = post?.title ?? 'Article'

  return new ImageResponse(
    <div style={{
      display: 'flex',
      width: '100%',
      height: '100%',
      padding: 64,
      alignItems: 'center',
      background: '#fff',
      color: '#111',
      fontSize: 54,
      fontWeight: 700,
    }}>
      {title}
    </div>,
    { ...size }
  )
}

getPostBySlug here is an application-specific function, not a Next.js API. Implement it with your project’s content source. If a page can change after publication, consider whether the image should update with it and whether your data access and route caching behavior support that expectation. Very long titles should be tested with your chosen design: wrap, truncate deliberately, or use a controlled font-size rule so text remains inside the image.

4. Configure metadata without a file convention

You can set Open Graph image URLs through a route’s metadata export or generateMetadata. This is useful when you already host a finished image elsewhere or want metadata tied to loaded page data.

import type { Metadata } from 'next'

export const metadata: Metadata = {
  openGraph: {
    images: [
      {
        url: 'https://example.com/social/blog-cover.png',
        width: 1200,
        height: 630,
        alt: 'Illustrated cover for the engineering blog',
      },
    ],
  },
}

For dynamic pages, return the equivalent openGraph.images structure from generateMetadata after loading the page record. The field accepts image URLs and optional dimensions and alt text. If a file-based opengraph-image exists in the route tree, it has higher priority according to the Next.js metadata reference. Remove or relocate an unintended convention file if you need the metadata configuration to control the image.

5. Put the image in the right route segment

Route placement determines which pages receive the image and which image wins where files are nested. Work through these checks:

  1. Map the public URL to its App Router segment. For /blog/my-post, that might be app/blog/[slug].
  2. Put the generator or static file in that segment if only those pages should use it.
  3. Place a general default higher in the tree if it should apply to a group of pages.
  4. Inspect nested segments for a more specific image that may override the default.
  5. Check the generated HTML head and the image URL emitted by Next.js after running the app or deploying it.

Layouts and metadata can be composed through the route tree, but image file conventions have priority over metadata configuration. This distinction explains many cases where changing openGraph.images appears to have no effect.

6. Generate multiple image variants

Use generateImageMetadata when one route needs multiple image variants, such as separate designed covers. The function returns metadata objects; each variant needs an ID that is passed to the image generation function. Consult the generateImageMetadata reference for the precise shape.

Version matters here: the current reference records that Next.js 16.0.0 changed the id and params props passed to image generation to promises. Older project versions may use different signatures. Check the installed package version and the documentation matching it before copying a multi-variant example. Do not silently mix a version’s older prop types with a newer function signature.

7. Test what Next.js actually emits

Do not stop at a successful build. Verify both that the image route responds and that the page head points to it.

  1. Run the application and open a representative route.
  2. View page source or inspect the rendered document head in browser developer tools.
  3. Find the og:image metadata and open its URL directly.
  4. Check the response is an image with the expected dimensions and content, not an error page.
  5. Test a parent route, a nested route, and an invalid slug if your generator uses dynamic data.
  6. Repeat against the deployed site because deployment configuration and available data can differ from local development.

Generated image routes are cached and statically optimized by default unless they use Dynamic APIs, uncached data, or dynamic route configuration. That behavior affects when route data is read and when a generated result may be reused. The Next.js docs do not provide a performance comparison between static and generated images, so choose based on required behavior and measure your own deployment if latency or rendering cost matters.

Or skip the browser setup

If your goal is to capture an existing rendered web page as an image, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. For Open Graph images built from custom page data, the Next.js route above gives you direct control over the design; ScreenshotNeo is an option when a screenshot of the rendered page is the desired output.

See the ScreenshotNeo API documentation for request options. This cURL example saves a WebP screenshot of a page:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Equivalent Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Equivalent Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
})
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()))

In a Node.js project without Bun, write the response bytes using the built-in node:fs/promises module:

import { writeFile } from 'node:fs/promises'

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
})
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))

ScreenshotNeo accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses say the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card required.

Options to consider for generated images

Keep the image route deliberately simple and reliable. Its output may be requested when a social platform or another consumer fetches page metadata, so the route should not depend on fragile, interactive browser state.

  • Dimensions: export a size object and pass the same values to ImageResponse. The documented examples use 1200 × 630.
  • Content type: export contentType that matches the response you return. The examples above declare PNG.
  • Alt text: use the alt export for generated output, or the sibling alt text file for a static image.
  • Design: use inline styles and supported JSX for layout; keep content legible at the image’s rendered size.
  • Data: handle missing records, unusually long titles, and characters that need appropriate rendering.
  • Variants: use generateImageMetadata when you need distinct versions, and confirm the installed Next.js signature.
  • Metadata route: use openGraph.images for hosted image URLs when a file convention is not the desired mechanism.

Common errors and fixes

Symptom Likely cause What to check
Build fails after adding a static image Open Graph file exceeds the documented 8 MB limit Reduce the asset size and rebuild.
The browser shows no expected og:image Image file is in a different segment, or another nested file wins Map the URL to its route folder and inspect convention files in parent and child segments.
Changing openGraph.images has no visible effect A file-based metadata image has higher priority Search the route tree for opengraph-image and choose one metadata mechanism intentionally.
TypeScript rejects params or id The example signature does not match the installed Next.js version Check the installed framework version and its matching file-convention or image metadata docs; Next.js 16 uses promise-based props.
Image renders a fallback title or fails on certain posts Slug lookup returned no record or data access failed Confirm the route param, data source, and not-found/fallback behavior.
Text is clipped or unreadable Title length or layout differs from the assumed content Test short and long titles, add wrapping or truncation, and adjust font size or spacing.
Image route errors only after deployment Deployed runtime, data availability, or dynamic behavior differs from local setup Open the image URL directly, inspect server logs, and verify route data and caching conditions in that environment.

Performance, reliability, and cost

Static files and generated routes solve different content needs. The documentation confirms generated routes are cached and statically optimized by default, with dynamic APIs, uncached data, or route configuration affecting that behavior. It does not establish that one method is always faster, so avoid assuming generation is costly or static files are universally faster without measuring your own setup.

For reliability, make the rendering inputs predictable. Keep data lookup bounded, supply a fallback for missing content where appropriate, and ensure image dimensions and metadata match the response. Validate a representative route after deploy, not just during local development. If you use changing source data, decide how freshness and caching should work before publishing.

There is no separate Next.js image-generation price stated in the cited documentation. Your practical cost depends on the infrastructure and data services used by your application. The documented constraints to plan around are the generated route’s runtime/caching behavior and the 8 MB maximum Open Graph image file size.

FAQ

Where do I put opengraph-image.tsx?

Put it in the App Router segment whose pages should receive the generated image, such as app/blog/[slug]/opengraph-image.tsx for individual blog posts.

Can I use an existing image instead of generating one?

Yes. Put a supported static image file named opengraph-image with its extension in the relevant segment.

Does the image file override metadata configuration?

Yes. The Next.js generateMetadata reference says file-based metadata has higher priority than the metadata object and generateMetadata.

What dimensions should I use?

The current documentation examples use 1200 × 630 pixels. Export and pass consistent dimensions for a generated response.

Can a route have more than one generated image?

Yes. The generateImageMetadata API supports multiple image metadata objects, each associated with an ID. Check the installed Next.js version for the correct prop signature.

Official references