ScreenshotNeo

BlogHow-to

How to Generate Dynamic Open Graph Images for Web Pages

Generate page-specific Open Graph images with Next.js, static assets, or Cloudinary. Includes runnable code, metadata checks, caching guidance, and troubleshooting.

By the ScreenshotNeo team4 October 20269 min read

To generate a unique Open Graph image for each page in a Next.js App Router site, add an opengraph-image.tsx file to the relevant route segment and return an image with ImageResponse. Read the page’s title or other content on the server and use it to render a 1200×630 social card. Next.js connects the generated image to the route’s metadata. If every page can share one image, use a static image file instead. For a managed transformation workflow, Cloudinary documents Next.js integrations that generate social-card images from its transformation service.

Choose an approach

Approach Use it when Tradeoff
Next.js ImageResponse You want page-specific images rendered from route data and your design fits its supported CSS. You own the template and need to respect the renderer’s CSS and bundle limits.
Static image file A route or group of routes can use a fixed card. It does not change automatically with page content.
Cloudinary transformations You already use Cloudinary or need its image transformations and delivery workflow. The image is generated through an external service.

The Next.js file convention supports route-level images and automatically adds the corresponding metadata tags. More specific route-segment images take precedence over broader ones. See the official Open Graph and Twitter image file convention documentation.

Generate a route-specific image in Next.js

Create app/blog/[slug]/opengraph-image.tsx. This example gets the post title from a server-side function; replace getPost with the data access already used by your application. The function should return null when no post exists, so an invalid route does not produce a misleading card.

import { ImageResponse } from 'next/og'
import { getPost } from '@/lib/posts'

type Props = {
  params: Promise<{ slug: string }>
}

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

export default async function OpenGraphImage({ params }: Props) {
  const { slug } = await params
  const post = await getPost(slug)

  if (!post) {
    return new Response('Post not found', { status: 404 })
  }

  const title = post.title || 'Untitled article'

  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'space-between',
          padding: '72px',
          background: '#101827',
          color: '#ffffff',
          fontFamily: 'Arial',
        }}
      >
        <div style={{ fontSize: 26, color: '#9fb5d1' }}>Engineering</div>
        <div
          style={{
            fontSize: 64,
            lineHeight: 1.12,
            fontWeight: 700,
            overflow: 'hidden',
          }}
        >
          {title}
        </div>
        <div style={{ fontSize: 24, color: '#9fb5d1' }}>Example.com</div>
      </div>
    ),
    {
      ...size,
    },
  )
}

This example follows the current App Router convention in which route parameters are asynchronous. If your installed Next.js version has a different function signature, follow that version’s documentation. ImageResponse renders JSX to PNG using the Vercel OG, Satori, and Resvg stack. It supports common CSS including flexbox and absolute positioning, custom fonts, text wrapping, centering, and nested images, but only a subset of CSS overall. For exact API details and the current supported CSS list, see the ImageResponse reference.

Use a custom font

Load font bytes in the image module and pass them to ImageResponse. Keep font files inside the deployment bundle and use a supported TTF, OTF, or WOFF file. Next.js recommends TTF or OTF for font parsing speed.

import { ImageResponse } from 'next/og'
import brandFont from './Brand-Bold.ttf'

const fontData = await brandFont.arrayBuffer()

export default function OpenGraphImage() {
  return new ImageResponse(
    (
      <div style={{ fontFamily: 'Brand', fontSize: 64 }}>
        A branded social card
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Brand',
          data: fontData,
          style: 'normal',
          weight: 700,
        },
      ],
    },
  )
}

Keep the complete generated-image bundle at or below Next.js’s documented 500 KB maximum. That limit includes JSX, CSS, fonts, images, and other assets. Large fonts and embedded images can push a seemingly small generator over the limit.

Fetch route data and consider caching

Use the same canonical data source as the page, rather than duplicating titles in a separate image-only store. Generated images are statically optimized and cached by default unless request-time APIs or uncached data make generation dynamic. Decide whether the image should change when the content changes, then align its caching or revalidation behavior with the page’s data freshness. See the Next.js metadata file documentation for route behavior and caching details.

For long-lived published pages, cached generation avoids rendering the same card on every request. For frequently updated titles, stale cards can persist if the image route’s cache policy does not follow the data’s revalidation policy. Verify the deployed behavior; development mode alone does not establish production caching behavior.

Use a static image when the design is shared

When a page does not need a unique card, add a static image using the file convention, such as app/blog/opengraph-image.png. Next.js accepts JPEG, PNG, and GIF for this convention and adds the metadata tags. A more specific route image takes precedence over a broader segment image. You can add a sibling opengraph-image.alt.txt file to supply alternative text.

Choose a static asset for a stable brand or section image. Generate images from route data when the title, author, product, or other page detail should appear in the share preview. For the documented Next.js convention, keep the Open Graph image within 8 MB and the Twitter image within 5 MB.

Use Cloudinary for managed transformations

Cloudinary documents two Next.js patterns: CldOgImage and getCldOgImageUrl. The latter can create a social-card URL for App Router metadata. Its documented default final dimensions are 1200×627, with JPG as the default social-card format. Cloudinary also documents transformation controls such as text overlays, backgrounds, tint, opacity, and underlays; background removal requires its AI Background Removal add-on. See Cloudinary’s Next.js image transformation documentation.

