Next.js Metadata: SEO and Social Tags
Every page you publish should tell search engines and social networks what it's about—via a meaningful title, description, and preview image. The App Router makes this effortless with metadata exports. You can define metadata statically in layout.tsx (for global tags) or dynamically in page.tsx via generateMetadata (for unique titles per post, product, or page). This article covers building SEO-friendly pages that rank well and look great when shared on Twitter, Facebook, and LinkedIn.
Static Metadata in Layouts
The simplest way to add metadata is a static export in layout.tsx or page.tsx:
// app/layout.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "My Blog - React Tips and Tricks",
description: "A blog about React, Next.js, and web development.",
keywords: ["react", "nextjs", "web development"],
openGraph: {
type: "website",
url: "https://myblog.com",
title: "My Blog",
description: "React tips and tricks",
images: [
{
url: "https://myblog.com/og-image.png",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
site: "@mybloghandle",
creator: "@myhandle",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head></head>
<body>{children}</body>
</html>
);
}
Next.js automatically adds these tags to the <head>:
<title>My Blog - React Tips and Tricks</title>
<meta name="description" content="A blog about React, Next.js, and web development." />
<meta name="keywords" content="react,nextjs,web development" />
<meta property="og:type" content="website" />
<meta property="og:title" content="My Blog" />
<meta property="og:image" content="https://myblog.com/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
Search engines and social networks crawl these tags to index your content and generate previews.
Dynamic Metadata with generateMetadata
For pages with unique content (blog posts, products, etc.), generate metadata dynamically. Use the generateMetadata function:
// app/blog/[slug]/page.tsx
import { Metadata } from "next";
interface BlogPost {
title: string;
excerpt: string;
author: string;
date: string;
image: string;
}
// Mock database
const posts: Record<string, BlogPost> = {
"react-hooks": {
title: "React Hooks: A Complete Guide",
excerpt: "Master React Hooks with practical examples.",
author: "Jane Doe",
date: "2026-06-02",
image: "https://myblog.com/hooks.png",
},
"nextjs-ssr": {
title: "Server-Side Rendering with Next.js",
excerpt: "Learn to build fast, SEO-friendly apps.",
author: "John Smith",
date: "2026-05-28",
image: "https://myblog.com/ssr.png",
},
};
async function getBlogPost(slug: string): Promise<BlogPost> {
return posts[slug];
}
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getBlogPost(params.slug);
if (!post) {
return {
title: "Post Not Found",
description: "The blog post you're looking for doesn't exist.",
};
}
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
type: "article",
title: post.title,
description: post.excerpt,
images: [
{
url: post.image,
width: 1200,
height: 630,
},
],
authors: [post.author],
publishedTime: post.date,
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getBlogPost(params.slug);
if (!post) {
return <div>Post not found.</div>;
}
return (
<article className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-2">{post.title}</h1>
<p className="text-gray-600 mb-2">By {post.author}</p>
<p className="text-gray-500 mb-8">{post.date}</p>
<img
src={post.image}
alt={post.title}
className="w-full rounded-lg mb-8"
/>
<div className="prose max-w-none">
<p>{post.excerpt}</p>
</div>
</article>
);
}
Now each blog post generates unique SEO tags based on its content. When someone shares /blog/react-hooks on Twitter, the card displays "React Hooks: A Complete Guide" and its preview image.
Metadata Fields Reference
| Field | Purpose |
|---|---|
title | Page title (search results, browser tab) |
description | Meta description (search snippets) |
keywords | Comma-separated keywords |
authors | Array of author objects (name, url, etc.) |
openGraph.type | Article, website, book, etc. |
openGraph.title | Title for social shares |
openGraph.description | Description for social shares |
openGraph.images | Array of image URLs (1200x630 px ideal) |
openGraph.publishedTime | ISO 8601 date (for articles) |
twitter.card | summary_large_image, summary, app |
twitter.title | Twitter card title |
twitter.description | Twitter card description |
twitter.images | Array of image URLs for Twitter |
twitter.creator | Twitter handle of author (@username) |
Generating OG Images Dynamically
For popular posts, create custom Open Graph images on-the-fly using libraries like @vercel/og:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Blog Post Cover";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
async function getBlogPost(slug: string) {
// Fetch post metadata
return { title: "React Hooks Guide", author: "Jane Doe" };
}
export default async function OgImage({ params }: { params: { slug: string } }) {
const post = await getBlogPost(params.slug);
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: "white",
textAlign: "center",
padding: "40px",
}}
>
<h1 style={{ fontSize: 60, marginBottom: 20 }}>{post.title}</h1>
<p style={{ fontSize: 32, opacity: 0.8 }}>By {post.author}</p>
</div>
),
{
...size,
}
);
}
When someone shares your blog post, instead of a generic preview, they'll see a beautiful, branded image with the post title and author.
Canonical URLs
For duplicate content or SEO best practices, declare the canonical URL:
export const metadata: Metadata = {
title: "My Article",
description: "...",
alternates: {
canonical: "https://myblog.com/article",
},
};
This tells search engines, "If you see this content elsewhere, treat this URL as the original."
Robots and Sitemap Meta
Control search engine crawling and add sitemap URLs:
export const metadata: Metadata = {
robots: {
index: true, // Allow indexing
follow: true, // Follow links
nocache: false, // Allow caching
},
alternates: {
canonical: "https://myblog.com/",
},
};
Best Practices
1. Keep Titles 50-60 Characters – Longer titles truncate in search results:
// Good
title: "React Hooks: A Complete Guide (2026)"
// Bad
title: "A Comprehensive and Detailed Guide to React Hooks for Beginners and Advanced Users"
2. Descriptions 120-160 Characters – Include keywords naturally:
description: "Master React Hooks with practical examples. Learn useState, useEffect, custom hooks, and best practices. Updated for React 19."
3. Use High-Quality Images – 1200x630 px, under 200 KB:
images: [
{
url: "https://myblog.com/og-image.png", // Exact size
width: 1200,
height: 630,
alt: "Blog post preview",
},
]
4. Match Page Content to Metadata – Inconsistent titles/descriptions confuse search engines. If your title says "React Hooks Guide" but the page is about "Vue Components," Google will rewrite your title.
Key Takeaways
- Static metadata in
layout.tsxapplies globally; dynamic metadata inpage.tsxviagenerateMetadatais unique per page. - OpenGraph tags make previews on Twitter, Facebook, LinkedIn look professional and increase click-through rates.
- Canonical URLs prevent duplicate content penalties in search results.
- OG images generated dynamically with
@vercel/ogare more engaging than generic previews. - Keep titles 50-60 chars, descriptions 120-160 chars—these are Google's recommended lengths.
Frequently Asked Questions
Do I need both OpenGraph and Twitter tags?
No, but it's recommended. OpenGraph is a standard used by Facebook, LinkedIn, and others. Twitter has its own twitter:* tags, which override OpenGraph if both are present. Define both for maximum compatibility.
Can I generate metadata from a database?
Yes. generateMetadata is an async function; fetch from your database or external API:
export async function generateMetadata({ params }) {
const post = await db.getPost(params.slug);
return { title: post.title, description: post.excerpt };
}
Does metadata affect SEO ranking?
Yes and no. Metadata doesn't directly influence rankings, but:
- Title and description affect click-through rate from search results (CTR is a ranking factor).
- OpenGraph tags don't affect Google ranking but boost social sharing, which drives traffic.
- Canonical URLs prevent duplicate content issues.
Can I use metadata in client components?
No. generateMetadata is a server-only function. Client components can't export it. Define metadata in the route's layout.tsx or page.tsx.
How often should I update metadata?
Update it whenever content changes. Use revalidatePath or revalidateTag to refresh cached metadata in production.
Further Reading
- Next.js Metadata API – official API reference and examples.
- Open Graph Protocol – og: tags specification and usage.
- Twitter Card Documentation – twitter: tags reference.
- Vercel OG Image Generation – dynamic OG image API.