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.

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-imagefile convention. Putopengraph-image.tsxin a route segment and Next.js adds theog: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.tsxreturnsnew 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: gridwill not. Unsupported properties are not an error, they just don't render, so check the output, not the code. - Fonts:
ttf,otforwoffonly. Nowoff2, which is the format most font CDNs serve. The docs preferttf/otffor parsing speed. You pass the font file as anArrayBufferyourself; 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-imagegoes over 8MB, or atwitter-imageover 5MB, the build fails. - Emoji are drawn from a set you choose (
twemojiby default;blobmoji,notoandopenmojiare 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.urlis dynamic. Ours does (it takes?title=), and route segmentrevalidatedid nothing for it; we set a longCache-Controlheader on theImageResponseinstead (it acceptsheaders). Confirm withcurl -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
| Option | Type | Free allowance | First paid plan |
|---|---|---|---|
next/og (ImageResponse) | Library in Next.js | Free; you pay your own compute | — |
| ScreenshotOne | Screenshot API (URL or HTML) | 100 screenshots/month, no card | Basic: $17/month, 2,000 |
| HTML/CSS to Image | HTML render + templates | 50 images/month, 5 templates | Basic: from $14/month, 1,000 |
| Orshot | Template API | 100 credits/month, no card | Launch: $39/month, 1,500 credits |
| Microlink | Screenshot and metadata API | 25 requests/day, no card | Pro: $49/month, 46,000 |
| Cloudinary | Image CDN with text overlays | 25 credits/month, no card | Plus: $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.
Microlink
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.tsxwithnext/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.