Next.js Metadata API — SEO Best Practices
Advertisement
Next.js Metadata API — SEO Best Practices
The Metadata API provides a declarative way to define SEO-friendly metadata for your pages without worrying about HTML head tag management.
- Next.js Metadata API — SEO Best Practices
- Basic Metadata
- Dynamic Metadata
- Open Graph Metadata
- Canonical URLs
- Robots and Indexing
- Structured Data (JSON-LD)
- Layout Metadata Merging
- Icons and App Manifest
- Viewport Settings
- Real-World Example: Blog Post SEO
- FAQ
Basic Metadata
Define metadata in any page or layout file:
// app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home - My Blog',
description: 'Welcome to my tech blog with articles on Next.js and React.',
}
export default function HomePage() {
return <div>Home page content</div>
}
Next.js automatically adds these to the HTML head.
Dynamic Metadata
Generate metadata dynamically:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
async function getPost(slug: string) {
const post = await db.posts.findUnique({ where: { slug } })
return post
}
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return {
title: 'Post Not Found',
}
}
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author }],
keywords: post.tags,
}
}
export default async function PostPage({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
Open Graph Metadata
Add social media sharing metadata:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://example.com/blog/${post.slug}`,
siteName: 'My Blog',
images: [
{
url: post.imageUrl,
width: 1200,
height: 630,
alt: post.title,
}
],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.imageUrl],
creator: '@yourhandle',
},
}
}
export default async function PostPage({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
Canonical URLs
Prevent duplicate content issues:
// app/products/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Products',
canonical: 'https://example.com/products',
alternates: {
canonical: 'https://example.com/products',
languages: {
es: 'https://example.com/es/products',
fr: 'https://example.com/fr/products',
},
},
}
export default function ProductsPage() {
return <div>Products list</div>
}
Robots and Indexing
Control search engine crawling:
// app/admin/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
robots: {
index: false, // Don't index this page
follow: false, // Don't follow links
nocache: true,
},
}
export default function AdminPage() {
return <div>Admin dashboard</div>
}
Structured Data (JSON-LD)
Add structured data for rich snippets:
// app/products/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await getProduct(params.id)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.image,
price: product.price,
ratingValue: product.rating,
reviewCount: product.reviewCount,
}
return {
title: product.name,
description: product.description,
other: {
'json-ld': JSON.stringify(jsonLd),
},
}
}
export default function ProductPage({ params }) {
const product = getProduct(params.id)
return <div>{product.name}</div>
}
Layout Metadata Merging
Metadata cascades through layouts:
// app/layout.tsx (Root layout)
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My Blog',
default: 'My Blog',
},
description: 'Tech blog about web development',
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://example.com',
siteName: 'My Blog',
},
}
export default function RootLayout({ children }) {
return <html>{children}</html>
}
// app/blog/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Blog',
}
export default function BlogLayout({ children }) {
return <div>{children}</div>
}
When viewing /blog, the title becomes "Blog | My Blog" from the template.
Icons and App Manifest
Define app icons and manifest:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'My awesome application',
icons: {
icon: '/favicon.ico',
shortcut: '/favicon.ico',
apple: '/apple-icon.png',
},
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
},
manifest: '/manifest.json',
}
export default function RootLayout({ children }) {
return <html>{children}</html>
}
Viewport Settings
Configure viewport behavior:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
}
export default function RootLayout({ children }) {
return <html>{children}</html>
}
Real-World Example: Blog Post SEO
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
async function getPost(slug: string) {
return db.posts.findUnique({
where: { slug },
include: { author: true, tags: true }
})
}
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return { title: 'Not Found' }
}
const url = `https://example.com/blog/${post.slug}`
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name, url: post.author.website }],
keywords: post.tags.map(t => t.name),
canonical: url,
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
}
],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPostPage({ params }) {
const post = await getPost(params.slug)
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Person',
name: post.author.name,
},
}
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div>{post.content}</div>
</article>
)
}
FAQ
Q: How do I check if my metadata is correct? A: Use Google's Rich Results Test or Meta's Sharing Debugger to validate your metadata.
Q: Can I change metadata based on user preferences? A: Metadata is generated server-side, so you can check user preferences but can't use client-side state.
Q: What about meta descriptions length? A: Keep descriptions between 120-160 characters for optimal display in search results.
Proper SEO metadata significantly improves search engine visibility and social sharing.
Advertisement