Back to blog

OG images

How to Add Dynamic OG Images in Next.js

June 15, 2026 · 4 min read · Grabbit Team

How to Add Dynamic OG Images in Next.js

Next.js has dynamic Open Graph images built in: drop an opengraph-image.tsx file in a route segment and the framework generates the image and wires the meta tags. That covers most cases. The one it does not cover is when you want the OG image to be a real render of the page itself, not a separate JSX layout. This guide shows both: the native @vercel/og approach for templated cards, and a screenshot API for capturing the actual page.

The native approach: opengraph-image

In the App Router, any route segment can export an OG image. A static file is the simplest version:

app/
  blog/
    [slug]/
      opengraph-image.png   <- served automatically as the OG image
      page.tsx

Next.js detects opengraph-image.png and adds the og:image meta tags for that route. No code, no manual tags.

For a dynamic card, use opengraph-image.tsx and return an ImageResponse from next/og:

import { ImageResponse } from 'next/og';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { slug: string } }) {
  const title = await getPostTitle(params.slug);

  return new ImageResponse(
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        background: '#0b1120',
        color: 'white',
        fontSize: 64,
        padding: 80,
      }}
    >
      {title}
    </div>,
    size
  );
}

The size export is what makes this a correct OG image: 1200 by 630 is the 1.91 to 1 ratio every major platform renders as a full-width card. For the full breakdown of why this ratio matters, see Open Graph image sizes.

This is the right tool when the card is a layout you control in JSX: a title, an author, a logo, a gradient. It runs on the edge, it is fast, and the image regenerates whenever the data changes.

Where opengraph-image falls short

@vercel/og renders a subset of CSS in an isolated environment. It does not render your actual page. That is a feature for simple cards, but it becomes a wall when the image you want is the page itself:

  • A dashboard or chart that already looks good and should appear in the preview as it really renders.
  • A page built with components, fonts, or CSS features that the OG renderer does not support, so you would have to rebuild the layout twice.
  • A marketing or docs page where the OG image should just be a clean shot of the top of the page.

In those cases you are no longer generating a card. You are taking a screenshot of a real page, and that is a different tool.

The screenshot approach: capture the real page

A screenshot API renders the actual URL in a real browser and hands back a hosted image. You point it at the page (or a dedicated /og route you build with your normal components) and use the result as your og:image:

curl https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/og/some-post",
    "width": 1200,
    "height": 630,
    "format": "png",
    "delay_ms": 500
  }'

The response includes a hosted image_url:

{
  "id": "grb_01jx...",
  "status": "done",
  "image_url": "https://cdn.grabbit.live/grabs/grb_01jx....png",
  "width": 1200,
  "height": 630,
  "format": "png",
  "bytes": 71240,
  "execution_ms": 1240
}

Width accepts 320 to 1920 and height 240 to 1080, so 1200 by 630 is in range. The delay_ms field (0 to 10000) gives client-rendered content or web fonts a moment to settle before the capture fires. Wire the returned image_url into your metadata:

export async function generateMetadata({ params }): Promise<Metadata> {
  const { ogImageUrl } = await getPost(params.slug);
  return {
    openGraph: {
      images: [{ url: ogImageUrl, width: 1200, height: 630 }],
    },
  };
}

Cache the result: call the API once per unique page, store image_url alongside the page record, and serve it from there so you are not capturing on every request. The same template-and-cache pattern, applied to your real page instead of a JSX card, is covered in how to generate dynamic OG images from any URL.

Which to use

  • Templated card (title, author, brand), no need to match the page: native opengraph-image.tsx with @vercel/og. It ships with Next.js and runs on the edge.
  • The OG image should be a real render of the page or app: a screenshot API. You capture what users actually see, including content @vercel/og cannot reproduce.

Many apps use both: @vercel/og for blog and marketing cards, a screenshot API for the pages that are too rich to rebuild as a card.

Before you ship, confirm the tags resolve and the card looks right by testing the live URL. New to the format? Start with what is an OG image.

FAQ

How do I add an OG image in Next.js App Router?
Add an opengraph-image file to any route segment in the app directory. A static opengraph-image.png is picked up automatically. For dynamic images, add opengraph-image.tsx that exports an ImageResponse from next/og. Next.js wires the og:image meta tags for you, so you do not set them by hand.
What size should a Next.js OG image be?
1200 by 630 pixels, the 1.91 to 1 ratio that Facebook, X, LinkedIn, Slack, and Discord render as a full-width card. Set the size in the ImageResponse options (or the exported size object) so the generated image matches exactly.
Why is my Next.js OG image not showing up?
The three usual causes are: the file is in the wrong place (it must sit in a route segment under app, not in public), the route is not deployed yet so the absolute URL 404s, or the social platform cached an older version. Test the live URL directly, then re-scrape with the platform's debugger (for example the Facebook Sharing Debugger) to clear the cache.
How do I preview a Next.js OG image locally?
Visit the image route directly in your browser. For a route segment at app/blog/[slug], the generated image is served at /blog/[slug]/opengraph-image. Open that URL in dev to see the rendered card without sharing the link anywhere.
Should I use @vercel/og or a screenshot API for OG images?
Use @vercel/og when the card is a layout you design in JSX (title, author, brand) that does not need to match the page pixel for pixel. Use a screenshot API when you want the OG image to be a real render of the actual page or app, including content @vercel/og cannot easily reproduce, such as charts, maps, or a live dashboard.

Capture any website with one API call

Get a free test key and capture your first screenshot in two minutes.

Written by

Grabbit Team

Screenshots as a service

The team behind Grabbit, the screenshot API for developers and AI agents. We write about web capture, rendering, and automating screenshots at scale.

Keep reading