Tutorial6 min read

How to Add Open Graph Tags in Next.js (App Router)

A step-by-step guide to implementing Open Graph meta tags in Next.js 13+ App Router — using the Metadata API, dynamic opengraph-image.tsx, and per-page overrides.

Next.js App Router ships with a first-class Metadata API that makes it straightforward to add Open Graph tags — no manual <head> manipulation needed. This guide covers static metadata, dynamic metadata for data-fetched pages, and auto-generated OG images using the opengraph-image.tsx convention.

Static Metadata in layout.tsx or page.tsx

Export a metadata object from any page or layout. Next.js merges these up the tree, so a root layout provides defaults and individual pages override what they need:

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

export const metadata: Metadata = {
  title: {
    default: 'OGProof',
    template: '%s — OGProof',
  },
  description: 'Validate Open Graph tags across 6 social crawlers.',
  openGraph: {
    title: 'OGProof',
    description: 'Validate Open Graph tags across 6 social crawlers.',
    url: 'https://ogproof.veridux.ai',
    siteName: 'OGProof',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'OGProof',
    description: 'Validate Open Graph tags across 6 social crawlers.',
  },
}

Any page.tsx that doesn't export its own metadata will inherit the layout's values. Pages that do export metadata get their values merged on top — so you only override what changes.

Dynamic Metadata for Data-Fetched Pages

For pages where the title and description come from a database or API, export generateMetadata instead of a static object. Next.js calls this function at request time (or build time for SSG):

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

interface Props {
  params: Promise<{ slug: string }>
}

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

  if (!post) return { title: 'Not Found' }

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      url: `https://example.com/blog/${slug}`,
      type: 'article',
      publishedTime: new Date(post.publishedAt).toISOString(),
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
    },
  }
}
TipNext.js deduplicates fetch calls — if you fetch the same data in generateMetadata and in the page component, Next.js only hits the network once. No need to pass data between the two.

Auto-Generated OG Images with opengraph-image.tsx

Instead of creating a 1200×630 PNG for every page, use the opengraph-image.tsx file convention. Next.js uses Satori to render your React component to an image at build time or on the edge:

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

export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

interface Props {
  params: { slug: string }
}

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

  return new ImageResponse(
    (
      <div
        style={{
          background: '#0a0a14',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'flex-end',
          padding: 64,
          fontFamily: 'sans-serif',
        }}
      >
        <div style={{ fontSize: 14, color: '#7c3aed', marginBottom: 16 }}>
          YOUR SITE · BLOG
        </div>
        <div style={{ fontSize: 52, fontWeight: 700, color: 'white', lineHeight: 1.2 }}>
          {post.title}
        </div>
        <div style={{ fontSize: 20, color: 'rgba(255,255,255,0.5)', marginTop: 20 }}>
          {post.description}
        </div>
      </div>
    ),
    { ...size }
  )
}

Satori has one important constraint: it supports only inline styles, not CSS classes. Tailwind class names will not work inside ImageResponse. Everything must be written as a style={{ }} object using CSS-in-JS syntax. Also avoid HTML tags that Satori doesn't support — <br />, <svg> with external hrefs, and some form elements.

The Metadata Waterfall

Next.js merges metadata from outermost layout to innermost page. The merge rules are:

  1. 1.Root layout (app/layout.tsx) sets the baseline for all pages.
  2. 2.Nested layouts (app/blog/layout.tsx) override the root for their subtree.
  3. 3.Individual pages (app/blog/[slug]/page.tsx) override their layout for that route only.
  4. 4.Fields are merged shallowly at the top level — if a page exports openGraph, the entire openGraph object from the parent is replaced, not merged field by field.
TipBecause openGraph is replaced wholesale, always include all the og: fields you want in each page's metadata export — don't rely on inheritance for nested fields.

Validating Your Next.js OG Tags

After adding OG tags, you need to verify what crawlers actually see — not what your browser renders. Social crawlers don't run JavaScript and they ignore cookies and auth. They fetch the raw HTML from the server.

The fastest way to validate is OGProof: paste your URL and it fetches server-side, parses every OG tag, and renders accurate previews for Twitter, LinkedIn, Facebook, Discord, Slack, and iMessage. It also validates each tag against each platform's documented constraints and tells you exactly what to fix.

  • Checks that og:image is an absolute HTTPS URL
  • Validates image dimensions against per-platform minimums
  • Flags titles and descriptions that will be truncated
  • Detects missing twitter:card that causes Twitter to use a small thumbnail
  • Shows the actual HTML the crawler sees, not the JavaScript-rendered DOM

Validate your OG tags now

Paste any URL and see exactly how it renders on Twitter, LinkedIn, Facebook, Discord, Slack, and iMessage — in seconds.

Try OGProof free →