Skip to content

Frontend

Dynamic OG Images in Next.js: next/og vs Screenshot and Template APIs

How to generate Open Graph images in Next.js with next/og, where Satori stops (CSS, fonts, 500KB), and when a screenshot or template API is the better buy.

November 11, 20268 min read#seo#react#api#frontend

For most Next.js sites, the right way to make dynamic Open Graph images is ImageResponse from next/og: a file called opengraph-image.tsx next to the page, JSX in, PNG out, no extra service and no bill beyond your own hosting. Reach for a hosted screenshot or template API when the card needs real browser CSS, when non-developers design it, or when the site isn't Next.js at all.

Below: what next/og can't do, a working example, and five hosted APIs with their free allowance. Every limit and price was checked against the Next.js docs and each vendor's own page on 11 November 2026.

What next/og actually is

ImageResponse is the @vercel/og library, bundled into Next.js. It turns your JSX into SVG with Satori, then into PNG with Resvg. There is nothing to install in an App Router project, and it is not tied to Vercel hosting: it runs wherever your Next.js app runs.

You can use it two ways:

  • The opengraph-image file convention. Put opengraph-image.tsx in a route segment and Next.js adds the og:image, type, width and height tags for you. The docs say these images are statically optimized (built once and cached) unless they use request-time APIs or uncached data. For a blog, that means one PNG per post at build time and zero runtime cost.
  • A Route Handler. app/og/route.tsx returns new ImageResponse(...) and reads, say, a ?title= query. One endpoint serves every page, but it now renders on request.

The limits that bite

These come from the Next.js and Vercel docs, which agree with each other:

  • CSS is a subset. Flexbox and absolute positioning work; the docs say advanced layouts such as display: grid will not. Unsupported properties are not an error, they just don't render, so check the output, not the code.
  • Fonts: ttf, otf or woff only. No woff2, which is the format most font CDNs serve. The docs prefer ttf/otf for parsing speed. You pass the font file as an ArrayBuffer yourself; there is no @font-face.
  • 500KB bundle limit. That counts your JSX, CSS, fonts and images together. One full-weight font can eat most of it, so subset the font or fetch assets at runtime.
  • Size limits on the output. If a generated opengraph-image goes over 8MB, or a twitter-image over 5MB, the build fails.
  • Emoji are drawn from a set you choose (twemoji by default; blobmoji, noto and openmoji are the other options), not from the system font.

A working example

A per-post card for a route like app/[category]/[slug]/page.tsx. It follows the current opengraph-image docs: params is a Promise since Next.js 16, and the font is read once at module scope.

// app/[category]/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { getPost } from '@/lib/posts' // your own content loader

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

// A .ttf in your repo; woff2 will not load.
const interBold = await readFile(join(process.cwd(), 'assets/Inter-Bold.ttf'))

export default async function Image({
  params,
}: {
  params: Promise<{ category: string; slug: string }>
}): Promise<ImageResponse> {
  const { slug } = await params
  const post = await getPost(slug)

  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'space-between',
          padding: 72,
          background: '#0b0b0f',
          color: '#f5f5f7',
          fontFamily: 'Inter',
        }}
      >
        <div style={{ display: 'flex', fontSize: 28, opacity: 0.7 }}>example.com</div>
        <div style={{ display: 'flex', fontSize: 64, lineHeight: 1.15 }}>
          {post.title}
        </div>
      </div>
    ),
    {
      ...size,
      fonts: [{ name: 'Inter', data: interBold, weight: 700, style: 'normal' }],
    }
  )
}

Three things worth knowing from running one of these on this site:

  • Keep it free of request-time data. Read the post from your content files, not from cookies or headers, and the docs' static optimization applies: one PNG per post, cached.
  • A Route Handler that reads request.url is dynamic. Ours does (it takes ?title=), and route segment revalidate did nothing for it; we set a long Cache-Control header on the ImageResponse instead (it accepts headers). Confirm with curl -I.
  • Cap the input. A query-string title is user input. Trim it to a sane length before it reaches the layout.

When a hosted API is the better buy

