Next.js 元数据管理 🏷️
🎯 概述
Next.js 提供了强大的元数据 API,用于定义应用程序的元数据(如 title、description、Open Graph 标签等),优化 SEO 和社交媒体分享效果。
📦 为什么需要元数据管理?
元数据的重要性
- SEO 优化:帮助搜索引擎理解页面内容
- 社交分享:优化在社交媒体上的展示效果
- 用户体验:提供准确的页面标题和描述
- 品牌识别:统一的图标和主题色
🚀 基础用法
静态元数据
tsx
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '首页',
description: '欢迎来到我的网站',
}
export default function Page() {
return <h1>首页内容</h1>
}动态元数据
tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
}
}
export default function Page({ params }: Props) {
return <article>...</article>
}📝 元数据字段
基本字段
tsx
export const metadata: Metadata = {
// 页面标题
title: '我的网站',
// 页面描述
description: '这是一个很棒的网站',
// 关键词
keywords: ['Next.js', 'React', 'TypeScript'],
// 作者
authors: [{ name: '张三', url: 'https://example.com' }],
// 创建者
creator: '张三',
// 发布者
publisher: '我的公司',
// 版权信息
copyright: '© 2024 我的公司',
// 语言
alternates: {
canonical: 'https://example.com',
languages: {
'en-US': 'https://example.com/en',
'zh-CN': 'https://example.com/zh',
},
},
}标题模板
tsx
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | 我的网站',
default: '我的网站',
},
}tsx
// app/blog/page.tsx
export const metadata: Metadata = {
title: '博客', // 最终显示:博客 | 我的网站
}🌐 Open Graph 元数据
基本 Open Graph
tsx
export const metadata: Metadata = {
openGraph: {
title: '我的网站',
description: '这是一个很棒的网站',
url: 'https://example.com',
siteName: '我的网站',
locale: 'zh_CN',
type: 'website',
images: [
{
url: 'https://example.com/og-image.jpg',
width: 1200,
height: 630,
alt: '网站预览图',
},
],
},
}文章类型
tsx
export const metadata: Metadata = {
openGraph: {
type: 'article',
title: '文章标题',
description: '文章描述',
publishedTime: '2024-01-01T00:00:00.000Z',
modifiedTime: '2024-01-02T00:00:00.000Z',
authors: ['张三', '李四'],
tags: ['Next.js', 'React'],
images: [
{
url: 'https://example.com/article-image.jpg',
width: 1200,
height: 630,
},
],
},
}🐦 Twitter 卡片
Summary Card
tsx
export const metadata: Metadata = {
twitter: {
card: 'summary',
title: '我的网站',
description: '这是一个很棒的网站',
creator: '@myhandle',
images: ['https://example.com/twitter-image.jpg'],
},
}Large Image Card
tsx
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
title: '文章标题',
description: '文章描述',
creator: '@myhandle',
images: {
url: 'https://example.com/large-image.jpg',
alt: '文章预览图',
},
},
}🎨 图标和主题
Favicon
tsx
// app/layout.tsx
export const metadata: Metadata = {
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png',
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
}多尺寸图标
tsx
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/icon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icon-32x32.png', sizes: '32x32', type: 'image/png' },
],
apple: [
{ url: '/apple-icon-57x57.png', sizes: '57x57' },
{ url: '/apple-icon-72x72.png', sizes: '72x72' },
],
},
}主题颜色
tsx
export const metadata: Metadata = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#000000' },
],
}🤖 Robots 和索引
Robots 配置
tsx
export const metadata: Metadata = {
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}阻止索引
tsx
// app/admin/page.tsx
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
}📱 移动端优化
Viewport
tsx
// app/layout.tsx
export const metadata: Metadata = {
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
}App Links
tsx
export const metadata: Metadata = {
appleWebApp: {
capable: true,
title: '我的应用',
statusBarStyle: 'black-translucent',
},
}🔄 动态生成元数据
从 API 获取数据
tsx
// app/products/[id]/page.tsx
import { Metadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then((res) => res.json())
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [
{
url: product.image,
width: 1200,
height: 630,
},
],
},
}
}
export default function Page({ params }: Props) {
return <div>产品详情</div>
}继承父级元数据
tsx
// app/blog/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | 博客',
default: '博客',
},
description: '我的博客文章',
}
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title, // 会使用父级的 template
description: post.excerpt,
}
}🎯 实战示例
完整的博客文章元数据
tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
interface Post {
title: string
excerpt: string
content: string
author: string
publishedAt: string
updatedAt: string
coverImage: string
tags: string[]
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.example.com/posts/${slug}`)
return res.json()
}
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
const baseUrl = 'https://example.com'
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author }],
keywords: post.tags,
openGraph: {
type: 'article',
title: post.title,
description: post.excerpt,
url: `${baseUrl}/blog/${params.slug}`,
siteName: '我的博客',
locale: 'zh_CN',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author],
tags: post.tags,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
creator: '@myhandle',
images: [post.coverImage],
},
alternates: {
canonical: `${baseUrl}/blog/${params.slug}`,
},
}
}
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}电商产品页面
tsx
// app/products/[id]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id)
return {
title: `${product.name} - ${product.price}元`,
description: product.description,
openGraph: {
type: 'website',
title: product.name,
description: product.description,
images: product.images.map(img => ({
url: img.url,
width: 800,
height: 600,
alt: product.name,
})),
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.images[0].url],
},
other: {
'product:price:amount': product.price.toString(),
'product:price:currency': 'CNY',
},
}
}📚 最佳实践
1. 使用模板减少重复
tsx
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
title: {
template: '%s | 我的网站',
default: '我的网站 - 欢迎页',
},
description: '默认描述',
openGraph: {
siteName: '我的网站',
locale: 'zh_CN',
type: 'website',
},
}2. 优化图片尺寸
- Open Graph: 1200x630px
- Twitter Card: 1200x675px
- Favicon: 32x32px, 16x16px
- Apple Touch Icon: 180x180px
3. 描述长度建议
- Title: 50-60 字符
- Description: 150-160 字符
- Keywords: 5-10 个关键词
4. 使用 JSON-LD 结构化数据
tsx
export default function Page() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: '文章标题',
author: {
'@type': 'Person',
name: '张三',
},
datePublished: '2024-01-01',
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>...</article>
</>
)
}🔍 测试和验证
测试工具
- Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
- Twitter Card Validator: https://cards-dev.twitter.com/validator
- Google Rich Results Test: https://search.google.com/test/rich-results
- LinkedIn Post Inspector: https://www.linkedin.com/post-inspector/
本地测试
tsx
// 查看生成的 HTML
export default function Page() {
return (
<head>
{/* 元数据会自动注入到这里 */}
</head>
)
}🔗 相关资源
下一步:学习 Next.js 错误处理,了解如何优雅地处理应用错误。