Skip to main content

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

FieldPurpose
titlePage title (search results, browser tab)
descriptionMeta description (search snippets)
keywordsComma-separated keywords
authorsArray of author objects (name, url, etc.)
openGraph.typeArticle, website, book, etc.
openGraph.titleTitle for social shares
openGraph.descriptionDescription for social shares
openGraph.imagesArray of image URLs (1200x630 px ideal)
openGraph.publishedTimeISO 8601 date (for articles)
twitter.cardsummary_large_image, summary, app
twitter.titleTwitter card title
twitter.descriptionTwitter card description
twitter.imagesArray of image URLs for Twitter
twitter.creatorTwitter 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.tsx applies globally; dynamic metadata in page.tsx via generateMetadata is 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/og are 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