next/og is a layout engine, not a browser. A hosted service is worth paying for when you need one of these:

  • Real CSS. Grid, web fonts in any format, your actual stylesheet: a headless browser renders it as your page does.
  • A screenshot of the page itself rather than a separate card.
  • Designers own the template. Template APIs give a visual editor, and the API only fills in the variables.
  • Not Next.js. A plain HTTP API works from any stack, or from a CMS webhook.

The cost is a monthly quota, a network round trip, and one more vendor between you and your share cards. Generate once per post and store the file; don't call the API on every crawler hit.

Side by side

OptionTypeFree allowanceFirst paid plan
next/og (ImageResponse)Library in Next.jsFree; you pay your own compute—
ScreenshotOneScreenshot API (URL or HTML)100 screenshots/month, no cardBasic: $17/month, 2,000
HTML/CSS to ImageHTML render + templates50 images/month, 5 templatesBasic: from $14/month, 1,000
OrshotTemplate API100 credits/month, no cardLaunch: $39/month, 1,500 credits
MicrolinkScreenshot and metadata API25 requests/day, no cardPro: $49/month, 46,000
CloudinaryImage CDN with text overlays25 credits/month, no cardPlus: $99/month ($89 yearly)

ScreenshotOne

A screenshot API that renders a URL or raw HTML in a real browser. The free plan gives 100 screenshots a month without a card, and HTML rendering is included. Good when you want the card styled with the same CSS as your site.

HTML/CSS to Image

You send HTML and CSS, it returns an image. The free plan has 50 images a month and up to 5 saved templates, without media uploads. The pricing page doesn't say whether a card is needed.

Orshot

Template-first: you design in Orshot Studio (or import from Canva or Figma), then call the render endpoint with a template ID and the values that change. The free plan is 100 credits a month, one credit per image. It suits a team where the designer, not the developer, owns the card.

Mostly a metadata and screenshot API. The free plan is 25 requests a day with an API key and no card; there is no monthly free quota. Its pricing page lists no separate OG-card product, so you would screenshot a card page you host.

Cloudinary

Not a renderer: you upload a base image and add text with a URL transformation, such as l_text:Arial_80:Coffee in the layers docs. The free plan is 25 credits a month, and one credit is 1,000 transformations. Fine for "background plus title"; awkward for anything with layout.

How to choose

  • A Next.js blog or docs site: opengraph-image.tsx with next/og. Static, free, and in the repo with the page it describes.
  • The design needs grid or browser-exact CSS: ScreenshotOne or HTML/CSS to Image, rendered once per post and stored.
  • A designer owns the card, or there are many templates: Orshot.
  • You already serve images through Cloudinary: a text overlay on a fixed background, with no new vendor.
  • You just need one good static card: draw it once in the OG image generator and commit the PNG.

The OG card is one of several image files a site ships; favicon sizes covers the rest of the set, from favicon.ico to the manifest icons.

FAQ

Is next/og the same as @vercel/og?

Yes. ImageResponse from next/og is @vercel/og, bundled into Next.js. Install @vercel/og only outside the App Router; the Vercel docs say App Router projects already include it.

Does ImageResponse support CSS Grid or Tailwind?

The Next.js docs say Grid will not work; use flexbox. Vercel lists Tailwind classes via a tw prop as experimental, so test the output before relying on it.

Why is my custom font not showing?

Usually the format or the size. next/og reads ttf, otf and woff, not woff2, and the whole bundle must stay under 500KB. Also check that the name in fonts matches the fontFamily in your styles.

Do dynamic OG images cost money?

next/og itself is free. A file-convention image without request-time data is built once and cached, so it costs nothing per share. A Route Handler that renders per request uses your host's compute, so cache its response. Hosted APIs bill per image past their free quota.

What size should an OG image be?

1200×630 pixels, the default in ImageResponse and the size Vercel recommends. Keep the file under 8MB for opengraph-image and 5MB for twitter-image, or the Next.js build fails.


OG images are one step in a wider launch list; the rest is in the launch-day SEO checklist. For a hand-made card, the OG image generator draws a 1200×630 PNG in the browser. More tools for building pages are in the Frontend directory.