ScreenshotNeo

BlogHow-to

How to Generate Open Graph Images with Vercel

Create dynamic 1200×630 Open Graph images with Vercel, connect them to Next.js metadata, and troubleshoot crawlers, fonts, caching, and deployment.

By the ScreenshotNeo team1 October 20269 min read

Use Next.js App Router and ImageResponse from next/og to render a public 1200×630 image route, then place that route’s absolute URL in your page’s og:image metadata. This is Vercel’s documented path for generated Open Graph (OG) images. The example below works with Node.js 22 or newer and Next.js 12.2.3 or newer. Read Vercel’s OG image guide for the platform details.

What you are building

The request flow has four parts:

  1. A browser or social crawler requests a page.
  2. The page metadata points to an absolute image URL such as https://example.com/api/og?title=....
  3. Vercel runs a route that renders JSX through ImageResponse.
  4. The route returns a PNG that social platforms can fetch.

Vercel recommends 1200 by 630 pixels for OG images. The renderer uses Satori and Resvg, supports flexbox and absolute positioning, and supports a subset of CSS. CSS Grid is not supported. Fonts must be TTF, OTF, or WOFF; TTF and OTF are recommended for parsing speed. The documented bundle limit is 500 KB, including JSX, CSS, fonts, images, and other assets. See the @vercel/og API reference.

Prerequisites and project setup

  • Node.js 22 or newer.
  • Next.js 12.2.3 or newer with the App Router.
  • A project that can be deployed at a public HTTPS URL.

In an App Router project, next/og is already included. You do not normally need to install @vercel/og separately. Create the route at app/api/og/route.tsx.

Build a static OG image route

Start with a fixed card to verify that rendering and deployment work before adding dynamic data.

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export async function GET() {
  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        width: '100%',
        height: '100%',
        alignItems: 'center',
        justifyContent: 'center',
        background: 'white',
        color: 'black',
        fontSize: 64,
        fontWeight: 700,
      }}
    >
      Article title
    </div>,
    { width: 1200, height: 630 },
  )
}

Visit /api/og locally. If the route returns an image, deploy it and test the public URL. The explicit dimensions avoid accidental output sizes and match Vercel’s recommended canvas.

Generate a dynamic image from a title

Read query parameters with request.url, constrain their length, and render the result. The 100-character slice below is an application safeguard from Vercel’s example; it is not a universal platform limit.

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const rawTitle = searchParams.get('title')
  const title = (rawTitle || 'My article').slice(0, 100)

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        width: '100%',
        height: '100%',
        padding: '72px',
        justifyContent: 'space-between',
        background: '#111827',
        color: 'white',
        fontFamily: 'Arial',
      }}
    >
      <div style={{ display: 'flex', fontSize: 30, color: '#93c5fd' }}>
        Example site
      </div>
      <div style={{ display: 'flex', fontSize: 64, fontWeight: 700 }}>
        {title}
      </div>
      <div style={{ display: 'flex', fontSize: 26, color: '#cbd5e1' }}>
        example.com
      </div>
    </div>,
    { width: 1200, height: 630 },
  )
}

Encode the title when constructing the URL. Treat all query values as untrusted input: cap lengths, replace unexpected control characters, and avoid rendering arbitrary HTML or user supplied CSS.

Connect the image to page metadata

The image route is not discovered automatically. Add an absolute URL to the page’s metadata. With the App Router, use the Metadata API:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'How to Generate Open Graph Images with Vercel',
  openGraph: {
    title: 'How to Generate Open Graph Images with Vercel',
    images: [
      {
        url: 'https://example.com/api/og?title=How%20to%20Generate%20Open%20Graph%20Images',
        width: 1200,
        height: 630,
        alt: 'Generated Open Graph image',
      },
    ],
  },
}

Use the deployed hostname, not localhost. A social crawler must be able to fetch the route from the public internet. If the hostname changes between preview and production, generate the URL from your deployment’s canonical site URL.

Choose an API route or an opengraph-image file

Pattern Use it when Trade-off
app/api/og/route.tsx Titles, authors, scores, or other values vary per request. One route serves many cards, but you must validate parameters and maintain URL construction.
opengraph-image.jsx A page or route has a stable, page-specific card. It follows a Next.js convention, but many pages can mean many files to maintain.

Vercel documents both patterns. The right choice depends on whether content varies and how many cards you need; the sources do not establish a universal winner. See the OG image examples.

Fonts, assets, and CSS constraints

Fonts

Only TTF, OTF, and WOFF font files are supported. To keep parsing quick, prefer TTF or OTF. Load a font from a local file with Node.js file access where your runtime supports it, or fetch a remote asset. Keep the complete bundle under the documented 500 KB limit.

Layout

Use flexbox, absolute positioning, dimensions, colors, borders, and other properties supported by the Satori renderer. Do not rely on CSS Grid, browser-only layout behavior, external stylesheets, or client-side JavaScript. Every element that affects the card must be represented in the server-rendered JSX.

Images and remote data

Remote images and data can be fetched at request time. Check the response, keep timeouts bounded, and provide a fallback when an asset is unavailable. A broken avatar should not prevent a card from rendering.

Runtime compatibility

