Next.js
元数据与 OG 图片
深入理解 Next.js 元数据和 OG 图片优化,包括静态元数据、动态元数据、Open Graph 图片和文件约定
概述
Metadata API 可用于定义应用程序元数据,以改善 SEO 和 Web 可分享性,包括:
- 静态
metadata对象 - 动态
generateMetadata函数 - 特殊文件约定,用于添加静态或动态生成的 favicon 和 OG 图片
Next.js 会自动为页面生成相关的 <head> 标签。
metadata 对象和 generateMetadata 函数导出仅在 Server Components 中支持。
默认字段
即使路由未定义元数据,也始终添加两个默认 meta 标签:
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />静态元数据
从 layout.js 或 page.js 文件导出 Metadata 对象来定义静态元数据。
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'My App Description',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}基本字段
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home',
description: 'Welcome to my website',
keywords: ['Next.js', 'React', 'JavaScript'],
}
export default function Page() {
return <h1>Home Page</h1>
}标题模板
使用 title.template 为子页面定义标题模板:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
}import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
}
// 输出: <title>About | My App</title>绝对标题
使用 title.absolute 忽略父布局中的 title.template:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
absolute: 'About',
},
}
// 输出: <title>About</title>动态元数据
使用 generateMetadata 函数根据动态信息生成元数据。
import type { Metadata } from 'next'
type Props = {
params: Promise<{ id: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params
const product = await fetch(`https://api.example.com/products/${id}`).then(
(res) => res.json()
)
return {
title: product.title,
description: product.description,
}
}
export default async function Page({ params }: Props) {
const { id } = await params
return <div>Product {id}</div>
}使用父元数据
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: Promise<{ id: string }>
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { id } = await params
const product = await fetch(`https://api.example.com/products/${id}`).then(
(res) => res.json()
)
// 访问并扩展父元数据
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}元数据字段
title
export const metadata: Metadata = {
title: 'My Page Title',
}description
export const metadata: Metadata = {
description: 'My page description',
}keywords
export const metadata: Metadata = {
keywords: ['nextjs', 'react', 'javascript'],
}authors
export const metadata: Metadata = {
authors: [{ name: 'John Doe' }, { name: 'Jane Doe', url: 'https://example.com' }],
}creator
export const metadata: Metadata = {
creator: 'John Doe',
}publisher
export const metadata: Metadata = {
publisher: 'Acme Inc',
}robots
export const metadata: Metadata = {
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: false,
noimageindex: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}alternates
export const metadata: Metadata = {
alternates: {
canonical: 'https://example.com',
languages: {
'en-US': 'https://example.com/en-US',
'zh-CN': 'https://example.com/zh-CN',
},
},
}icons
export const metadata: Metadata = {
icons: {
icon: '/icon.png',
shortcut: '/shortcut-icon.png',
apple: '/apple-icon.png',
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
}openGraph
export const metadata: Metadata = {
openGraph: {
title: 'My Page Title',
description: 'My page description',
url: 'https://example.com',
siteName: 'My Site',
images: [
{
url: 'https://example.com/og.png',
width: 800,
height: 600,
},
{
url: 'https://example.com/og-alt.png',
width: 1800,
height: 1600,
alt: 'My custom alt',
},
],
locale: 'en_US',
type: 'website',
},
}export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
title: 'My Page Title',
description: 'My page description',
siteId: '1467726470533754880',
creator: '@nextjs',
creatorId: '1467726470533754880',
images: ['https://example.com/twitter-image.png'],
},
}verification
export const metadata: Metadata = {
verification: {
google: 'google-site-verification-code',
yandex: 'yandex-verification-code',
yahoo: 'yahoo-verification-code',
other: {
me: ['my-email', 'my-link'],
},
},
}Open Graph 图片
静态 OG 图片
在路由段中添加 opengraph-image.(jpg|jpeg|png|gif) 文件:
app/
├── opengraph-image.png
└── about/
└── opengraph-image.pngNext.js 会自动生成相应的 <meta> 标签。
动态 OG 图片
使用 opengraph-image.tsx 或 opengraph-image.jsx 文件:
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'About Acme'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image() {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
About Acme
</div>
),
{
...size,
}
)
}使用外部数据
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'Post'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((res) =>
res.json()
)
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<h1>{post.title}</h1>
<p>{post.description}</p>
</div>
),
{
...size,
}
)
}使用自定义字体
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export default async function Image() {
const interSemiBold = fetch(
new URL('./Inter-SemiBold.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'Inter',
}}
>
Hello world!
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await interSemiBold,
style: 'normal',
weight: 600,
},
],
}
)
}Twitter 图片
静态 Twitter 图片
在路由段中添加 twitter-image.(jpg|jpeg|png|gif) 文件:
app/
├── twitter-image.png
└── about/
└── twitter-image.png动态 Twitter 图片
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'About Acme'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image() {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
About Acme
</div>
),
{
...size,
}
)
}Favicons
静态 Favicon
在 app 目录中添加 favicon.ico 文件:
app/
└── favicon.ico动态 Favicon
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = {
width: 32,
height: 32,
}
export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 24,
background: 'black',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
}}
>
A
</div>
),
{
...size,
}
)
}Apple Icon
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = {
width: 180,
height: 180,
}
export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'black',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
borderRadius: '20%',
}}
>
A
</div>
),
{
...size,
}
)
}Sitemap
静态 Sitemap
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://acme.com</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
<changefreq>yearly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://acme.com/about</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>动态 Sitemap
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://acme.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://acme.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://acme.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}从 API 生成 Sitemap
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `https://acme.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.5,
}))
return [
{
url: 'https://acme.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://acme.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
...postEntries,
]
}Robots.txt
静态 Robots
User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://acme.com/sitemap.xml动态 Robots
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://acme.com/sitemap.xml',
}
}多个用户代理
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: 'Googlebot',
allow: ['/'],
disallow: '/private/',
},
{
userAgent: ['Applebot', 'Bingbot'],
disallow: ['/'],
},
],
sitemap: 'https://acme.com/sitemap.xml',
}
}元数据继承和合并
替换字段
export const metadata = {
title: 'Acme',
openGraph: {
title: 'Acme',
description: 'Acme is a...',
},
}export const metadata = {
title: 'About',
openGraph: {
title: 'About',
},
}
// 输出:
// <title>About</title>
// <meta property="og:title" content="About" />继承字段
export const metadata = {
title: 'Acme',
openGraph: {
title: 'Acme',
description: 'Acme is a...',
},
}export const metadata = {
title: 'About',
}
// 输出:
// <title>About</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme is a..." />最佳实践
1. 在根布局中定义默认元数据
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
description: 'My App Description',
openGraph: {
siteName: 'My App',
locale: 'en_US',
type: 'website',
},
}2. 为每个页面定义特定元数据
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
description: 'Learn more about us',
}3. 使用动态元数据获取数据
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
}
}4. 为 OG 图片使用推荐尺寸
- Open Graph: 1200x630
- Twitter Card: 1200x630
- Favicon: 32x32
- Apple Icon: 180x180
5. 提供 Sitemap 和 Robots.txt
export default async function sitemap() {
const posts = await getAllPosts()
return [
{ url: 'https://example.com', priority: 1 },
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
priority: 0.8,
})),
]
}6. 使用验证标签
export const metadata: Metadata = {
verification: {
google: 'your-google-verification-code',
},
}总结
Next.js Metadata 和 OG Images 提供了强大的 SEO 优化功能:
- 静态元数据:使用
metadata对象定义静态元数据 - 动态元数据:使用
generateMetadata函数根据动态数据生成元数据 - Open Graph 图片:支持静态和动态 OG 图片生成
- 文件约定:使用特殊文件名自动生成 favicon、OG 图片等
- Sitemap:自动或动态生成 sitemap.xml
- Robots.txt:配置搜索引擎爬虫规则
- 元数据继承:子路由自动继承父路由的元数据
- ImageResponse:使用 JSX 和 CSS 动态生成图片
通过合理使用这些功能,可以显著提升应用的 SEO 和社交媒体分享体验。
在 GitHub 上编辑
上次更新于