Type something to search...
Next.js opengraph-image and twitter-image

Next.js opengraph-image and twitter-image

The image that shows up when a link to your site is shared on Slack, Twitter/X, or iMessage isn't magic — it's read from specific <meta> tags (og:image, twitter:image) that your page either has or doesn't. opengraph-image and twitter-image are the file conventions Next.js provides to generate those tags correctly, per route segment, without you hand-authoring meta tags or juggling image dimensions and MIME types yourself.

Static Image Files

Placing a correctly-named image file in any route segment gives that segment its own social preview image, distinct from every other segment:

File conventionSupported file types
opengraph-image.jpg, .jpeg, .png, .gif
twitter-image.jpg, .jpeg, .png, .gif
opengraph-image.alt.txt
twitter-image.alt.txt

Next.js evaluates the file and generates the corresponding tags automatically:

<meta property="og:image" content="<generated>" />
<meta property="og:image:type" content="<generated>" />
<meta property="og:image:width" content="<generated>" />
<meta property="og:image:height" content="<generated>" />
<meta name="twitter:image" content="<generated>" />
<meta name="twitter:image:type" content="<generated>" />
<meta name="twitter:image:width" content="<generated>" />
<meta name="twitter:image:height" content="<generated>" />

A hard file-size limit is worth knowing before you build the rest of your pipeline around a specific image: twitter-image must not exceed 5MB, and opengraph-image must not exceed 8MB — limits imposed by Twitter/X and Facebook respectively, not by Next.js itself. Exceed either, and the build fails outright rather than silently shipping an oversized image — this is a build-time hard stop, not a runtime warning you might miss.

Alt Text as a Sibling File

Rather than an inline prop, alt text for a static image file is its own sibling text file:

About Acme
<meta property="og:image:alt" content="About Acme" />

Same pattern applies identically to twitter-image.alt.txt.

Generating Images with Code

For images that need to reflect actual page content — a blog post's title rendered directly into the preview image, rather than one static generic graphic for the whole site — opengraph-image.tsx or twitter-image.tsx can default-export a function, typically built on the ImageResponse API from next/og:

import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";

export const alt = "About Acme";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

const interSemiBold = await readFile(
  join(process.cwd(), "assets/Inter-SemiBold.ttf"),
);

export default async function Image() {
  return new ImageResponse(
    <div
      style={{
        fontSize: 128,
        background: "white",
        width: "100%",
        height: "100%",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      About Acme
    </div>,
    {
      ...size,
      fonts: [
        { name: "Inter", data: interSemiBold, style: "normal", weight: 400 },
      ],
    },
  );
}

That module-level readFile call for the font — executed once, at module scope, rather than inside the Image function — is a deliberate pattern worth internalizing beyond just this specific example: a local asset that doesn't depend on request data should be read once and reused, not re-read on every invocation. The docs frame this as a "predictable values" caching pattern that applies broadly across Next.js's caching model, not just here.

Reflecting Real Page Data

Combine params with a live data fetch to generate genuinely per-page images:

import { ImageResponse } from "next/og";

export const alt = "About Acme";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetch(`https://.../posts/${slug}`).then((res) =>
    res.json(),
  );

  return new ImageResponse(
    <div
      style={{
        fontSize: 48,
        background: "white",
        width: "100%",
        height: "100%",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      {post.title}
    </div>,
    { ...size },
  );
}

By default this generated image is statically optimized — but because it depends on external data, you control whether it stays static or revalidates by configuring the fetch call's own caching options, or by setting the route segment's revalidate config directly, the same levers you'd use for any other data-dependent static content.

Using Local Image Assets in a Generated Image

If you need an actual raster asset (a logo) embedded inside a generated image rather than pure CSS/text styling, read it from disk and pass it as a base64 data URI:

import { ImageResponse } from "next/og";
import { join } from "node:path";
import { readFile } from "node:fs/promises";

const logoData = await readFile(join(process.cwd(), "logo.png"), "base64");
const logoSrc = `data:image/png;base64,${logoData}`;

export default async function Image() {
  return new ImageResponse(
    <div
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <img src={logoSrc} height="100" />
    </div>,
  );
}

Place the local asset path relative to your project root, not relative to the source file itself — an easy mistake if you're used to relative imports resolving against the file that contains them.

An ArrayBuffer also technically works as an <img src> value here, since the rendering engine behind next/og (Satori) supports it — but because TypeScript's own <img> typings follow the strict HTML spec (which doesn't include ArrayBuffer as a valid src), you'll need a @ts-expect-error comment to silence the type error if you go this route. It's a real, working feature; it's just outside what the type system officially sanctions.

Props

params (optional)

Same shape and behavior as elsewhere in the App Router — a promise resolving to the dynamic segment values from the root down to wherever the file is colocated:

RouteURLparams
app/shop/opengraph-image.js/shopundefined
app/shop/[slug]/opengraph-image.js/shop/1Promise<{ slug: '1' }>
app/shop/[tag]/[item]/opengraph-image.js/shop/1/2Promise<{ tag: '1', item: '2' }>

If you're using generateImageMetadata to produce multiple images from one file, the function also receives an id prop — a promise resolving to the id from the corresponding generateImageMetadata entry.

Returns

The function should return a ResponseImageResponse satisfies this contract directly, which is why it's the recommended default rather than constructing a Response by hand.

Config Exports

OptionType
altstring
size{ width: number; height: number }
contentTypestring (image MIME type)
export const alt = "My images alt text";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default function Image() {}

Each maps directly onto its corresponding meta tag — alt becomes og:image:alt, size becomes og:image:width/og:image:height, contentType becomes og:image:type — again with no manual tag-writing on your part.

Route Segment Config Applies Here Too

opengraph-image and twitter-image are, mechanically, specialized Route Handlers — meaning they accept the same route segment configuration options (dynamic, revalidate, and the rest) as ordinary pages and layouts, and are cached by default under exactly the same rule every other metadata-file Route Handler follows: cached unless a Request-time API or dynamic config forces otherwise.

Version History

VersionChanges
v16.0.0params became a promise resolving to an object
v13.3.0opengraph-image and twitter-image introduced

Key Takeaways

AspectDetail
Static forms.jpg, .jpeg, .png, .gif — plus a sibling .alt.txt for alt text
Size limits8MB for opengraph-image, 5MB for twitter-image — exceeding either fails the build
Dynamic forms.js/.ts/.tsx default-exporting a function, typically using ImageResponse
Local assets in generated imagesRead once at module scope for anything request-independent; path is relative to project root
CachingStatically optimized/cached by default, like other metadata-file Route Handlers
Config exportsalt, size, contentType — map directly onto the corresponding meta tags

The two-tier design here — a dead-simple static file for the common case, a full code-generation path for anything that needs to reflect real content — mirrors the same pattern Next.js uses across its whole metadata-file family. Reach for the static file first; reach for ImageResponse the moment a single fixed image genuinely stops being good enough for every page on your site.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper