Next.js OG Image Generator: Beyond @vercel/og

April 24, 2026

Next.js ships with @vercel/og, a serverless OG image generator that uses Satori to render JSX into images. It works — until it doesn't. Hit a CSS feature Satori doesn't support (grid, Flexbox quirks, custom fonts beyond the basics, transforms) and you're rewriting your template from scratch.

This guide shows a simpler path: render plain HTML into an image with a screenshot API. You get the full power of CSS, no runtime limits, and it works whether you deploy on Vercel, Netlify, AWS, or your own server.

Why look beyond @vercel/og?

If your OG images are simple, @vercel/og is fine. If you want full HTML/CSS, read on.

The plan

  1. Build a Next.js page that renders your OG template at /og/[slug]
  2. Hit a screenshot API to capture that page as a 1200×630 PNG
  3. Cache the result so each post generates its image once

Step 1 — Render the OG template page

Create app/og/[slug]/page.tsx in your Next.js 14+ app:

// app/og/[slug]/page.tsx
import { getPost } from "@/lib/posts";

export default async function OgPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return (
    <div style={{
      width: 1200,
      height: 630,
      display: "grid",
      gridTemplateRows: "auto 1fr auto",
      padding: 60,
      background: "linear-gradient(135deg, #0d6efd 0%, #2563eb 100%)",
      color: "#fff",
      fontFamily: "Inter, system-ui",
    }}>
      <div style={{ fontSize: 24, opacity: 0.7 }}>myblog.com</div>
      <h1 style={{ fontSize: 72, fontWeight: 800, lineHeight: 1.1 }}>
        {post.title}
      </h1>
      <div style={{ fontSize: 28, opacity: 0.8 }}>{post.author} · {post.date}</div>
    </div>
  );
}

This is regular React with full CSS support — grid, gradients, anything you want.

Step 2 — Capture it with a screenshot API

// app/api/og/[slug]/route.ts
import { Client } from "screenshotapis";

const client = new Client(process.env.SCREENSHOT_API_KEY!);

export async function GET(_: Request, { params }: { params: { slug: string } }) {
  const { data } = await client.screenshot({
    url: `${process.env.SITE_URL}/og/${params.slug}`,
    viewport_width: 1200,
    viewport_height: 630,
    device_scale_factor: 2,  // retina
    full_page: false,
  });

  return new Response(data, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=31536000, immutable",
    },
  });
}

That's the entire generator. The 1-year cache header means each post's OG image renders once, then serves from CDN forever.

Step 3 — Wire it into metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    openGraph: {
      images: [`/api/og/${params.slug}`],
    },
    twitter: {
      card: "summary_large_image",
      images: [`/api/og/${params.slug}`],
    },
  };
}

Done. Every blog post now has a dynamic OG image with full CSS support.

What about cost?

If you publish 10 posts per month and each OG image is generated once and cached for a year, you use 10 renders/month. That's free on most screenshot APIs. Even a busy site rarely exceeds the free tier for OG images.

Caveats

Generate Next.js OG images with full HTML/CSS — 100/month free

Get your API key — free