Back to blog
TutorialJuly 23, 2026 · 10 min read

How to Add OG Images to a Next.js Site

Three methods: static images, per-page metadata, and dynamic generation with satori. Works with Next.js App Router (14+).

Why your Next.js site needs OG images

When someone shares a link to your Next.js site on Twitter, Slack, or LinkedIn, the platform fetches your page and looks for og:image meta tags. Without them, your link shows up as a boring text URL — no preview image, no title, no description.

Links with OG images get 2-3x more engagementthan plain text links. If you're using Next.js, you have the Metadata API built in — adding OG images takes just a few lines of code.

Prerequisites

  • Next.js 14+ (App Router)
  • React 18+ (React 19 also works)
  • A basic understanding of the App Router file structure
  • An OG image (1200x630px PNG) — or use our generator

This guide covers the App Router(app/ directory). If you're on the Pages Router, use next/head instead.

Method 1: Static OG image (simplest)

The easiest approach: add a single image that's used across your entire site. Perfect for landing pages and simple sites.

Step 1: Add your image

Place a 1200x630 PNG in your public/ directory:

public/
  og-image.png   ← 1200x630 PNG

Step 2: Configure metadata

In your app/layout.tsx, export a metadata object:

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My Awesome Site",
  description: "A description of my site",
  openGraph: {
    title: "My Awesome Site",
    description: "A description of my site",
    images: [
      {
        url: "/og-image.png",
        width: 1200,
        height: 630,
        alt: "My Awesome Site",
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    images: ["/og-image.png"],
  },
};

That's it. Next.js will automatically add the og:image, og:title, and og:description meta tags to every page.

Note: Use absolute URLs if your image is hosted elsewhere: https://example.com/og.png

Method 2: Per-page OG images (recommended)

For blogs or multi-page sites, you want each page to have its own OG image. Next.js makes this easy with per-page metadata.

Step 1: Set up a title template

In layout.tsx, add a title template:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "My Awesome Site",
    template: "%s | My Awesome Site",  // ← per-page titles
  },
  openGraph: {
    siteName: "My Awesome Site",
    type: "website",
  },
};

Step 2: Add per-page metadata

In each page file, export its own metadata:

// app/blog/my-post/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "How to Build a REST API",
  description: "A complete guide to building REST APIs with Node.js",
  openGraph: {
    title: "How to Build a REST API",
    description: "A complete guide to building REST APIs with Node.js",
    url: "https://example.com/blog/my-post",
    type: "article",
    publishedTime: "2026-07-23",
    images: [
      {
        url: "/blog/my-post/og-image.png",
        width: 1200,
        height: 630,
        alt: "How to Build a REST API",
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    title: "How to Build a REST API",
    images: ["/blog/my-post/og-image.png"],
  },
};

export default function Page() {
  return <article>...</article>;
}

Pro tip: Use the OG Image Generator to create a unique image for each blog post in seconds. Just type your headline and download.

Method 3: Dynamic OG images with satori

For sites with many pages (like docs or blogs with 100+ posts), creating individual images manually doesn't scale. Instead, generate them on the fly with satori and resvg-js.

Step 1: Install dependencies

npm install satori @resvg/resvg-js

Step 2: Create an OG image route

// app/api/og/route.tsx
import { NextRequest } from "next/server";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get("title") || "Default Title";
  const subtitle = searchParams.get("subtitle") || "";

  // Render React-like JSX to SVG with satori
  const svg = await satori(
    {
      type: "div",
      props: {
        style: {
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          justifyContent: "center",
          alignItems: "center",
          background: "linear-gradient(135deg, #f43f5e, #f59e0b)",
          padding: "60px",
          color: "white",
        },
        children: [
          {
            type: "h1",
            props: {
              style: { fontSize: 60, fontWeight: 700 },
              children: title,
            },
          },
          subtitle && {
            type: "p",
            props: {
              style: { fontSize: 28, opacity: 0.9, marginTop: 20 },
              children: subtitle,
            },
          },
        ].filter(Boolean),
      },
    },
    { width: 1200, height: 630, fonts: [] }
  );

  // Convert SVG to PNG
  const resvg = new Resvg(svg, {
    fitTo: { mode: "width", value: 1200 },
  });
  const png = resvg.render().asPng();

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

Step 3: Use it in metadata

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const ogUrl = `/api/og?title=${encodeURIComponent(post.title)}&subtitle=${encodeURIComponent(post.excerpt)}`;

  return {
    openGraph: {
      images: [{ url: ogUrl, width: 1200, height: 630 }],
    },
  };
}

Font loading: Satori requires font files loaded as ArrayBuffers. Download a .ttf or .otf and use fetch(fontUrl).then(r => r.arrayBuffer()). See the satori docs for details.

Adding Twitter card meta tags

Twitter/X uses its own set of meta tags alongside OG tags. Add these to your metadata to ensure proper rendering:

export const metadata: Metadata = {
  twitter: {
    card: "summary_large_image",   // ← large image card
    title: "Your Title",
    description: "Your description",
    images: ["/og-image.png"],
    creator: "@yourhandle",         // ← your Twitter handle
  },
};
summary_large_image — Large card with image above the title (recommended for blogs).
summary — Small square thumbnail next to the title.
player — For video/audio content.

Testing your OG images

Always verify your OG images render correctly before deploying. Use these free tools:

Twitter Card ValidatorPreview your link on Twitter/X
Facebook Sharing DebuggerPreview + clear Facebook cache
LinkedIn Post InspectorCheck LinkedIn rendering
OpenGraph.xyzMulti-platform preview tool

Cache tip: If you update your OG image, platforms may serve a cached version for up to 30 days. Use the Facebook Sharing Debugger to force a re-scrape.

Shortcut: Use our dynamic image API

Don't want to set up satori yourself? Our Pro plan includes a dynamic image API. Just construct a URL with your title and theme, and we return a rendered PNG:

// Drop this directly in your Next.js metadata
export const metadata = {
  openGraph: {
    images: [
      {
        url: "https://og-image-generator.appsnap.co.uk/api/og?title=My+Blog+Post&theme=rose",
        width: 1200,
        height: 630,
      },
    ],
  },
};

Pro plan includes: Dynamic API, no watermark, premium templates, bulk CSV generation. See pricing →

Frequently asked questions

Do I need next/og or @vercel/og?

No. @vercel/og is a Vercel-specific wrapper around satori. If you're not on Vercel, install satori + @resvg/resvg-js directly. It works on any Node.js runtime.

How do I add OG images to dynamic routes ([slug])?

Use generateMetadata() — an async function that receives params and returns a Metadata object. Fetch your post data, construct the OG image URL, and return it. See Method 3 above.

Why is my OG image not showing on Twitter?

Three common causes: (1) Missing twitter:card tag — add card: 'summary_large_image'. (2) Relative image URL — must be absolute. (3) Cached — use the Twitter Card Validator to force a re-scrape.

Can I use .webp for OG images?

Most platforms support WebP now, but PNG is still the safest choice for maximum compatibility. If you use WebP, test it on all target platforms.

Should OG images be in the public/ directory?

For static images, yes. For dynamic images generated by satori, they're served from an API route (e.g., /api/og) and don't need to be in public/.

Skip the setup — generate OG images instantly

Pick a template, type your headline, download a 1200x630 PNG. Or use our dynamic API on the Pro plan.

Get started free