Vercel’s documented runtime table supports return new Response(...) for Pages Router with Edge, App Router with Node.js, and App Router with Edge. The same syntax is not supported for Pages Router with Node.js in the documented vercel/og combination. If you use a different handler shape, framework, or runtime, check the current guide before deploying.

For an App Router route, explicitly exporting runtime = 'edge' is common, but App Router with Node.js is also documented. Select the runtime based on the APIs your route needs, especially filesystem access and external dependencies.

Make crawlers able to fetch the image

Allow the image endpoint in robots.txt. For a route under /api/og/, Vercel’s example uses:

User-agent: *
Allow: /api/og/*

Then verify all of these conditions:

  • The page contains an absolute og:image URL.
  • The deployed route responds without authentication.
  • The route returns an image content type rather than an error page.
  • The route and its remote assets are reachable by social crawlers.

Use Vercel’s Open Graph preview tooling to inspect metadata before production. Preview tools help find wiring and fetch problems, but social platforms can still render cards differently.

Test the route with cURL, Python, and Node.js

These checks confirm the public endpoint and response headers independently of a social platform.

curl -i "https://example.com/api/og?title=Hello%20Vercel"
import requests

url = 'https://example.com/api/og'
r = requests.get(url, params={'title': 'Hello Vercel'}, timeout=30)
r.raise_for_status()
open('og.png', 'wb').write(r.content)
const q = new URLSearchParams({ title: 'Hello Vercel' });
const res = await fetch(`https://example.com/api/og?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('og.png', data));

Or skip the browser setup

If your goal is a clean screenshot or preview image rather than a custom JSX card, ScreenshotNeo provides a one-call website screenshot API. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.

See the ScreenshotNeo documentation for the full option set. A direct call looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

You can also request PNG, JPEG, or PDF; full pages with lazy images loaded; a CSS-selected element; dark mode; device presets or custom viewports; retina scale; custom CSS and JavaScript; click, selector, delay, or network-idle waits; blocked ads, trackers, requests, or resource types; custom headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, and usage information. There are 1,000 free shots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Troubleshooting

Symptom Likely cause Fix
404 for /api/og File is outside app/api/og/route.tsx or the deployment is stale. Check the App Router path, redeploy, and request the exact public URL.
500 with a CSS or JSX error Unsupported CSS, malformed JSX, or a browser-only API. Reduce the card to flexbox and supported properties; remove window, DOM APIs, and CSS Grid.
Image is blank Text or assets are not represented in server-rendered output, or a remote asset failed. Render a fallback title and background; check each remote response.
Font does not load Unsupported format, excessive bundle size, or an inaccessible URL. Use TTF, OTF, or WOFF; reduce the bundle below 500 KB; verify the asset URL.
Social card is missing Relative URL, blocked route, authentication, or disallowed crawler path. Use an absolute deployed URL, allow the route in robots.txt, and inspect with Vercel’s preview tool.
Old card remains after an update Image URLs or intermediary caches still contain the old response. Version the image URL with a stable content parameter when content changes; do not assume every external cache follows your deployment settings.
Pages Router Node.js handler fails The documented return new Response(...) combination is unsupported there. Use an App Router route, Pages Router Edge, or the handler shape supported by your current framework version.

Performance, reliability, and cost

Performance

  • Keep JSX and assets small, especially fonts and embedded images.
  • Prefer local or fast, cacheable assets and avoid unnecessary remote fetches.
  • Limit title and description lengths before rendering.
  • Choose a single card layout instead of performing multiple dependent data fetches.

Reliability

  • Always render a default title when a query parameter is absent.
  • Use fallback colors and text when optional assets fail.
  • Keep the route publicly reachable and test it from outside your local network.
  • Use a versioned URL when a card’s content must change despite long-lived caches.

Cost

The documented workflow runs through Vercel Functions and a public deployment. The cited Vercel sources do not provide a universal per-image price, so check your Vercel plan and function usage. Generated routes also add work when crawlers request them. If you need website screenshots rather than code-rendered cards, ScreenshotNeo bills only clean shots and offers 1,000 free shots monthly with no card.

Deployment checklist

  • Node.js 22+ and a supported Next.js version are selected.
  • The route returns ImageResponse at 1200×630.
  • Only supported CSS and font formats are used.
  • The total bundle is below 500 KB.
  • The page metadata uses an absolute deployed og:image URL.
  • robots.txt allows the OG route.
  • The public route returns an image with a successful status.
  • Vercel’s OG preview shows the expected metadata and image.

FAQ

Can I use a different image size?

Yes, pass different dimensions to ImageResponse, but Vercel’s recommended OG size is 1200×630.

Is the 100-character title limit required?

No. It is a defensive limit in the example. Choose a limit that fits your design and sanitize your own inputs.

Does App Router require installing @vercel/og?

The documented App Router setup imports ImageResponse from next/og, which is included in Next.js App Router projects.

Will every social network display the card identically?

No guarantee is made in the documentation. Validate metadata and fetchability, then check the platforms your audience uses.

When should I use ScreenshotNeo?

Use it when you need a screenshot of an existing public page, PDF, or element and want consent banners, popups, chat widgets, failed loads, and bot checks handled by the capture service. Start free with 1,000 screenshots per month and no card.