跳到主要内容

Next.js Metadata、资源与 SEO

如果把 SEO 理解成“改个 title”,放在 Next 项目里通常会太浅。

更完整的理解应该是:

  • 页面标题和描述
  • Open Graph / Twitter Card
  • canonical
  • robots
  • sitemap
  • manifest
  • 图标、OG 图、分享图
  • 动态页面的 metadata 生成方式

到了 App Router,这一整套已经被 Next 收成了官方 Metadata API 和 metadata file conventions。

先看最核心的两条线

1. metadata / generateMetadata

用来定义页面或布局的 <head> 信息。

2. metadata files

用来定义一些特殊文件或资源,例如:

  • robots.ts
  • sitemap.ts
  • manifest.ts
  • opengraph-image.tsx
  • icon.tsx

这两条线一起组成了现代 Next 项目里最常见的 SEO 和分享能力。

metadata 适合静态信息

import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Docs',
description: 'Project documentation',
};

export default function Page() {
return <div>Docs</div>;
}

这适合:

  • 固定标题
  • 固定描述
  • 固定分享信息
  • 稳定页面段落

generateMetadata 适合动态信息

import type { Metadata } from 'next';

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);

return {
title: post.title,
description: post.summary,
};
}

适合:

  • 博客详情页
  • 商品详情页
  • 用户资料页
  • 动态内容型页面

一个关键点是:generateMetadata 本身就是渲染的一部分。如果页面还能预渲染,而且 metadata 逻辑没有引入真正的动态行为,结果仍然可以进首屏 HTML。

这套 API 的边界

官方文档明确说明:

  • metadatagenerateMetadata 只支持 Server Components
  • 它们不是给 client component 用的

这很合理,因为 <head> 管理本来就更适合在服务端模型里完成。

最常见的 metadata 字段

标题与描述

export const metadata = {
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
};

metadataBase

export const metadata = {
metadataBase: new URL('https://example.com'),
};

这个字段很重要,因为很多相对路径 metadata 最终都要基于它展开。

Open Graph

export const metadata = {
openGraph: {
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
url: 'https://example.com/docs/next',
siteName: 'Example',
images: [
{
url: '/og/next.png',
width: 1200,
height: 630,
alt: 'Next.js Guide',
},
],
type: 'article',
},
};

Twitter Card

export const metadata = {
twitter: {
card: 'summary_large_image',
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
images: ['/og/next.png'],
},
};

canonical

export const metadata = {
alternates: {
canonical: 'https://example.com/docs/next',
},
};

这在多语言、分页、重复内容路径场景里尤其重要。

viewport 和旧字段的变化

较新的文档里,官方已经明确标注:

  • themeColormetadata 里已废弃
  • colorSchememetadata 里也已废弃

现在更推荐走 viewport 配置。

robots.ts 怎么理解

robots.ts 是一类特殊 metadata file。

import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin', '/private'],
},
sitemap: 'https://example.com/sitemap.xml',
};
}

它的价值主要是:

  • 明确告诉搜索引擎哪些路径该抓、哪些别抓
  • 把 sitemap 暴露给爬虫

常见误区是把它当成安全控制。实际上 robots.txt 只是爬虫协作协议,不是访问控制机制。

sitemap.ts 怎么理解

import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();

return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
})),
];
}

适合:

  • 文档站
  • 博客
  • 商品站点
  • 内容量较大的内容平台

较新的官方文档还提供了 generateSitemaps(),用来把超大站点拆成多份 sitemap。

manifest.ts

import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Example Docs',
short_name: 'Example',
description: 'Documentation site',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#111111',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
],
};
}

这和 SEO 不完全等价,但它常常和安装体验、PWA 外观、品牌感知放在一起整理,所以通常会归到这一组。

图标、OG 图、分享图

Next 还支持通过特殊文件约定处理:

  • favicon.ico
  • icon.png / icon.tsx
  • apple-icon.png
  • opengraph-image.png / opengraph-image.tsx
  • twitter-image.png / twitter-image.tsx

这类文件定义后,Next 会自动把相关 head 信息接起来。

动态页面里最容易踩的坑

1. canonical 和分享图没跟着动态路由变化

结果通常是:

  • 所有详情页看起来 title 不同
  • 但分享卡片或 canonical 还指向同一个固定地址

2. metadataBase 没设好

相对地址可能会在部署后生成错误链接。

3. sitemap 只放了静态页

内容型站点如果详情页没进 sitemap,抓取效率通常会差很多。

4. robots 当成权限控制

robots.txt 不能阻止真实访问,只能表达抓取建议。

一套比较稳的落地顺序

  1. 先在根布局补 metadataBase、全站 title template、description
  2. 再给关键详情页补 generateMetadata
  3. 再补 robots.tssitemap.ts
  4. 再补 OG 图、Twitter 图、manifest 和图标
  5. 最后处理多语言 canonical、分页 canonical、分区爬虫策略

常见页面类型怎么配

文档站

重点:

  • title template
  • canonical
  • sitemap
  • robots
  • OG 图

博客

重点:

  • 文章级 generateMetadata
  • 文章更新时间进 sitemap
  • article 类型的 openGraph

电商或商品站

重点:

  • 商品详情 metadata 动态生成
  • 分类与详情的 canonical
  • 大规模 sitemap 拆分

图片、字体与第三方脚本

