
metadata object for static content and the generateMetadata function for dynamic pages.key prop to each tag within next/head, and ensure crawlers see your tags by fetching data server-side with getServerSideProps or getStaticProps.You've spent hours meticulously setting up your Next.js project with the perfect meta tags. You've added your og:image and crafted a compelling description that would make any SEO expert proud. But when you share the link on Twitter or Facebook, the preview is completely broken. Or maybe you've inspected your page only to discover duplicate <meta> tags appearing after navigation.
If you're nodding along in frustration, you're not alone. These are common headaches for Next.js developers, especially when everything looks perfect on localhost but mysteriously fails once deployed to Vercel.
"On localhost the meta tags are showing but when I deploy my app to vercel, the og meta tags are not displaying in page source or in the inspector console." - Reddit user
This disconnect between what you expect and what actually happens stems from a fundamental misunderstanding of how Next.js handles meta tags in its different rendering environments. Let's demystify this once and for all.
The root of most Next.js meta tag issues lies in a critical distinction that's often overlooked: what search engines see versus what humans see in the browser.
Search engine crawlers, social media bots, and tools like the Facebook Sharing Debugger don't typically execute JavaScript. They only read the initial HTML document that your server sends. This creates a fundamental rule for debugging:
Always use "View Page Source" (Ctrl+U or Cmd+Opt+U) to see what crawlers see.
This differs dramatically from using "Inspect Element," which shows you the live DOM after JavaScript has run. If your tags look perfect in the inspector but are missing from the page source, crawlers won't see them either.
Another common point of confusion is the necessity of multiple meta tag types. Is it really necessary to have standard HTML meta tags, Open Graph tags, AND Twitter Cards?
Yes, and here's why:
<meta name="description"> that search engines like Google use to understand your page content.<meta name="description" content="A guide to fixing meta tag issues in Next.js" />
og:) control how your content appears when shared on Facebook, LinkedIn, and many other platforms.<meta property="og:title" content="Why Your Next.js Meta Tags Aren't Working" />
<meta property="og:image" content="https://yourdomain.com/og-image.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Fix Your Next.js Meta Tags" />
Each set serves a different purpose, and omitting any can lead to poor presentation on different platforms. Now let's dive into solutions for both the App Router and Pages Router.
If you're using Next.js 13+ with the App Router, you have access to the powerful Metadata API - the officially recommended approach for managing your <head> elements.
For tags that don't change between renders, export a metadata object directly from your layout or page file:
// app/page.tsx or app/blog/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Next.js Blog',
description: 'A blog built with Next.js and the App Router',
openGraph: {
title: 'My Next.js Blog',
description: 'A blog built with Next.js and the App Router',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My Next.js Blog',
description: 'A blog built with Next.js and the App Router',
images: ['/og-image.jpg'],
}
}
For content that depends on dynamic data (like a blog post title based on its slug), use the generateMetadata function:
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// Fetch blog post data
const post = await fetch(`https://api.example.com/blog/${params.slug}`).then((res) => res.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.ogImage || '/default-og.jpg'],
}
}
}
A common question among developers is: "Is it okay to have metadata in both layout.jsx and index page.jsx?"
Yes! Next.js uses a powerful inheritance model for metadata. It reads the metadata object from the root layout, then merges it with metadata from nested layouts and pages, with the most specific values taking precedence.
This means you can define common metadata in your root layout and override specific fields in individual pages.
While the App Router supports streaming (progressively rendering parts of your page), crawlers need metadata in the initial HTML chunk. Fortunately, Next.js automatically defers streaming until metadata is generated for known crawler user-agents.
For dynamic Open Graph images, the App Router provides the ImageResponse constructor from next/server:
// app/og/route.tsx
import { ImageResponse } from 'next/server'
export const runtime = 'edge'
export async function GET() {
return new ImageResponse(
(
<div
style={{
display: 'flex',
background: 'black',
width: '100%',
height: '100%',
}}
>
<h1 style={{ color: 'white' }}>Dynamic OG Image</h1>
</div>
),
{
width: 1200,
height: 630,
}
)
}
You can test your dynamic OG images using Vercel's OG Playground.
If you're using the Pages Router with <Head> from next/head, here are the most common issues and their solutions:
Symptom: When navigating between pages, you see duplicate meta tags in the inspector.
Cause: When you navigate, Next.js might not unmount old tags before mounting new ones.
Solution: Add a unique key prop to each <meta> and <link> tag within <Head>:
// pages/about.js
import Head from 'next/head'
function AboutPage() {
return (
<>
<Head>
<title key="title">About Us</title>
<meta name="description" content="About our company" key="desc" />
<meta property="og:title" content="About Us" key="og-title" />
</Head>
{/* Page content */}
</>
)
}
The key props ensure React knows to replace these elements rather than adding new ones during client-side navigation.
_document.jsSymptom: Meta tags appear duplicated or aren't being updated properly.
Cause: Using <Head> from next/document and <Head> from next/head for the same tags.
Solution: Keep your _document.js minimal. It should only contain tags that apply to every page and never change:
// pages/_document.js - KEEP THIS CLEAN
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* Only put unchanging, global tags here */}
<meta charSet="utf-8" />
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
All other meta tags—titles, descriptions, social tags—should go in page components using import Head from 'next/head'.
Symptom: Tags look perfect in the browser inspector but are missing when you "View Page Source" or in social media previews.
Cause: You're likely fetching data on the client side (e.g., in a useEffect hook) before setting meta tags, but crawlers only see the server-rendered HTML.
Solution: Fetch data on the server using getServerSideProps or getStaticProps:
// pages/posts/[id].js
import Head from 'next/head'
function Post({ post }) {
return (
<>
<Head>
<title key="title">{post.title}</title>
<meta name="description" content={post.excerpt} key="desc" />
<meta property="og:title" content={post.title} key="og-title" />
<meta property="og:image" content={post.ogImage} key="og-image" />
</Head>
{/* Post content */}
</>
)
}
export async function getServerSideProps(context) {
const res = await fetch(`https://api.example.com/posts/${context.params.id}`)
const post = await res.json()
return { props: { post } }
}
export default Post
This ensures your meta tags are included in the initial HTML that crawlers receive.
When your meta tags aren't working as expected, follow this debugging process:
The most critical step is to check what's in your page source (Ctrl+U or Cmd+Opt+U in most browsers). If your tags aren't here, search engines and social media bots can't see them.
These tools act like crawlers and show you exactly what they see:
og: tags.twitter: tags.Use browser dev tools to inspect your page after client-side navigation. If you see duplicates, you likely need to add key props to your meta tags in the Pages Router.
Successfully implementing meta tags in Next.js comes down to understanding the rendering pipeline and how crawlers interact with your site.
For the App Router, trust the Metadata API. Use the metadata object for static content and generateMetadata for dynamic pages.
For the Pages Router, use next/head, always add key props to prevent duplication, keep _document.js clean, and fetch data server-side with getServerSideProps or getStaticProps.
By following these guidelines and using the debugging toolkit, you can ensure your Next.js application is perfectly optimized for search engines and social media platforms. The key is always remembering the distinction between what crawlers see (the page source) and what users see (the rendered DOM).
Mastering meta tags is a core skill for technical SEO. With the right approach, you can ensure your carefully crafted content looks great everywhere it's shared, improving both discoverability and user engagement.
Synscribe helps B2B companies with SEO & GEO using programmatic SEO approach. Book a call to find out how we help you win.