Next.js OG Image: Complete Guide (App Router + Pages Router) — share-preview.com

How to set up OG images in Next.js 13+ App Router using opengraph-image.tsx and ImageResponse, plus Pages Router og:image tags. Covers dynamic generation, caching, dimensions, common pitfalls, and testing.

Next.js OG Image: Complete Guide (App Router + Pages Router)

Open Graph images in Next.js need special handling — the framework's two routers take completely different approaches, and there are caching pitfalls that will silently break your previews in production. This guide covers the opengraph-image.tsx file convention, the ImageResponse API for dynamic generation, the Pages Router og:image tag approach, and how to test everything before it goes live.

1. Why Next.js OG Images Need Special Handling

In a plain HTML site, adding an OG image is trivial: put a <meta property="og:image" content="..."> tag in your <head> and you're done. Next.js complicates this in several ways:

  • Two different routing systems — The App Router (Next.js 13+) and Pages Router have completely different APIs for setting metadata. Using the wrong approach for your router version produces no error but also no OG tags.
  • Server-side rendering — Social media bots crawl your page at render time and need fully-formed og:image tags in the server-rendered HTML. Client-side metadata manipulation (like using useEffect to set meta tags) is invisible to crawlers.
  • Dynamic routes — Blog posts, product pages, and user profiles need unique OG images per page, often generated dynamically from database content. Static images won't work for these.
  • Vercel / edge deployment — Dynamic OG image generation at the edge has specific constraints around supported fonts, images, and CSS that don't exist in a standard Node.js environment.
  • Caching conflicts — Social platforms cache OG images aggressively. If your og:image URL doesn't change when the image changes, platforms serve stale previews.

2. App Router: opengraph-image.tsx File Convention

Next.js 13+ App Router introduces a file-based convention for OG images: place an opengraph-image.tsx (or opengraph-image.jsx, opengraph-image.png, opengraph-image.jpg) file inside any route segment, and Next.js automatically generates the og:image meta tag for that route.

Static Image File

The simplest approach: place a static image file named opengraph-image.png (or .jpg, .gif, .webp) in your route segment folder:

// File structure:
app/
  opengraph-image.png     ← applies to / (home page)
  blog/
    opengraph-image.png   ← applies to /blog
    [slug]/
      opengraph-image.png ← applies to /blog/[slug] (same image for all posts)

Next.js reads the image file, sets appropriate cache headers, and automatically adds the og:image, og:image:width, og:image:height, and og:image:type meta tags to the route's HTML head.

Dynamic Generation with generateMetadata

For unique OG images per page (e.g., blog posts with the article title on the image), combine generateMetadata with a dynamic image route:

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [
        {
          url: `/api/og?title=${encodeURIComponent(post.title)}`,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
  }
}

The opengraph-image.tsx Route File

Alternatively, place an opengraph-image.tsx file directly in the route segment. This file exports an Image component using the ImageResponse API:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPost } from '@/lib/posts'

export const runtime = 'edge'
export const alt = 'Blog post OG image'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

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

  return new ImageResponse(
    (
      <div style={{
        background: '#1e293b',
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        padding: '60px 80px',
      }}>
        <div style={{ color: '#f8fafc', fontSize: 60, fontWeight: 800, lineHeight: 1.2 }}>
          {post.title}
        </div>
        <div style={{ color: '#94a3b8', fontSize: 28, marginTop: 24 }}>
          yourblog.com
        </div>
      </div>
    ),
    { ...size }
  )
}

Next.js automatically creates the route /blog/[slug]/opengraph-image and sets the og:image tag to that URL. The runtime = 'edge' export is recommended for faster cold starts on Vercel.

3. The ImageResponse API (next/og)

The ImageResponse class from next/og is the core tool for dynamic OG image generation in Next.js. It accepts JSX and an options object, and returns a PNG (or JPEG) image rendered server-side.

import { ImageResponse } from 'next/og'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get('title') ?? 'Default Title'

  return new ImageResponse(
    (
      <div style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        fontSize: 48,
        color: 'white',
        fontWeight: 700,
        padding: 40,
        textAlign: 'center',
      }}>
        {title}
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  )
}

ImageResponse Options

  • width / height — Image dimensions in pixels. Default: 1200×630.
  • fonts — Array of custom font objects with name, data (ArrayBuffer), and weight. Load fonts using fetch() from Google Fonts or your CDN.
  • emoji — Emoji rendering style: 'twemoji', 'blobmoji', 'noto', or 'openmoji'.
  • headers — Custom HTTP headers to add to the response.
  • status — HTTP status code (defaults to 200).

CSS Support in ImageResponse

