大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
元数据与国际化
这一课你将能做到
- 用
generateMetadata和静态metadata对象为任意页面配置 SEO 元数据 - 用
generateStaticParams为动态路由预生成所有静态页面 - 用
app/i18n/目录或路由段实现多语言站点 - 用 Middleware 实现基于地理位置或浏览器语言的重定向
- 用
sitemap.ts和robots.ts自动生成搜索引擎需要的文件
为什么要学
SEO 和国际化是商业网站的生死线。没有正确的 <title> 和 <meta name="description">,搜索引擎不会收录;没有多语言支持,海外市场直接放弃。Next.js 的 App Router 把元数据从「手写 <head>」变成了「声明式导出」,而且支持动态生成——商品详情页的标题自动带商品名,博客文章的分享卡片自动带封面图。
人话:元数据是网站的「名片」,国际化是网站的「翻译官」。名片印错了,客户找不到你;没有翻译官,外国客户进不来。
先建立图像
元数据生成方式:
├── 静态 metadata(export const metadata) ← 固定内容页面
├── 动态 generateMetadata(async function) ← 数据驱动页面
└── 文件约定(icon.png, opengraph-image.png) ← favicon、社交卡片
多语言路由结构:
app/
├── [lang]/
│ ├── layout.tsx ← 接收 lang 参数,注入字典
│ ├── page.tsx ← /zh/ 或 /en/
│ └── about/page.tsx ← /zh/about 或 /en/about
├── middleware.ts ← 检测语言,重定向到 /zh 或 /en
└── dictionaries/
├── zh.json
└── en.json
跟着做
步骤 1:静态 metadata
app/page.tsx:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '首页',
description: '这是我的 Next.js 学习项目首页',
keywords: ['Next.js', 'React', '学习'],
openGraph: {
title: '首页',
description: '分享时的标题',
images: ['/og-image.png'],
},
};
export default function Home() {
return <h1>首页</h1>;
}
构建后 HTML 的 <head> 自动包含:
<title>首页 | 我的站</title>
<meta name="description" content="这是我的 Next.js 学习项目首页">
<meta property="og:title" content="首页">
<meta property="og:image" content="https://example.com/og-image.png">
步骤 2:动态 generateMetadata
app/blog/[slug]/page.tsx:
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface Props {
params: Promise<{ slug: string }>;
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (!res.ok) return null;
return res.json();
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: '文章不存在' };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
每篇文章的标题、描述、分享卡片都根据内容动态生成。
步骤 3:generateStaticParams 预生成
配合动态路由,构建时预生成所有已知页面:
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: any) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: Props) {
// ...
}
构建时 Next.js 会为每个 slug 生成静态 HTML,运行时直接返回缓存。
步骤 4:多语言路由
app/[lang]/layout.tsx:
import { notFound } from 'next/navigation';
const locales = ['zh', 'en'];
export function generateStaticParams() {
return locales.map(lang => ({ lang }));
}
export default async function LangLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
if (!locales.includes(lang)) {
notFound();
}
return (
<html lang={lang === 'zh' ? 'zh-CN' : 'en'}>
<body>{children}</body>
</html>
);
}
app/[lang]/page.tsx:
import { getDictionary } from './dictionaries';
export default async function Home({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
return (
<main>
<h1>{dict.title}</h1>
<p>{dict.description}</p>
</main>
);
}
app/[lang]/dictionaries.ts:
import 'server-only';
const dictionaries = {
zh: () => import('./dictionaries/zh.json').then(m => m.default),
en: () => import('./dictionaries/en.json').then(m => m.default),
};
export const getDictionary = async (locale: string) =>
dictionaries[locale as keyof typeof dictionaries]();
app/[lang]/dictionaries/zh.json:
{
"title": "欢迎来到我的网站",
"description": "这是一个多语言 Next.js 示例"
}
app/[lang]/dictionaries/en.json:
{
"title": "Welcome to My Site",
"description": "This is a multilingual Next.js example"
}
步骤 5:Middleware 语言重定向
middleware.ts:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['zh', 'en'];
const defaultLocale = 'zh';
function getLocale(request: NextRequest): string {
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage?.includes('zh')) return 'zh';
return defaultLocale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// 检查路径是否已包含语言前缀
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// 重定向到带语言前缀的路径
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
访问 /about 自动重定向到 /zh/about(或根据浏览器语言到 /en/about)。
步骤 6:sitemap.ts 和 robots.ts
app/sitemap.ts:
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const posts = [
{ slug: 'hello-world', updatedAt: '2024-01-01' },
{ slug: 'nextjs-guide', updatedAt: '2024-01-15' },
];
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.8,
})),
];
}
app/robots.ts:
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/'],
},
sitemap: 'https://example.com/sitemap.xml',
};
}
构建后自动可用:/sitemap.xml 和 /robots.txt。
讲清楚要点
- metadata 的继承与覆盖:子路由的 metadata 会覆盖父路由的同名字段,数组字段(如
keywords)会合并。根 layout 的metadata是全局默认值。 - generateMetadata 是服务端函数:只在服务端执行,可以访问数据库、调用 API。返回的对象会被合并到
<head>。 - generateStaticParams 决定哪些路径被预渲染:返回空数组表示全部走 SSR;返回部分路径表示这些路径 SSG,其余 SSR。
- 多语言的两种方案:① 子路径方案
/zh/about(推荐,SEO 友好);② 域名方案zh.example.com(需要 DNS 配置)。App Router 用[lang]动态段实现子路径方案。 - Middleware 是 Edge Runtime:在边缘节点执行,延迟极低。适合做重定向、A/B 测试、鉴权。但不能做重计算,不能访问 Node.js API。
- 文件约定元数据:
app/icon.png自动生成 favicon;app/opengraph-image.png自动生成 og:image;app/twitter-image.png自动生成 twitter:image。
你可能会懵的地方
-
generateMetadata 和 generateStaticParams 能同时用吗? 能。
generateStaticParams决定哪些路径被预渲染,generateMetadata在渲染时为每个路径生成元数据。两者独立工作。 -
动态路由的 metadata 能根据搜索参数变化吗? 不能。
generateMetadata只接收params(路由参数),不接收searchParams(查询参数)。查询参数导致的页面变化不影响元数据。 -
多语言站点的 URL 结构哪种最好? 子路径
/zh/about最好:SEO 权重集中在一个域名,实现简单,用户切换语言方便。子域名zh.example.com适合品牌独立运营的场景。 -
Middleware 里能读写 Cookie 吗? 能。
request.cookies读,response.cookies写。适合基于 Cookie 的语言偏好、主题切换。 -
server-only包是干嘛的? 防止敏感代码被意外打包到客户端。import 'server-only'后,如果该模块被 Client Component 引用,构建时报错。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 在 Client Component 里 export metadata | 不支持,metadata 是 Server API | 把 metadata 拆到 Server layout/page |
generateMetadata 里用 window | 服务端没有 window | 只在服务端逻辑里操作 |
| 多语言字典直接 import 全部 JSON | 打包体积大 | 动态 import,按需加载 |
| Middleware 里做数据库查询 | Edge Runtime 不支持 Node.js API | 用轻量 API 或提前计算好 |
忘记 export const config 的 matcher | Middleware 在所有路径执行 | 精确配置 matcher,排除静态资源 |
卡点急救
- metadata 不生效:确认是在 Server Component 里导出的;确认没有 Client Component 在中间截断;检查构建后的 HTML 是否包含对应 meta 标签。
- generateStaticParams 构建时报错:确保返回的数组元素是对象,键名和动态段名一致。
{ slug: 'hello' }对应[slug]。 - 多语言重定向循环:检查 Middleware 的 matcher 是否排除了
/_next/和/api/;检查语言检测逻辑是否稳定。 - sitemap.xml 404:确认文件在
app/sitemap.ts;确认部署后访问的是/sitemap.xml不是/sitemap。 - 动态 import 字典时报类型错误:给 JSON 文件加类型声明,或用
as const断言。
面试常问
-
Next.js 的 SEO 优化手段有哪些? ① 静态/动态 metadata 自动生成
<title>和<meta>;② 语义化 HTML 结构;③ SSG/SSR 保证内容在 HTML 中;④sitemap.ts和robots.ts自动生成;⑤<Image>的 alt 属性;⑥ 结构化数据(JSON-LD)手动注入。 -
generateMetadata 和 generateStaticParams 的执行时机?
generateStaticParams在构建时执行,决定哪些路径被预渲染;generateMetadata在渲染时执行(构建时或请求时),为每个路径生成元数据。 -
多语言站点的实现方案对比? 子路径
/zh/about:SEO 最好,实现简单,推荐;子域名zh.example.com:品牌独立,DNS 配置复杂;查询参数?lang=zh:SEO 差,不推荐。 -
Middleware 和 Route Handler 的区别? Middleware 在请求到达路由前执行,适合做重定向、重写、鉴权;Route Handler 是具体的 API 端点,处理业务逻辑。Middleware 是「门卫」,Route Handler 是「服务员」。
自测三问
generateMetadata能访问searchParams吗?为什么?- 一个多语言站点用子路径方案,URL 是
/zh/about,对应的app/目录结构是什么? - Middleware 的
matcher配置['/((?!api|_next).*)']是什么意思?
小结一句话
元数据是 Next.js 的声明式 SEO:静态页面用 export const metadata,动态页面用 generateMetadata,多语言用 [lang] 路由段 + Middleware 重定向,sitemap 和 robots 用文件约定自动生成——不用手写 <head>,全部代码化。
延伸
- Next.js 官方 - Metadata
- Next.js 官方 - Internationalization
- 下一课:性能与可观测——Core Web Vitals、代码分割、懒加载、Analytics、错误监控。