这块经常被拆成三个零散话题来看:

  • next/image
  • next/font
  • next/script

但放到一起看更容易理解。它们其实都在做同一类事:

  • 优化首屏资源加载
  • 减少布局抖动
  • 控制第三方资源对性能的影响
  • 让资源加载策略更明确

next/image 解决什么问题

官方的 <Image /> 不是普通 <img> 的简单封装,它主要解决:

  • 自动尺寸优化
  • 现代格式输出
  • 原生懒加载
  • 避免布局偏移
  • 远程图片安全白名单控制
import Image from 'next/image';

export default function Avatar() {
return (
<Image
src="/profile.png"
alt="Profile"
width={500}
height={500}
/>
);
}

为什么它比 <img> 更值得默认用

最直接的原因有三个:

  • 宽高信息明确,能避免 CLS
  • 浏览器不会一次性把所有图片都抢先拉下来
  • 可以根据设备和视口输出更合适的资源尺寸

本地图片和远程图片的区别

本地图片

放在项目内,Next 更容易推断资源。

远程图片

要显式允许来源。

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/products/**',
search: '',
},
],
},
};

export default nextConfig;

官方文档现在更推荐 remotePatterns,而不是旧式 domains。新的方向更细,也更安全。

<Image /> 最常见的坑

1. 没有宽高

如果不是 fill 模式,就应该提供 widthheight

2. 远程图片白名单太宽

配置越泛,越容易引入滥用和安全边界问题。官方也强调要尽量写得具体。

3. 把所有首屏图都设成高优先级

较新的版本里,priority 已经走到废弃路线,新的方向是 preload。但本质没变:

  • 只有真正关键的首屏图才值得提前加载
  • 不要把整页所有图都当成关键资源

4. 还在使用 next/legacy/image

这通常说明项目还带着 13 之前的历史包袱,应该逐步迁到现在的 next/image

next/font 解决什么问题

字体是性能里很容易被低估的一项。

官方的 next/font 主要做两件事:

  • 自动优化字体加载
  • 自动自托管字体文件,避免浏览器直接请求 Google Fonts
import { Geist } from 'next/font/google';

const geist = Geist({
subsets: ['latin'],
display: 'swap',
});

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
);
}

为什么 next/font 很值得默认采用

  • 减少外部网络请求
  • 隐私更好
  • 更容易控制 display 策略
  • 更容易避免字体导致的布局跳动

本地字体怎么接

import localFont from 'next/font/local';

const brandFont = localFont({
src: './fonts/Brand-Regular.woff2',
display: 'swap',
variable: '--font-brand',
});

这在品牌项目、中文字体、自定义字库场景里很常见。

字体最常见的几个判断

全局统一字体

通常放根布局。

某个专题或品牌块单独字体

可以局部定义,避免把全站都绑进去。

多字体系统

尽量明确层级和变量,不要让字体定义到处散落。

next/script 解决什么问题

第三方脚本经常是性能问题大户。next/script 的价值在于:

  • 控制脚本什么时候加载
  • 控制加载范围
  • 避免在路由切换时重复插入同一脚本
import Script from 'next/script';

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>{children}</body>
<Script src="https://example.com/analytics.js" />
</html>
);
}

Script 的四种策略

beforeInteractive

在任何 Next 代码和页面水合之前加载。

适合:

  • 极早期必须可用的脚本
  • 很少数必须抢在应用前执行的能力

afterInteractive

默认策略。页面发生一部分 hydration 后尽早加载。

适合:

  • 分析脚本
  • 常规第三方 SDK

lazyOnload

在浏览器空闲时加载。

适合:

  • 非关键统计
  • 不影响主流程的增强脚本

worker

实验态,把脚本放进 web worker。

但官方文档也明确提醒:

  • 这还是实验能力
  • 在 App Router 里还不能稳定使用

所以现在不适合作为默认方案。

脚本应该挂在哪

根布局

适合全站都要用的脚本。

某个子布局

适合某个业务区块才会用的脚本。

某个页面

适合范围非常明确的第三方能力。

官方也明确建议:尽量把第三方脚本限制在必要页面或布局,而不是全站一股脑注入。

最常见的第三方脚本问题

1. 脚本放太早

首屏被第三方拖慢。

2. 脚本挂太高

明明只有一个页面用,却放进全站布局。

3. 直接手写 <script>

能跑,但失去了 Next 对策略和复用的控制。

4. 图库、播放器、广告脚本直接上 SSR

这类组件很多天然偏浏览器环境,必要时要配合客户端组件、动态导入甚至 ssr: false 来处理。

一套比较稳的资源加载思路

  1. 图片默认先用 next/image
  2. 字体默认先用 next/font
  3. 第三方脚本默认先用 next/script
  4. 只有确认边界特殊时,再退回更底层写法

什么时候该保守一点

图片

  • 首屏关键图数量别太多
  • 远程来源范围别放太宽

字体

  • 不要一次挂太多字重和子集
  • 中文字体尤其要控制体积

脚本

  • 先判断是不是真的必须全站加载
  • 先判断是不是必须在首屏前执行

推荐继续往下看

  1. 国际化、多语言与路由策略
  2. 部署、自托管与 standalone
  3. 渲染、数据获取与水合

参考资料

推荐继续往下看

  1. 国际化、多语言与路由策略
  2. 部署、自托管与 standalone
  3. Streaming、Suspense 与 PPR

参考资料