How to Set Up Open Graph Meta Tags in Next.js (App Router)
So you've built a Next.js app, shipped it to production, and shared the link on Twitter. And then... nothing. No image. No nice preview card. Just a sad, bare URL sitting there.
Yeah, I've been there too.
Setting up Open Graph tags in Next.js sounds simple until you actually try to do it. The docs show you the happy path, but they don't warn you about the gotchas that'll waste your afternoon. Let me save you some time.
The Basics: Next.js Metadata API
If you're using the App Router (Next.js 13+), forget about next/head. That's Pages Router stuff. Instead, you've got two options: static metadata objects or the generateMetadata function.
Here's the simplest version that actually works:
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Cool App',
description: 'It does cool things',
openGraph: {
title: 'My Cool App',
description: 'It does cool things',
url: 'https://mycoolapp.com',
siteName: 'My Cool App',
images: [
{
url: 'https://mycoolapp.com/og-image.png',
width: 1200,
height: 630,
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'My Cool App',
description: 'It does cool things',
images: ['https://mycoolapp.com/og-image.png'],
},
}
Notice how I'm duplicating the title and description? That's intentional. The openGraph and twitter objects need their own values—they don't automatically inherit from the top-level fields.
The metadataBase Problem
Here's where most people hit their first wall. You deploy to Vercel, test your OG tags, and see this warning:
metadata.metadataBase is not set for resolving social open graph or twitter images
What's happening? Next.js needs a base URL to resolve relative image paths. Without it, your images point to localhost:3000 in production. Not ideal.
Fix it in your root layout:
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://mycoolapp.com'),
// ... other metadata
}
But wait—what if you're deploying to Vercel and have preview deployments? Each commit gets a unique URL. You can't hardcode your production URL or preview deployments will break.
Here's what actually works:
// app/layout.tsx
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000'
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || baseUrl),
}
Set NEXT_PUBLIC_SITE_URL to your production domain in Vercel's environment variables. Now production uses your real domain, and previews use the Vercel deployment URL.
Dynamic Metadata for Blog Posts
Static metadata is fine for your homepage, but what about blog posts or product pages? You need generateMetadata:
// app/blog/[slug]/page.tsx
import { 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) // your data fetching function
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
}
}
One thing that trips people up: generateMetadata only works in Server Components. You can't use it in a client component, and you can't export both a static metadata object and a generateMetadata function from the same file.
The File-Based Approach
Next.js has a neat trick: drop an opengraph-image.png file in your route folder, and it automatically generates the meta tags. No code needed.
app/
opengraph-image.png // Used for all routes by default
blog/
opengraph-image.png // Overrides for /blog routes
[slug]/
page.tsx
This is great for static images, but what about dynamic images with post titles baked in?
Generating Dynamic OG Images
This is where next/og comes in. You can generate images on-the-fly with JSX:
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'My App'
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1a1a2e',
color: 'white',
fontSize: 60,
fontWeight: 700,
}}
>
{title}
</div>
),
{
width: 1200,
height: 630,
}
)
}
Then reference it in your metadata:
openGraph: {
images: [`/api/og?title=${encodeURIComponent(post.title)}`],
}
A few gotchas with ImageResponse:
- Only flexbox works. No CSS Grid.
- Font loading is tricky. You'll need to fetch and pass font files manually.
- The bundle size limit is 500KB, including fonts and images.
- It only runs on the Edge runtime.
Why Your OG Image Isn't Showing
Debugging OG tags is frustrating because social platforms cache aggressively. Here's my checklist when things break:
1. Check the HTML source
View your page source and look for the meta tags. If they're not there, your metadata isn't being rendered server-side.
2. Use absolute URLs
Relative URLs don't work. Your image URL must be https://example.com/image.png, not /image.png.
3. Check image size
Twitter limits images to 5MB. The recommended size is 1200x630 pixels. Larger images will silently fail on some platforms.
4. Clear platform caches
Facebook, Twitter, and LinkedIn all cache OG data. Use their debugger tools:
5. Make sure your image is publicly accessible
If your image is behind authentication or returns a redirect, crawlers won't be able to fetch it.
Quick Checklist Before You Ship
-
metadataBaseis set in your root layout - OG image is at least 1200x630 pixels
- Image file is under 5MB
- All URLs are absolute (include
https://) - Tested with platform debugger tools
- Images are publicly accessible (no auth, no redirects)
Wrapping Up
Getting OG tags right in Next.js isn't hard once you know the gotchas. Set your metadataBase, use absolute URLs, keep your images under 5MB, and test with the platform debuggers before you share that link.
The Metadata API is actually pretty nice once you understand it. And if you want to check how your OG tags look across different platforms without manually testing each one, tools like OG Check can show you previews for Facebook, Twitter, LinkedIn, and Discord all at once.
References
- Getting Started: Metadata and OG images - Next.js Documentation
- Functions: generateMetadata - Next.js API Reference
- Metadata Files: opengraph-image and twitter-image - Next.js File Conventions
- Facebook Sharing Debugger - Meta for Developers