ImageResponse uses Satori under the hood, which supports a subset of CSS. Supported: flexbox layout, most color formats, border-radius, box-shadow, font-weight, text-align, padding/margin, background-image gradients, and opacity. Not supported: CSS Grid, animations, pseudo-elements, and most complex selectors. All layout must be done with flexbox.

Font tip: System fonts are not available in the edge runtime. You must fetch and provide any custom fonts as ArrayBuffer objects. Fetching fonts at request time adds latency — consider caching them at module level outside the handler function.

4. Pages Router: og:image with next/head

In the Pages Router (Next.js 12 and earlier, or current projects still on Pages Router), OG tags are set using the next/head component inside your page component:

// pages/blog/[slug].tsx
import Head from 'next/head'

export default function BlogPost({ post }) {
  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta
          property="og:image"
          content={`https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`}
        />
        <meta property="og:image:width" content="1200" />
        <meta property="og:image:height" content="630" />
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:image"
          content={`https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`}
        />
      </Head>
      {/* page content */}
    </>
  )
}

export async function getStaticProps({ params }) {
  const post = await getPost(params.slug)
  return { props: { post } }
}

The dynamic image endpoint lives at pages/api/og.tsx and uses the same ImageResponse API:

// pages/api/og.tsx
import { ImageResponse } from 'next/og'
import { NextRequest } from 'next/server'

export const config = { runtime: 'edge' }

export default function handler(req: NextRequest) {
  const { searchParams } = new URL(req.url)
  const title = searchParams.get('title') ?? 'My Site'

  return new ImageResponse(
    <div style={{ /* your JSX styles */ }}>{title}</div>,
    { width: 1200, height: 630 }
  )
}

5. Static vs Dynamic OG Images: Trade-offs

Quick rule: Static images for brochure pages (home, about, pricing). Dynamic images for content pages (blog posts, product pages, user profiles). Hybrid for everything in between.

Aspect Static OG Image Dynamic OG Image
Setup complexity Drop a PNG file, done Requires ImageResponse code
Performance Fastest (CDN cached) Slight render overhead
Per-page uniqueness Same image for all pages Unique image per page/post
Design flexibility Full design freedom (Photoshop, Figma) Limited to Satori's CSS subset
Updates Requires code deploy per change Updates with content automatically
Best for Landing pages, marketing pages Blog posts, products, user profiles

6. OG Image Dimensions and Formats

Standard Dimensions

The universal standard for OG images is 1200 × 630 pixels at 72 DPI (pixels on screen, not print). This is the size recommended by Facebook, and it renders well on Twitter/X, LinkedIn, WhatsApp, and Slack. Always specify width and height explicitly in your ImageResponse options — without them, Next.js defaults to 1200×630 but some edge cases can produce unexpected sizes.

Always also add og:image:width and og:image:height meta tags alongside og:image. This prevents platforms from having to fetch the image just to determine dimensions before deciding whether to display a preview card.

PNG vs JPEG for OG Images

  • PNG — Default format for ImageResponse. Lossless, supports transparency, larger file size. Recommended for images with text and sharp edges.
  • JPEG — Smaller file size, faster loading. No transparency. Good for photographic OG images. Set via contentType = 'image/jpeg' in your image route.
  • WebP — Better compression than both, but not universally supported by all social media crawlers. Avoid for OG images until platform support matures.

7. Common Pitfalls with Next.js OG Images

Pitfall 1: Image Not Updating (Aggressive Caching)

The most common OG image problem: you deploy a new image, but Facebook, LinkedIn, or Slack still shows the old one. Social platforms cache og:image URLs aggressively — sometimes for weeks. Solutions:

  • Add a version query parameter when the image changes: /api/og?title=...&v=2
  • Use Facebook's Sharing Debugger or LinkedIn's Post Inspector to manually invalidate cached previews
  • Change the image filename or URL path (different URL = platform re-fetches)
  • In Next.js, use export const revalidate = 0 in your image route to set Cache-Control: no-cache

Pitfall 2: Wrong Dimensions

If your OG image is not exactly 1200×630, platforms will either crop it, letterbox it, or refuse to display a large card preview and fall back to a small thumbnail. Always set both width and height options in ImageResponse, and add the corresponding meta tags.

Pitfall 3: Localhost vs Production Differences

Social media crawlers cannot access localhost. Any OG image URL pointing to http://localhost:3000/api/og will fail when tested through Facebook's debugger or Twitter's card validator. Always test with a deployed URL — either your production domain or a Vercel preview deployment. For local testing, use ngrok to expose your local server with a public HTTPS URL.

Pitfall 4: External Images Blocked by CORS or Authentication

