You built a Next.js site and the share preview shows a blank card. The App Router ships a Metadata API that replaces manual <head> tags. Use it correctly and crawlers get og:title, og:image, and the rest in server HTML on the first request.
Short answer
In Next.js App Router, define Open Graph tags through the metadata export for static pages or generateMetadata for dynamic routes. Set openGraph.title, openGraph.description, openGraph.images, and openGraph.url inside the metadata object. Next.js renders them as <meta property="og:*"> tags in the initial HTML response. Always use absolute HTTPS URLs for images. After deploy, scan the live URL with OpenGraph Check before sharing.
Why Next.js previews break
Social crawlers request HTML once and read the <head>. They do not run React or wait for client hydration. Common Next.js mistakes that break link previews:
- Meta tags defined only in a client component
- Relative
og:imagepaths like/og.jpgwithout a base URL generateMetadatafetching draft content that differs from production- Image routes behind auth or returning redirects
- Missing tags on nested layouts when child pages override metadata incorrectly
The Metadata API solves this when you keep all OG values on the server.
Static metadata export
For pages with fixed content, export a metadata object from page.tsx or layout.tsx:
import type { Metadata } from "next";export const metadata: Metadata = { title: "Product Launch 2026", description: "Everything you need to know about our new release.", openGraph: { title: "Product Launch 2026", description: "Everything you need to know about our new release.", url: "https://example.com/launch", siteName: "Example Co", images: [ { url: "https://example.com/og/launch.jpg", width: 1200, height: 630, alt: "Product launch hero image", }, ], locale: "en_US", type: "website", }, twitter: { card: "summary_large_image", title: "Product Launch 2026", description: "Everything you need to know about our new release.", images: ["https://example.com/og/launch.jpg"], },};Next.js converts this into proper meta tags in the document head. Child layouts merge with parent layouts unless you call metadataBase at the root.
Set metadataBase once at the root
Relative image paths only work when metadataBase is set in the root layout:
export const metadata: Metadata = { metadataBase: new URL("https://example.com"), openGraph: { images: ["/og/default.jpg"], },};Without metadataBase, Next.js may emit broken relative URLs that crawlers reject. Set it in app/layout.tsx and every relative path resolves to an absolute URL.
Dynamic routes with generateMetadata
Blog posts, product pages, and CMS-driven content need async metadata:
import type { Metadata } from "next";type Props = { params: Promise<{ slug: string }> };export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await getPost(slug); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, url: `https://example.com/blog/${slug}`, type: "article", publishedTime: post.publishedAt, authors: [post.author.name], images: [ { url: post.ogImageUrl, width: 1200, height: 630, }, ], }, };}Fetch data inside generateMetadata, not in a client hook. The function runs on the server during the HTML request.
og:image strategies in Next.js
Static files in /public
Place og.jpg in public/ and reference it:
openGraph: { images: ["https://example.com/og.jpg"],}Simple and reliable. Good for marketing pages with one shared image.
Dynamic OG images with ImageResponse
Next.js supports dynamic OG image generation via opengraph-image.tsx:
import { ImageResponse } from "next/og";export const runtime = "edge";export const alt = "Blog post title";export const size = { width: 1200, height: 630 };export default async function Image({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); return new ImageResponse( ( <div style={{ fontSize: 60, background: "#000", color: "#fff", width: "100%", height: "100%" }}> {post.title} </div> ), { ...size } );}Next.js automatically wires the route as og:image. Test the generated image URL directly in a browser tab after deploy.
Remote images from a CMS
When images come from Contentful, Sanity, or another CDN, pass the full HTTPS URL. Confirm the CDN allows crawler access without cookies. See How to Fix a Missing OG Image if previews fail.
Layout inheritance and overrides
Metadata merges from root layout down to the page. A child page replaces parent title and openGraph fields it defines. If a child sets openGraph: { title: "About" } without images, it may drop parent images depending on merge behavior.
Safe pattern: define defaults in root layout and override only what changes per page:
// app/layout.tsx - defaultsexport const metadata: Metadata = { metadataBase: new URL("https://example.com"), openGraph: { siteName: "Example Co", images: ["/og/default.jpg"], },};// app/about/page.tsx - page-specificexport const metadata: Metadata = { title: "About Us", openGraph: { title: "About Us", description: "Our story and team.", url: "https://example.com/about", },};Twitter Card tags alongside Open Graph
Next.js maps twitter in metadata to twitter:* tags. X falls back to Open Graph, but explicit tags give more control. Read Open Graph vs Twitter Card for the full comparison.
Common mistakes in Next.js projects
Client-side only metadata
Never set OG tags with next/head in the App Router or inside "use client" components. Crawlers will not see them.
Testing on localhost
Social crawlers cannot reach localhost. Deploy to a public HTTPS URL first. See How to Test Open Graph Tags.
Wrong image dimensions
Use 1200 × 630 px for a safe default. Platform-specific guidance in Open Graph Image Size Guide.
Forgetting og:url
Set openGraph.url to the canonical URL you share. Mismatch between shared link and og:url confuses crawlers and analytics.
Stale cache after deploy
Platforms cache previews. After changing tags, re-scrape with platform debuggers. See Why Social Platforms Cache Link Previews.
Step-by-step verification workflow
- Deploy the page to production HTTPS
- View Page Source and search for
og:title - Open the
og:imageURL in a new tab (expect 200 OK) - Paste the URL into OpenGraph Check
- Confirm previews on Facebook, LinkedIn, and WhatsApp look correct
- Run Facebook Sharing Debugger if posting to Meta apps
FAQ
Does the Metadata API work with the Pages Router?
The Pages Router uses next/head or a custom _document.tsx. The Metadata API is App Router only. Migrate to app/ for the cleaner API.
Can I use generateMetadata with static export?
output: "export" supports static metadata exports. Dynamic generateMetadata that fetches at request time requires a server. Static export bakes metadata at build time.
Where do og tags appear in the HTML?
In the <head> section of the server-rendered HTML. Verify with View Page Source, not the React DevTools Elements panel.
How do I set og:image for a specific route only?
Export route-level metadata from that route's page.tsx or add an opengraph-image.tsx file in the route folder.
Does Next.js set og:type automatically?
It defaults to website. Set openGraph.type: "article" for blog posts with publishedTime and authors.
Why does my dynamic og image show the wrong title?
Check that generateMetadata and opengraph-image.tsx fetch the same slug and data source. Race conditions or stale ISR cache can cause mismatches. Revalidate after CMS updates.
Should I duplicate title in openGraph and metadata.title?
Yes. metadata.title sets the document title and may add a template suffix. openGraph.title controls the share card headline. They can differ if you want a shorter social headline.
Bottom line
Next.js App Router makes Open Graph tags straightforward when you use the Metadata API on the server. Set metadataBase, export metadata or generateMetadata with absolute image URLs, deploy, and verify with OpenGraph Check. Skip client-side head injection and your link previews will match what you designed.