@vercel/og: Generate Dynamic OG Images with Next.js Edge Functions
How to use @vercel/og and ImageResponse to generate dynamic Open Graph social preview images in Next.js using JSX, custom fonts, and edge runtime — with zero image hosting needed.
Updated March 2026
What is @vercel/og?
@vercel/og is a library that converts JSX/HTML into PNG images at the edge — no browser, no Puppeteer, no server setup. It uses Satori (an SVG renderer) under the hood and runs on Vercel's Edge Runtime for near-zero latency image generation.
The result: dynamically generated OG images for every page of your site, with custom fonts, brand colors, and page-specific text — served from a Vercel Edge Function.
Installation
npm install @vercel/og # Already included in Next.js 13.3+ via next/og
Basic Setup: app/og/route.tsx
// app/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "My Site";
const description = searchParams.get("description") ?? "";
return new ImageResponse(
(
<div
style={{
width: "1200px",
height: "630px",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
background: "linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)",
padding: "80px",
}}
>
<p style={{ color: "#a78bfa", fontSize: 24, margin: "0 0 16px" }}>
yoursite.com
</p>
<h1
style={{
color: "white",
fontSize: 64,
fontWeight: "bold",
lineHeight: 1.1,
margin: "0 0 24px",
maxWidth: "900px",
}}
>
{title}
</h1>
<p style={{ color: "#9ca3af", fontSize: 32, margin: 0, maxWidth: "900px" }}>
{description}
</p>
</div>
),
{
width: 1200,
height: 630,
}
);
}Using it in generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug);
const ogImageUrl = new URL("/og", process.env.NEXT_PUBLIC_SITE_URL);
ogImageUrl.searchParams.set("title", post.title);
ogImageUrl.searchParams.set("description", post.excerpt);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: ogImageUrl.toString(),
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
images: [ogImageUrl.toString()],
},
};
}Adding Custom Fonts
// app/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
// Fetch your font from a CDN or include it in public/
async function loadFont(url: string): Promise<ArrayBuffer> {
const res = await fetch(url);
return res.arrayBuffer();
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "My Site";
const interBold = await loadFont(
"https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2"
);
return new ImageResponse(
(
<div style={{ fontFamily: "Inter", /* ...rest of your layout */ }}>
{title}
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: interBold,
weight: 700,
style: "normal",
},
],
}
);
}Caching OG Images
By default, Vercel caches Edge Function responses. To control caching on your OG image route:
export async function GET(request: Request) {
// ... generate image
return new ImageResponse(element, {
width: 1200,
height: 630,
headers: {
// Cache for 1 hour at the CDN
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
}Common Issues and Fixes
- Flexbox only: Satori only supports a subset of CSS — primarily flexbox layout. Grid, absolute positioning, and most CSS properties are not supported. Stick to flex containers.
- No external images without fetching: To include an image from a URL, fetch it and convert to base64 first.
- Font not loading: Ensure the font URL is accessible from the edge and returns an ArrayBuffer. Avoid Google Fonts redirect URLs — use the direct
gstatic.comCDN URL. - 500 errors in production: Edge runtime has a 4MB memory limit. Large font files or complex images can hit this. Use compressed font subsets.
Test your generated OG image →
Paste your page URL into OGFixer to verify the dynamically generated og:image is rendering correctly on all social platforms.
Preview my OG image →