The opengraph-image and twitter-image conventions let you set a social card by putting a file in a route segment. Next.js reads the file at build time and writes the image URL, type, dimensions, and alt text into that route's tags. There is nothing to keep in sync by hand.
Both conventions accept either an image file or a module that generates one. The behavior below was verified with Next.js 16.3 in an App Router project, reading tags from a production build that uses the default caching model, with the Cache Components flag off.
Drop in a static image
The fastest version is a real image file at the app root, which covers every route in the project. Name it after the convention, keep it next to the layout, and let Next.js do the rest.
app/
opengraph-image.png
layout.tsx
page.tsxRequesting the home page now returns Open Graph tags for that image, including a cache-busting query string in the URL and the width and height read from the file itself. A request to a deeper route such as /docs returns the same tags, because the file applies to the segment it lives in and everything below.
Four file types are accepted for both conventions.
| Convention | File types |
|---|---|
| opengraph-image | jpg, jpeg, png, gif |
| twitter-image | jpg, jpeg, png, gif |
| opengraph-image.alt | txt |
| twitter-image.alt | txt |
The alt files are worth using. A card without alt text is unreadable to anyone using a screen reader on the platform that renders it.
Acme Store home page cardSave that as app/opengraph-image.alt.txt next to the image, and its contents become the image alt attribute in the rendered tags. There is no other way to give a static image file alt text through this convention.
Overriding the image for one section
A deeper file wins for its own subtree. This is how a blog gets one card and the marketing site keeps another, with no configuration.
app/
opengraph-image.png
blog/
opengraph-image.pngRequests to /blog and everything under it now use the blog image, while the rest of the site keeps the root one. The rule is proximity, so the closest file to the rendered page wins.
Twitter images follow the same placement rules and are only needed when the card should differ. Without one, Next.js mirrors the Open Graph image into the Twitter tags, dimensions and alt text included, which is covered in Open Graph and Twitter card metadata.
Generating the image with code
Replacing the image with a module of the same name switches to generated images. The module default-exports a function and can export metadata about the result.
// app/shop/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const alt = 'Acme Shop'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
const card = { width: '100%', height: '100%', display: 'flex', background: '#111' }
export default function Image() {
return new ImageResponse(<div style={card}>Acme Shop</div>, size)
}The build now lists /shop/opengraph-image as its own route, prerendered when nothing about it depends on the request. The exported alt, size, and content type become the corresponding meta tags, replacing what the alt text file and file inspection would have provided.
Only one of the two forms can exist per segment, so pick the file for a fixed design and the module when the card carries data. The mechanics of the generated version are covered in generating dynamic OG images.
Precedence against the metadata object
This is where the conventions surprise people. A file does not always win, and the rule depends on which segment declares what.
The verified behavior falls into three cases:
- The segment sets an images value in its metadata: that value wins and the file is ignored, whether it came from a static export or from the metadata function.
- The segment sets an Open Graph object without images: the file wins and supplies the image.
- The segment sets nothing: the file wins over any image inherited from a parent.
A child that sets only an Open Graph title replaces the whole inherited object, so an image inherited from a parent segment disappears while the child's own image file still applies. Read the rendered tags after any per-route override.
There is one useful interaction with generateMetadata. The resolved parent metadata already includes the image file that applies to the route, so spreading the parent images into your own array keeps the generated card rather than replacing it.
Multiple images from one file
A single segment can produce several images through a companion export. Each entry needs an id, and that id is passed to the image function as a promise.
// app/shop/opengraph-image.tsx
export function generateImageMetadata() {
return [
{ id: 'wide', size: { width: 1200, height: 630 }, alt: 'Acme Shop' },
{ id: 'square', size: { width: 600, height: 600 }, alt: 'Acme Shop' },
]
}The image function then awaits the id and branches on it, and the page renders one Open Graph image tag per entry. Each entry can carry its own size, alt text, and content type, so the two cards do not have to share a design.
Use this when a platform genuinely needs a different aspect ratio. Every extra entry is another route to build and another image for a crawler to fetch, and most sites do fine with one well-made card per route.
Common mistakes
Each of these looks fine locally and fails where you cannot see it.
- Putting the card in the public folder and linking it by hand, which skips the generated dimensions and alt text.
- Exceeding the platform size limits, which fails the build rather than shipping a card that will not load.
- Adding an image file to a segment whose metadata already sets an images value, so the file is quietly ignored.
- Forgetting the alt text file next to a static image.
- Assuming the file at the app root still applies after a child page declares its own Open Graph object.
Rune AI
Key Insights
- opengraph-image and twitter-image accept jpg, jpeg, png, and gif files, or js, ts, and tsx modules that generate one.
- A file covers its own segment and everything below it, and a deeper file wins.
- A sibling file ending in .alt.txt supplies the alt text for a static image.
- Twitter tags are mirrored from the Open Graph image unless a Twitter image exists.
- An images value set in the same segment's metadata takes precedence over the file.
Frequently Asked Questions
Does one image at the app root cover every route?
Do I need a separate twitter-image?
Why did my image disappear on one page?
How do I add alt text to a static image file?
Conclusion
The image file conventions are the least error-prone way to ship social cards, because the tags are generated from the file itself rather than kept in sync by hand. Put a default at the app root, override it in the segments that deserve their own card, and remember that an explicit images value in metadata wins over the file in the same segment.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.