generateImageMetadata

Produce several images from one icon or opengraph-image file, each with its own id, size and alt.

Signature
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

ParameterTypeDescription
$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.
$idPromise<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 params handed to generateImageMetadata is a plain object; the params and id handed to the default export are Promises. Awaiting the wrong one, or not awaiting the right one, is the usual bug.
  • id comes back as whatever type you returned — numeric indexes stay numbers.
  • size here only sets the metadata; the ImageResponse still renders at its own dimensions unless you pass the same values through.

Example

Example · typescript
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>.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.