背景
最近在为Oceanz.site 做 SEO 排查时,发现一个比较隐蔽的问题:网站已经提交 Sitemap 至 Google Search Console,搜索引擎确仅能检索出个别页面,绝大多数页面没有进入索引,无法被检索。
网站技术栈如下:
上线一段时间后,在 GSC 中发现:
- 实际收录页面数量几乎没有;
- site:oceanz.site 只能搜索到极少量页面;
同时在 GSC 的 Indexing -> Pages 中出现大量提示:
Duplicate, Google chose different canonical than user
原因
如果咨询 LLM ,大部分会建议先检查 Sitemap,同时审视内容质量,但是以上都不是原因,主要的原因是 Canonical 配置错误。
由于我在 App Router 的根布局中配置了默认 Metadata:
export const metadata = {
alternates: {
canonical: "https://www.oceanz.site/en",
},
}/en/articles/nextjs-canonical-hreflang-google-indexing
看到的却是:
<link rel="canonical" href="https://www.oceanz.site/en" />
<link rel="alternate" hreflang="en" href="https://www.oceanz.site/en" />
<link rel="alternate" hreflang="zh-CN" href="https://www.oceanz.site/zh-cn" />这相当于亲口告诉 当前这篇文章页不是独立页面,它的本体/规范页面其实是英文首页(canonical 指向了 /en)。中文对应版本也是中文首页(Hreflang 指向了 /zh-cn)。
听了指挥,就会把这篇文章的内容直接“合并”到首页里,不给文章页建立索引。由于所有文章都指向了首页, 就会认为网站有几十个页面全在“抄袭”首页,从而触发了之前看到的 "Duplicate, Google chose different canonical than user"(因为 觉得不对,于是自己瞎猜了一个)或者直接判定为内容重复而不予收录。
解决方式
让所有文章页都指向自己的规范页面,而不是首页。
让 canonical 和 languages 对应的链接根据当前页面实际的路径(slug)自动拼接。
由于使用的是 Next.js 静态导出(Static Export),在 generateMetadata 中无法直接读取运行时(Runtime)的请求头(比如 headers() 或 pathname),所以最标准、最稳妥的做法是接收路由参数(params)来动态拼接 URL。
例如 [locale]/articles/[slug]/page.tsx
import { Metadata } from 'next';
type Props = {
params: Promise<{ locale: string; slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, slug } = await params;
// 1. 正常加载当前文章的元数据(比如从 mdx 中读取的 title, description)
// const article = await getArticleData(slug, locale);
const baseUrl = 'https://www.oceanz.site';
// 规范化语言标签(GSC 对大小写敏感,建议全小写,或严格遵循标准)
const currentLocale = locale === 'zh-cn' ? 'zh-cn' : 'en';
// 动态拼接当前页面的实际相对路径
const relativePath = `/${currentLocale}/articles/${slug}`;
return {
metadataBase: new URL(baseUrl),
title: "文章标题", // article.title
description: "文章描述", // article.description
alternates: {
// 核心修改 1:Canonical 必须指向自己当前的动态完整 URL
canonical: relativePath,
// 核心修改 2:Hreflang 必须指向当前文章“对应的其他语言版本”的动态完整 URL
languages: {
'en': `/en/articles/${slug}`,
'zh-CN': `/zh-cn/articles/${slug}`,
'x-default': `/en/articles/${slug}`, // 推荐加上默认语言版本
},
},
};
}调整后的预期应类似如下:
<link rel="canonical" href="https://www.oceanz.site/en/articles/nextjs-canonical-hreflang-google-indexing" />
<link rel="alternate" hreflang="en" href="https://www.oceanz.site/en/articles/nextjs-canonical-hreflang-google-indexing" />
<link rel="alternate" hreflang="zh-CN" href="https://www.oceanz.site/zh-cn/articles/nextjs-canonical-hreflang-google-indexing" />
<link rel="alternate" hreflang="x-default" href="https://www.oceanz.site/en/articles/nextjs-canonical-hreflang-google-indexing" />Canonical
专业术语:
- Canonical URL(规范 URL)
- Canonical Tag(规范标签)
- Canonical Link Element(规范链接元素)
会影响索引归属,用于声明当前页面的权威 URL 是哪一个。
基本语法
<link rel="canonical" href="https://example.com/page" />放在页面的 <head> 中,使用绝对路径。
主要解决的问题: 同一内容可能通过多个 URL 被访问,搜索引擎会将其视为重复内容并分散权重:
- 参数页面:
/article?utm_source=newsletter、/article?ref=twitter - 重复 URL:
www与非www、HTTP 与 HTTPS、末尾斜杠等 - 权重归并:将分散的页面权重集中到指定的规范 URL 上
其他使用场景
- 自引用:即使没有重复版本,也建议加上自引用 canonical 以防止外部抓取产生的重复问题
- 跨域内容:转载或内容聚合时,指向原始来源的 URL
- 分页内容:将第 1 页声明为规范版本
注意事项
- 每个页面只放一个 canonical 标签,多个时搜索引擎会忽略
- Canonical 是「建议」而非「指令」,搜索引擎可能忽略
- 配合 301 重定向效果更佳,两者不冲突
与相关技术对比
| 方法 | 强制性 | 适用场景 |
|---|---|---|
canonical 标签 | 建议性 | 重复内容声明 |
| 301 重定向 | 强制性 | 永久迁移 URL |
noindex | 强制性 | 完全不索引该页 |
hreflang | 建议性 | 多语言页面区分 |
Hreflang
用于建立跨语言页面之间的关联。
专业术语:
- Hreflang Annotation(多语言标注)
- Alternate Language Tag(语言替代标签)
- Language Alternate Link(语言替代链接)
与 Canonical 的核心区别:不指定规范页面,也不进行权重归并,只负责告诉搜索引擎「这些页面是同一内容的不同语言版本」。
基本语法
放在每个语言页面的 <head> 中,所有语言版本互相声明:
- 英文页面:
<link rel="canonical" href="https://example.com/en/page" />
<link rel="alternate" hreflang="en" href="https://example.com/en/page" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-cn/page" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/page" />- 中文页面:
<link rel="canonical" href="https://example.com/zh-cn/page" />
<link rel="alternate" hreflang="en" href="https://example.com/en/page" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-cn/page" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/page" />核心原则
- 每个语言页面的 Canonical 指向自己,语言版本之间通过 Hreflang 建立关联
- 不要让中文页的 Canonical 指向英文页
- 不要让所有语言页面的 Canonical 都指向首页
x-default用于声明默认/兜底版本(通常是英文或语言选择页)
关于 "Google chose different canonical than user"
这个状态不一定意味着 Canonical 写错,它表示的是:你声明了一个 Canonical,但 Google 最终选择了另一个 URL 作为规范页面。
如果大量页面出现这个状态,应重点排查以下几项是否存在冲突:
canonical标签hreflang标注- Sitemap 收录的 URL
- 重定向(Redirect)规则
- 内链结构指向
结论
对于 Next.js App Router 国际化网站来说,Canonical 配置错误是一个非常容易忽略的问题。尤其是在根布局中配置静态 Metadata 时,很容易让所有页面继承同一个 Canonical,最终导致 Google 将大量页面识别为重复内容。如果网站出现:
- 收录数量异常少;
- Duplicate, Google chose different canonical than user;
- 多语言版本不收录,或者收录数量异常少;
那么第一件事应该检查的,就是页面最终输出的 Canonical 与 Hreflang 是否正确。