How to Add Dynamic OG Images in Next.js

javascript dev.to

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
Enter fullscreen mode Exit fullscreen mode

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
  );
}
Enter fullscreen mode Exit fullscreen mode

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
  }'
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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 }],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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.


Originally published on the Grabbit blog.

Source: dev.to

arrow_back Back to Tutorials