@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.com CDN 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 →