generateImageMetadata
Produce several images from one icon or opengraph-image file, each with its own id, size and alt.
export function generateImageMetadata({
params,
}: {
params: { [key: string]: string | string[] }
}): Array<{
id: string | number
alt?: string
size?: { width: number; height: number }
contentType?: string
}>- Added in Next.js v13.3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$paramsoptional | { [key: string]: string | string[] } | Dynamic route params from the root segment down to the segment the image file sits in, read synchronously. undefined for a non-dynamic segment such as app/shop/icon.js. |
$id | Promise<string | number> | Passed to the file's default export rather than to this function: a promise resolving to the id of the entry being rendered. Its type follows what you returned. |
Return value
Array<{ id: string | number; alt?: string; size?: { width: number; height: number }; contentType?: string }>One entry per image to generate. id is required and is passed to the default image export; alt, size and contentType become the corresponding meta tags and response headers.
Errors
Every returned entry must include an id — without it Next.js has no route to serve that image at.
Changelog
- PHP v16.0.0The id prop given to the image generation function is a Promise resolving to a string or number and must be awaited.
- PHP v16.0.0The params prop given to the image generation function is a Promise; the params argument of generateImageMetadata itself stays synchronous.
- PHP v13.3.0Introduced.
A metadata image file such as icon.tsx or opengraph-image.tsx normally produces exactly one image. Export generateImageMetadata from the same file and it produces one per entry you return, calling the default export once for each id. Next.js writes the matching <link rel="icon" sizes="..."> or og:image tags for you, so the sizes never drift out of sync with the files.
When to reach for it
Two situations: a favicon set that needs 48px, 72px and 192px variants from one drawing, and a route whose Open Graph images come from data — one card per gallery image, per chapter, per pricing tier.
What trips people up
- The
paramshanded togenerateImageMetadatais a plain object; theparamsandidhanded to the default export are Promises. Awaiting the wrong one, or not awaiting the right one, is the usual bug. idcomes back as whatever type you returned — numeric indexes stay numbers.sizehere only sets the metadata; theImageResponsestill renders at its own dimensions unless you pass the same values through.
Example
import { ImageResponse } from 'next/og'
export function generateImageMetadata() {
return [
{
contentType: 'image/png',
size: { width: 48, height: 48 },
id: 'small',
},
{
contentType: 'image/png',
size: { width: 72, height: 72 },
id: 'medium',
},
]
}
export default async function Icon({ id }: { id: Promise<string | number> }) {
const iconId = await id
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 88,
background: '#000',
color: '#fafafa',
}}
>
Icon {iconId}
</div>
)
)
}When to use it
- Emit 48px, 72px and 192px favicons from one JSX drawing.
- Generate one Open Graph card per image in a product gallery.
- Render a differently sized Twitter card and OG card from the same template.
- Produce per-chapter share images for a docs route keyed on its slug.
Frequently asked questions
How do I generate multiple favicon sizes in the App Router?
Export generateImageMetadata from app/icon.tsx returning one entry per size, each with an id and a size object, and read the id in the file's default export to draw at that scale. Next.js emits one <link rel="icon"> per entry with the sizes attribute filled in, so you never hand-maintain the tags.
Why is my id undefined in the image function?
Since Next.js 16 the id prop is a Promise and has to be awaited — reading it directly gives you a pending promise, not the value. The same applies to params in the default export, though not to the params argument of generateImageMetadata itself, which stays synchronous.
Can I use generateImageMetadata with opengraph-image and twitter-image?
Yes — it works in any metadata image file, including icon, apple-icon, opengraph-image and twitter-image. Each entry produces one image and one corresponding tag, so an opengraph-image file returning three entries emits three og:image tags on that route.
More examples
Open Graph cards driven by external data
For a product with three gallery images, the head of /products/42 carries three og:image tags, each with its own og:image:alt and 1200x600 dimensions. Note the asymmetry: params inside generateImageMetadata is a plain object (params.id), while inside the Image function both params and id are Promises. Because id was returned as a number, it arrives as Promise<number>.
// app/products/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getCaptionForImage, getOGImages } from '@/app/utils/images'
export async function generateImageMetadata({
params,
}: {
params: { id: string }
}) {
const images = await getOGImages(params.id)
return images.map((image, idx) => ({
id: idx,
size: { width: 1200, height: 600 },
alt: image.text,
contentType: 'image/png',
}))
}
export default async function Image({
params,
id,
}: {
params: Promise<{ id: string }>
id: Promise<number>
}) {
const productId = (await params).id
const imageId = await id
const text = await getCaptionForImage(productId, imageId)
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
fontSize: 64,
background: '#fff',
}}
>
{text}
</div>
)
)
}A favicon set sized from one table
Three link tags are emitted, one per entry, with sizes taken from the size field. The second argument to ImageResponse is what actually sets the raster dimensions — the size in the returned entry only writes the sizes attribute. Leaving them out of sync produces a 192x192 tag pointing at a 1200x630 image, which is the most common report against this API.
// app/icon.tsx
import { ImageResponse } from 'next/og'
const SIZES = [
{ id: '32', px: 32 },
{ id: '96', px: 96 },
{ id: '192', px: 192 },
]
export function generateImageMetadata() {
return SIZES.map(({ id, px }) => ({
id,
contentType: 'image/png',
size: { width: px, height: px },
alt: `Acme logo, ${px} pixels`,
}))
}
export default async function Icon({ id }: { id: Promise<string> }) {
const px = Number(await id)
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: px * 0.7,
background: '#111',
color: '#fff',
}}
>
A
</div>
),
{ width: px, height: px }
)
}
// <link rel="icon" href="/icon/32?<hash>" type="image/png" sizes="32x32" />
// <link rel="icon" href="/icon/96?<hash>" type="image/png" sizes="96x96" />
// <link rel="icon" href="/icon/192?<hash>" type="image/png" sizes="192x192" />
Discussion