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.

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.

| 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.

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:
- Map the public URL to its App Router segment. For
/blog/my-post, that might beapp/blog/[slug]. - Put the generator or static file in that segment if only those pages should use it.
- Place a general default higher in the tree if it should apply to a group of pages.
- Inspect nested segments for a more specific image that may override the default.
- 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.
- Run the application and open a representative route.
- View page source or inspect the rendered document head in browser developer tools.
- Find the
og:imagemetadata and open its URL directly. - Check the response is an image with the expected dimensions and content, not an error page.
- Test a parent route, a nested route, and an invalid slug if your generator uses dynamic data.
- 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
sizeobject and pass the same values toImageResponse. The documented examples use 1200 × 630. - Content type: export
contentTypethat matches the response you return. The examples above declare PNG. - Alt text: use the
altexport 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
generateImageMetadatawhen you need distinct versions, and confirm the installed Next.js signature. - Metadata route: use
openGraph.imagesfor 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.