Compare the options against your actual needs: integration effort, template flexibility, existing asset delivery, transformations, caching, and whether an external image service fits your deployment. The documented Next.js example uses 1200×630 while Cloudinary documents 1200×627. These are different presets; check the requirements for the social platforms you target rather than assuming the dimensions are interchangeable.

Set and verify page metadata

The image endpoint must return an image, and the page metadata must point to it. With the Next.js file convention, the framework supplies the associated image tags. Inspect the server-rendered HTML for a valid og:image URL and, when required, Twitter image metadata. Confirm that the URL is publicly reachable by the social crawler and does not depend on an authenticated session.

  1. Open a page that uses the image route and inspect its server-produced HTML.
  2. Confirm og:image resolves to the intended image URL.
  3. Request that image URL directly and confirm it returns the expected image content type and dimensions.
  4. Try a long title, missing content, and an invalid route.
  5. Check the deployed cache or revalidation behavior against your content update frequency.
  6. Verify file size stays within the relevant platform and framework limits.

Do not treat image dimensions or dynamic generation as evidence of higher click-through or engagement. The sources here document implementation details, not engagement outcomes.

Design and edge cases

Long titles and missing data

Social cards have finite space. Test your longest real titles and use a font size, line height, and container size that leave room for wrapping. Avoid relying on unsupported CSS truncation behavior. Provide a useful fallback for absent titles, and handle missing records deliberately, for example by returning a 404 response from the image route.

Fonts and assets

Include fonts and assets in the deployed bundle or load them through a supported server-side path. Check that assets are available in the production runtime and do not exceed the bundle limit. TTF and OTF are preferred over WOFF for parsing speed according to the Next.js reference.

Layout support

Keep layouts within the renderer’s CSS subset. Flexbox and absolute positioning are documented as supported; CSS Grid is an example of an advanced layout that will not work. If a layout fails, simplify it to supported primitives before changing unrelated page code.

Content freshness

Cached generated output may not immediately reflect changed source data. Decide whether the card is immutable after publishing or should revalidate when its source changes. Then check both the image route’s behavior and the page metadata that references it.

Performance, reliability, and cost

For native generation, the main practical constraints documented here are the supported CSS subset, the 500 KB bundle maximum, and whether the route is cached or generated dynamically. Keep image templates and fonts small, and use static optimization when the content does not require request-time data. With a managed service, consider the transformation and delivery workflow and its external-service dependency; consult that service’s current terms and pricing for cost decisions.

For reliability, ensure the generator handles missing page data, valid titles, fonts, and assets in the deployed runtime. A successful page render does not by itself prove that the image URL is reachable to a social crawler. Verify the final image response and metadata after deployment.

Troubleshooting

Symptom Likely cause Fix
No og:image tag appears. The image file is outside the intended route segment, or the page’s rendered metadata is not what you expect. Check the route file location and inspect the server-produced HTML for that exact page.
The image route returns an error. Data lookup failed, the route parameter was not handled for the installed Next.js version, or an asset is unavailable. Check the route parameters and server data access, handle missing records, and confirm fonts and assets are included in the deployment.
Text or styling differs from the design. The generator uses a limited CSS subset. Use supported layout primitives such as flexbox and absolute positioning; avoid unsupported features such as CSS Grid.
Build reports the image bundle is too large. Fonts, images, CSS, or other bundled assets exceed 500 KB. Reduce or replace bundled assets and fonts, then review the complete bundle size.
A changed title does not appear in the card. The generated output is cached or the page data has not revalidated. Align route caching and revalidation with the source data’s update policy, then request the deployed image again.
Social platform shows an old or absent preview. The metadata URL is wrong, the image is inaccessible to the crawler, or the platform retains a cached preview. Verify the HTML tag and image URL from outside an authenticated session, then use the platform’s available refresh mechanism.
The image is rejected or does not fit. Dimensions or file size do not meet the target platform’s expectations. Check the target platform’s current requirements and verify actual dimensions and file size of the response.

Or skip the browser setup

If the page already exists and you need a screenshot of it rather than a designed social card, ScreenshotNeo is a website screenshot API and MCP server for developers. It captures a URL as PNG, JPEG, WebP, or PDF with one request. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo screenshots an existing page; it does not replace a designed, data-driven OG image generator.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does a dynamic OG image need a separate metadata tag in Next.js?

The opengraph-image file convention generates the associated metadata tags. Inspect the rendered HTML to confirm the image URL is present.

Can I use the same image for every page?

Yes. Put a static image in the route segment that should share it. More specific route-segment images take precedence over broader ones.

Does ImageResponse support CSS Grid?

No. It supports a subset of CSS, and the Next.js reference identifies display: grid as unsupported.

Which dimensions should I use?

The Next.js generated-image example uses 1200×630. Cloudinary documents a 1200×627 default. Check the current requirements of the platforms where the image will appear.

Will a dynamic image improve engagement?

The cited documentation describes how to generate and attach images; it does not establish an engagement effect.