In a current Next.js App Router project, add app/opengraph-image.png (or .jpg, .jpeg, or .gif) for a static image, or create app/opengraph-image.tsx and return new ImageResponse() for a generated image. Next.js discovers these special files and emits the Open Graph metadata for the route automatically. Use metadata.openGraph.images instead when an image already exists at an absolute URL.
Choose the right Next.js Open Graph method
Open Graph (OG) images are the previews shown when a URL is shared on platforms such as social networks and chat applications. The App Router offers four patterns:
| Pattern | Best for | How Next.js finds it |
|---|---|---|
| Static special file | One prepared image reused by a route or section | opengraph-image.jpg, .jpeg, .png, or .gif in app or a route segment |
| Generated special file | Branded or data-driven images rendered at request/build time | opengraph-image.tsx returning ImageResponse |
| Metadata URL | An image already hosted elsewhere | metadata.openGraph.images with an absolute URL |
| Multiple generated variants | Several OG images for one segment | generateImageMetadata plus a generated image function |
The examples below follow the App Router metadata conventions documented by Next.js. They do not apply the same way to the legacy Pages Router.
Add a static Open Graph image
Site-wide default
Put the file directly in the app directory:
app/opengraph-image.png
Next.js uses the file to create og:image metadata and derives the image type, width, and height. A static alternative text file can sit beside it:
#1 Best Overall
app/opengraph-image.alt.txt
For a section, place the asset in that segment’s folder:
app/blog/opengraph-image.jpg
Segment-level files override a higher-level file. Thus, a post under app/blog/[slug] can have its own app/blog/[slug]/opengraph-image.png, which takes precedence over app/blog/opengraph-image.jpg and the root image. This lets you define a default once and replace it only where needed.
Static-file checklist
- Use one of the supported extensions:
.jpg,.jpeg,.png, or.gif. - Keep the Open Graph file at or below the documented 8 MB limit. Twitter image files have a separate 5 MB limit.
- Place
opengraph-image.alt.txtnext to the image when static alt text is required. - Deploy the route and inspect the generated HTML for an absolute
og:imageURL.
Generate an image with opengraph-image.tsx
Create this file in the route segment that should own the image. The default export returns an ImageResponse from next/og; the named exports describe the generated metadata.
import { ImageResponse } from 'next/og'
export const alt = 'About Acme'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function Image() {
return new ImageResponse(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
About Acme
</div>,
)
}
The official example uses 1200 × 630 pixels, a broadly accepted sharing ratio. ImageResponse supports flexbox and a subset of CSS properties; it is not a general browser renderer, so CSS Grid and arbitrary browser features are not available in the documented renderer.
Recommended Free Tools
Rank #2
Use route parameters for post-specific cards
Put the generated file inside a dynamic segment, such as app/blog/[slug]/opengraph-image.tsx. In current Next.js 16 documentation, params resolves to a promise:
import { ImageResponse } from 'next/og'
type Props = { params: Promise<{ slug: string }> }
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({ params }: Props) {
const { slug } = await params
const title = slug.replaceAll('-', ' ')
return new ImageResponse(
<div style={{ display: 'flex', fontSize: 64, padding: 48 }}>
{title}
</div>,
)
}
Replace the slug transformation with a database or CMS lookup when you need the real post title. Keep that lookup reliable and fast; a failed data request can make the image route fail.
Point metadata at an existing image URL
If another service already hosts the image, export typed metadata from the route:
import type { Metadata } from 'next'
export const metadata: Metadata = {
openGraph: {
images: [
{
url: 'https://example.com/og.png',
width: 1200,
height: 630,
alt: 'Example article preview',
},
],
},
}
Each openGraph.images URL must be absolute, including the scheme and hostname. Relative paths such as /og.png do not satisfy this documented requirement. Width, height, and alt are optional metadata fields but are useful to consumers that read them.
Rank #3
Generate multiple OG variants with generateImageMetadata
Use generateImageMetadata when one route segment needs more than one generated image—for example, language or campaign variants. Return an array containing each variant’s id, alt, size, and contentType. The default image function receives the selected id.
import { ImageResponse } from 'next/og'
export function generateImageMetadata() {
return [
{
id: 'light',
alt: 'Light theme preview',
size: { width: 1200, height: 630 },
contentType: 'image/png',
},
{
id: 'dark',
alt: 'Dark theme preview',
size: { width: 1200, height: 630 },
contentType: 'image/png',
},
]
}
export default function Image({ id }: { id: string }) {
const background = id === 'dark' ? '#111827' : '#ffffff'
return new ImageResponse(
<div style={{ display: 'flex', background, color: id === 'dark' ? 'white' : 'black', fontSize: 64 }}>
{id} theme
</div>,
)
}
Understand precedence, caching, and freshness
Precedence follows the folder tree
A more specific image in a deeper route segment wins over an image higher in the tree. This is the key to combining a global default with section and post overrides.
Generated routes are cached by default
Next.js caches generated metadata routes by default. They can become dynamic when they use Dynamic APIs or uncached data. Decide deliberately whether a card should be stable and cacheable or regenerated from fresh content. If you fetch a post title without caching, expect dynamic behavior and account for the added latency and load.
Validate the public result
- Build and deploy the application.
- Open the route’s page source and confirm an absolute
og:imagevalue. - Open the image URL directly to verify it returns the expected content type and dimensions.
- Share a URL in your target platform and allow for that platform’s own preview cache when testing changes.
Common failures and fixes
No og:image appears
Check spelling and location: the convention is exactly opengraph-image with a supported extension, or a correctly named opengraph-image.tsx. Confirm the file is under app rather than an unrelated directory and that the route is using the App Router.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteThe image URL is rejected
For metadata.openGraph.images, change a relative path to a complete absolute URL. Also verify that the deployed host is publicly reachable; private localhost URLs cannot be fetched by social crawlers.
The generated image throws a rendering error
Reduce the component to supported flexbox and documented CSS properties. Remove CSS Grid, unsupported fonts, browser-only APIs, and components that depend on client-side state. Return a valid ImageResponse from the default export.
Dynamic titles are stale
Inspect caching. A generated route is cached unless Dynamic APIs or uncached data make it dynamic. Choose an explicit caching strategy for your data source and redeploy or invalidate the relevant cache after a title change.
The file is too large
Keep opengraph-image output under 8 MB; Twitter images have a 5 MB maximum. Reduce embedded assets, simplify the layout, or choose a more compact format where appropriate.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Fonts or images do not render
Generated images run in a server-side renderer, not a full browser. Treat external assets as dependencies: use stable, reachable URLs or load them in a way supported by your deployment runtime, and test the deployed route rather than relying only on local development.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance and design decisions
- Static versus generated: static files are simplest and avoid render-time data work; generated routes provide per-post branding and text.
- Build-time versus fresh data: cached output is faster and more predictable, while uncached data keeps cards current at the cost of work per request.
- One image versus variants: use one default unless readers genuinely need multiple alternatives;
generateImageMetadataadds flexibility but also more output to maintain. - Layout constraints: design with flexbox and supported properties first. A visually complex web page cannot be copied directly into
ImageResponse.
Or skip the browser setup
If you only need a dependable screenshot of a rendered page, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners as a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and bills only clean shots. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; each response identifies the page verdict and billing status in headers.
One GET request returns PNG, JPEG, WebP, or PDF. The API supports full-page and element capture, device presets and custom viewports, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
See the ScreenshotNeo documentation for parameter details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
Frequently Asked Questions
Can I use both a static file and metadata.images?
Yes, but define a deliberate precedence strategy in your route tree. A more specific special-file image can override a higher-level image, while metadata declarations provide an explicit hosted URL.
What dimensions should a generated OG image use?
The official ImageResponse example uses 1200 × 630 pixels. Keep the same ratio unless the platform or design system requires another size.
Does Next.js render arbitrary CSS in ImageResponse?
No. The documented renderer supports flexbox and a subset of CSS properties, so browser-only layouts such as CSS Grid should be redesigned.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