If your og:image URL returns a 401, 403, or redirects to a login page, crawlers cannot load it. All OG image URLs must be publicly accessible without any authentication headers. On Vercel, make sure your preview environments don't have password protection enabled when testing OG images.

Pitfall 5: Unsupported CSS in ImageResponse

ImageResponse/Satori does not support CSS Grid, CSS variables, position: absolute without a flex parent, or most pseudo-elements. Attempting to use unsupported CSS silently produces unexpected layouts — there are no console errors. Always test your ImageResponse layout at the exact target dimensions before deploying.

Pitfall 6: Missing twitter:image

Twitter/X does not always fall back to og:image — it prefers the explicit twitter:image meta tag. Always set both. In Next.js App Router's generateMetadata, include a twitter key alongside openGraph.

8. How to Test and Validate Your Next.js OG Image

Testing OG images before they go live prevents the embarrassment of sharing a broken link preview. Here's the recommended testing workflow:

  1. Test the raw image URL directly — Open yourdomain.com/api/og?title=Test+Title (or your opengraph-image route URL) directly in a browser. Verify the image looks correct at 1200×630.
  2. Use share-preview.com — Paste your page URL at share-preview.com to see accurate previews of how your link will appear on Facebook, Twitter/X, LinkedIn, WhatsApp, and Slack — simultaneously, with a full OG tag audit report. No need to post to each platform manually.
  3. Facebook Sharing Debuggerdevelopers.facebook.com/tools/debug — Shows the exact tags Facebook scraped and lets you force a cache refresh.
  4. LinkedIn Post Inspectorlinkedin.com/post-inspector — Shows LinkedIn's scraped data and clears their cache.
  5. Twitter Card Validatorcards-dev.twitter.com/validator — Validates Twitter Card tags and image rendering.

Fastest workflow: Deploy to a Vercel preview URL, then paste that preview URL into share-preview.com. You get all five platform previews in one view, plus any OG tag errors highlighted — without touching each platform's individual debugger tool.

9. Integration with Vercel OG Image Generation

Vercel (the company behind Next.js) provides additional tooling for OG image generation optimized for their platform:

@vercel/og Package

The @vercel/og package is the predecessor to next/og. If you're on an older Next.js version or want to use OG image generation outside of Next.js, @vercel/og works the same way — same ImageResponse API, same Satori rendering engine. Since Next.js 13.3, next/og is the canonical way to use this in Next.js projects.

Edge Runtime Optimization

Set export const runtime = 'edge' in your OG image route to run on Vercel's Edge Network. This reduces cold start times from seconds (Node.js serverless) to milliseconds (edge), which matters because social platform crawlers have short timeouts when fetching OG images. If your image takes more than ~2 seconds to generate, some crawlers skip it and show no preview image.

Image Caching on Vercel

Vercel caches edge function responses automatically. For dynamic OG images that change with content updates, use revalidate to control the cache TTL:

// Revalidate the image every hour
export const revalidate = 3600

// Or force no caching for always-fresh images
export const revalidate = 0

10. FAQ

In Next.js 13+ App Router, placing an opengraph-image.tsx file inside any route segment automatically generates an og:image meta tag for that route. The file can be a static image or a TSX file that exports an ImageResponse. Next.js handles URL generation and cache headers automatically — you don't write og:image tags manually.
ImageResponse is an API from the 'next/og' package that generates OG images dynamically using JSX and CSS — rendered server-side to PNG images. It's powered by Satori (HTML-to-SVG) and resvg-js. You define your image layout as JSX, and Next.js serves it as a PNG. It runs on the edge runtime for minimal latency.
The standard OG image size is 1200×630 pixels — the format recommended by Facebook, Twitter/X, and LinkedIn. Next.js's ImageResponse defaults to this size. Always specify width: 1200 and height: 630 explicitly in your ImageResponse options and include og:image:width and og:image:height meta tags to prevent platform rendering issues.
Social platform caching is the most common cause. Platforms cache og:image URLs aggressively — sometimes for weeks. To force refresh: (1) Use Facebook's Sharing Debugger or LinkedIn's Post Inspector to manually clear their cache, (2) append a version query parameter (?v=2) when you update the image, or (3) change the image URL path. In Next.js, you can also set export const revalidate = 0 to disable server-side caching on the image route.
The fastest way to test Next.js OG images across all platforms is share-preview.com — paste your page URL and instantly see platform-accurate previews for Facebook, Twitter/X, LinkedIn, WhatsApp, and Slack simultaneously. For localhost testing, use ngrok to expose your local server with a public HTTPS URL, or deploy to a Vercel preview environment.

See How Your Page Looks When Shared

Test your OG tags across Twitter, LinkedIn, Facebook, Slack, and WhatsApp instantly.

Test Share Preview Free →