twitter-image

Set a route segment's X card image from a static file or a server-rendered ImageResponse.

Signature
export default function Image(props: { params?: Promise<Record<string, string | string[]>>; id?: Promise<string> }): Response | Promise<Response>
  • Added in Next.js 13.3.0

Parameters

ParameterTypeDescription
$paramsoptionalPromise<Record<string, string | string[]>>Promise resolving to the dynamic route parameters from the root segment down to the segment this file sits in. Undefined when no segment above it is dynamic.
$idoptionalPromise<string>Only passed when the file also exports generateImageMetadata. Resolves to the id of the image currently being generated.
$altoptionalstringNamed export. Alt text emitted as twitter:image:alt. For a static image file, put the same text in twitter-image.alt.txt beside it.
$sizeoptional{ width: number; height: number }Named export. Emitted as twitter:image:width and twitter:image:height. Spread it into the ImageResponse options so the bitmap matches.
$contentTypeoptionalstringNamed export. Image MIME type emitted as twitter:image:type, for example 'image/png'.

Return value

ResponseA Response carrying the image bytes. An ImageResponse from next/og satisfies this type.

Errors

The build fails if a static twitter-image file exceeds 5MB — a tighter limit than opengraph-image's 8MB.

Changelog

  • PHP 16.0.0params is now a promise that resolves to an object, so it must be awaited.
  • PHP 13.3.0opengraph-image and twitter-image conventions introduced.

twitter-image is opengraph-image with a different set of meta tags. Same file types, same alt/size/contentType exports, same ImageResponse rendering — but Next.js writes twitter:image instead of og:image.

When to reach for it

  • X needs a different aspect ratio from the 1.91:1 card the rest of the web gets.
  • You want handle-specific wording, or a card without text that would be cropped.
  • A campaign runs on X only and should not change the Open Graph card.

What trips people up

Most sites do not need this file. X's crawler already falls back to og:image when no twitter:image is present, so adding one is a deliberate choice, not a requirement — and once you add it you now have two templates to keep in sync.

The size limit is 5MB, lower than Open Graph's 8MB, and exceeding it fails the build rather than warning. The image alone does not pick a card style either: summary_large_image comes from the twitter.card field in your metadata object, so without it a wide 1200×630 render is displayed as a small square thumbnail.

Example

Example · typescript
import { ImageResponse } from 'next/og'

export const alt = 'Acme — engineering notes'
export const size = {
  width: 1200,
  height: 600,
}
export const contentType = 'image/png'

export default function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          gap: 16,
          padding: 64,
          background: '#111827',
          color: '#f9fafb',
        }}
      >
        <div style={{ fontSize: 64 }}>Acme</div>
        <div style={{ fontSize: 32, opacity: 0.75 }}>@acme · engineering notes</div>
      </div>
    ),
    {
      ...size,
    }
  )
}

When to use it

  • Ship a 2:1 crop for X while the Open Graph card stays 1200x630
  • Add an @handle and post title to the card X users see
  • Override the inherited root card on a single campaign landing page
  • Attach twitter-image.alt.txt so the card is described to screen readers

Frequently asked questions

Do I need a twitter-image if I already have an opengraph-image?

No. X's crawler falls back to og:image when twitter:image is absent, so a single opengraph-image is enough for most sites. Add twitter-image only when the X card should differ in crop, size or wording.

Why does my X card show as a small square instead of a wide image?

Check the emitted twitter:card tag first. Next.js defaults the card to summary_large_image whenever Twitter images are resolved, and a twitter-image file counts, so the convention on its own already produces the wide card. You only need twitter: { card: 'summary_large_image' } in a layout or page metadata export if something else in your metadata set the card to summary or another type. If the tag is already summary_large_image and the preview is still small, the usual causes are X's cached card for that URL (re-scrape it) or an image the crawler cannot fetch.

What size should a twitter-image be in Next.js?

1200x600 (2:1) is the safe choice for summary_large_image, and the file must stay under 5MB or the build fails. Export the same numbers as the size variable so twitter:image:width and twitter:image:height match the actual bitmap.

More examples

Per-post card sharing a template with Open Graph

Both opengraph-image.tsx and twitter-image.tsx import the same Card component, so a design change lands in both cards at once while each file keeps its own size export — 1200x630 for Open Graph, 1200x600 here. `params` must be awaited; it is a promise from Next.js 16 onward.

Example · typescript
// app/blog/[slug]/twitter-image.tsx
import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/posts'
import { Card } from '@/app/lib/card'

export const alt = 'Acme blog post'
export const size = { width: 1200, height: 600 }
export const contentType = 'image/png'

export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)

  return new ImageResponse(<Card title={post.title} handle="@acme" />, {
    ...size,
  })
}

Static image and the tags it produces

Note the tags use name=, whereas Open Graph uses property= — a difference that matters if you are hand-writing them instead of using the convention. Width, height and type are read from the file itself, so re-exporting the PNG at a new size updates the tags with no code change.

Example · typescript
// File layout, no code required:
//
// app
// └── about
//     ├── twitter-image.png        (must stay under 5MB)
//     ├── twitter-image.alt.txt    (contains: About Acme)
//     └── page.tsx
//
// Emitted into <head> for /about:
//
// <meta name="twitter:image" content="<generated>" />
// <meta name="twitter:image:alt" content="About Acme" />
// <meta name="twitter:image:type" content="image/png" />
// <meta name="twitter:image:width" content="1200" />
// <meta name="twitter:image:height" content="600" />

export {}

Choosing the card style in the metadata object

The twitter-image file supplies the picture; this supplies the card type. Without card: 'summary_large_image' the wide render is shown as a cropped square. `metadataBase` matters too — without it the image URL can be emitted as a relative path, which crawlers on other origins cannot resolve.

Example · typescript
// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://acme.com'),
  title: 'Acme',
  twitter: {
    card: 'summary_large_image',
    site: '@acme',
    creator: '@acme',
  },
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Discussion

  • Be the first to comment on this lesson.