同一篇内容有五个网址,搜索引擎该收录哪个
个人站长最常遇到的收录怪象之一:你明明只写了一篇文章,但在搜索引擎眼里却变成了好几条重复的 URL。打开抓取统计一看,同一条内容有下面这些地址都被抓过:
https://example.com/post/123.htmlhttps://example.com/post/123.html?utm_source=wechat&from=timelinehttps://example.com/post/123.html?ref=homehttps://example.com/index.php/post/123.htmlhttp://www.example.com/post/123.html(无 HTTPS、带 www)
这五条地址内容完全相同,但路径不同。搜索引擎的爬虫会分别抓取、分别建索引,最终的结果通常是:权重被摊薄、排名上不去,甚至主 URL 反而不在索引里。这就是典型的重复内容(duplicate content)问题。而 canonical 标签和 hreflang 标签,就是解决这个问题的两把标准钥匙。本文从原理讲到可直接抄的配置,重点放在个人站长最常踩的坑上。
搜索引擎如何看待「重复」
先纠正一个常见误解:搜索引擎并不会因为你有重复 URL 就惩罚你。Google 官方多次说明,重复内容本身不是惩罚项,它的问题是信号被稀释——多个 URL 各分到一部分外链和点击,导致任何一个都不够强。更麻烦的是「就近选择」:搜索引擎会自己挑一个它认为最合适的版本,而你未必喜欢它挑的那个。可能出现的结果包括:
- 用户参数版(带 utm)被收录,统计全乱;
- http 版被收录,用户点进去是「不安全」提示;
- 你精心优化的 www 版反而不在索引里;
- 老域名和迁移后的新域名同时存在,两边都半死不活。
所以处理重复内容的目标不是「避免惩罚」,而是把分散的信号收敛到你指定的那一个 URL 上。手段有三个层次:
- 301 重定向——最彻底,适用于你有权决定跳转的场景(http→https、非 www→www、带参数自动跳无参数)。
- canonical 标签——当 URL 必须保留但内容一样时使用(比如带参数的分页、排序、打印版)。
- robots.txt / noindex——用于确实不想让搜索引擎再出现的页面,但要注意与抓取的互斥关系(robots 屏蔽后搜索引擎看不到 noindex)。
判断顺序建议是:能 301 就 301,不能 301 就用 canonical,canonical 也不合适才考虑 noindex。下面逐层展开。
第一层:用 301 把网址收敛到一个规范形态
在讨论 canonical 之前,先把最基础的规范化做掉,因为很多站长的「重复内容」其实就是域名形态没统一。规范形态应该是唯一的:要么 https://www.example.com,要么 https://example.com,二者选一个,另一个 301 过去。Nginx 配置如下(以统一到 https + 非 www 为例):
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
root /www/example;
# ... 正式配置
}注意三个细节。第一个细节:return 301 里必须写 https:// 全前缀,写成 return 301 $scheme://... 有可能被 CDN 干扰,明确写死更安全。第二个细节:return 301 比 rewrite ... permanent 效率高,因为它不经过正则引擎。第三个细节:一定要用 $request_uri 保留原始路径和查询参数,否则带参数的 URL 跳过去会丢参数,反而让统计失真。
路径形态也要统一,比如是否带 index.php:
# 把 /index.php/foo.html 收敛到 /foo.html
if ($request_uri ~ ^/index\.php/(.*)$) {
return 301 https://example.com/$1;
}这条规则在很多 PHP CMS(Typecho、WordPress 默认结构)上很有用,因为 index.php 形式的 URL 会自动生成一份完全相同的副本。
第二层:canonical 标签的正确写法
canonical 是放在 <head> 里的一个 link 标签,形如:
<link rel="canonical" href="https://example.com/post/123.html">它的语义是:这一页有多个版本,请把当前这个版本视为规范版的副本,所有信号都算到 href 指定的那个 URL 上。
写法规则
- 必须是绝对 URL,包含协议和域名。写相对路径虽然多数引擎能猜,但规范不保证。
- 必须指向内容真正相同(或高度相似)的页面。指向内容不同的页面会被视为误导,严重时整条 canonical 被忽略。
- 自指(self-referencing)是推荐做法:规范页自己也要写一条指向自己的 canonical。这样能防止下游环节(CDN、参数拼接)产生意外副本。很多站长误以为规范页不用写,其实写上更稳。
- 一个页面只能有一条 canonical,写两条会导致引擎忽略全部。
- https/http、www/非 www 必须和你实际选择的规范形态完全一致,这也是最容易出错的地方:页面已经 301 到 https 了,canonical 里还写着 http。
个人站长最容易踩的三个 canonical 坑
坑一:把 canonical 指向了分页的前一页或首页。有些 SEO 教程说「聚合页都 canonical 到首页」,这个做法风险很高。如果你把一个有独立内容的分页 canonical 到首页,等于告诉搜索引擎「这些内容都不算数」,长期下来该分页的收录会消失。正确做法是让每个分页自指自己的 URL,或者对确实不需要收录的聚合页用 noindex,而不是用 canonical 制造「内容归属不明」。
坑二:canonical 与 CDN / 缓存拼接的域名不一致。有些 CDN 会自动把回源域名重写,导致渲染出来的 HTML 里 canonical 指向了源站域名(比如一个内网域名)。排查方法很简单:curl -s https://example.com/post/123.html | grep -i canonical,看域名对不对。这里必须用带时间戳的请求绕开缓存,否则你看到的是旧内容。
坑三:canonical 写成了动态参数版。如果模板里直接输出 $_SERVER['REQUEST_URI'],那么带 utm 的访问者看到的 canonical 就带 utm,等于每一条参数链接都自成一派,canonical 完全失效。正确做法是在模板层面拼装规范 URL:只取路径部分,丢掉查询串,或者在白名单里保留必要参数(比如分页的 page)。
// 反例:把参数也带进 canonical
$canonical = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
// 正例:只用路径,白名单保留必要参数
$path = strtok($_SERVER['REQUEST_URI'], '?');
$canonical = 'https://example.com' . $path;第三层:hreflang —— 多语言/多地区站点的信号互认
如果你的站点有简体中文和繁体中文两个版本,或者有面向香港、台湾、新加坡的不同分站,那么问题就不只是「哪一个是规范版」,而是「这三个版本我都想要,分别是给不同人群的」。这种场景不能用 canonical 把所有版本收敛到一个,因为那等于放弃其他语言版本。此时该用 hreflang。
<link rel="alternate" hreflang="zh-Hans" href="https://example.com/zh-cn/post/123.html">
<link rel="alternate" hreflang="zh-Hant" href="https://example.com/zh-tw/post/123.html">
<link rel="alternate" hreflang="en" href="https://example.com/en/post/123.html">
<link rel="alternate" hreflang="x-default" href="https://example.com/post/123.html">关键点:
- 互指是硬要求。A 页面声明了 B,B 页面也必须声明 A,否则整组 hreflang 会被引擎判为无效。这是最常见的失败原因,务必用工具核对。
- 语言代码要用标准写法。中文简体是
zh-Hans,繁体是zh-Hant;地区限定如zh-HK、zh-TW也可用,但不要写zh-cn之外的方言式写法。 - x-default 是兜底,用于「语言不匹配任何一条时的默认版」,通常指向主站或不带语言前缀的页面。
- hreflang 和 canonical 可以共存但语义要分清:hreflang 表示「这组是多语言兄弟」,canonical 表示「这一个才是主版本」。如果各语言版本内容确实不同,每个版本应自指自己的 canonical;如果只是同一语言的区域变体,则可以把区域变体 canonical 到主语言版。
怎么验证 canonical 和 hreflang 真的生效了
光写完配置不算数,必须验证。四步走:
# 1. 确认页面输出的 canonical 是规范的绝对地址
curl -s "https://example.com/post/123.html?_=$(date +%s)" \
| grep -o '<link rel="canonical"[^>]*>'
# 2. 确认 hreflang 组完整(应能看到全部语言版本)
curl -s "https://example.com/zh-cn/post/123.html?_=$(date +%s)" \
| grep -o 'hreflang="[^"]*"'
# 3. 确认带参数版本指向同一个 canonical
curl -s "https://example.com/post/123.html?utm_source=test&_=$(date +%s)" \
| grep -o '<link rel="canonical"[^>]*>'
# 4. 确认 301 链路只有一跳,不要链条
curl -sI "http://www.example.com/post/123.html" | grep -iE '^(HTTP|location)'第 4 条尤其重要:重定向链(A→B→C)会损耗权重,应做到 A→C 一跳到位。上面给出的 Nginx 配置是 http 直接 301 到最终的 https 非 www 形态,只有一跳;如果写成「http 先跳 https 再跳非 www」,就是两跳,效率更差。
在搜索引擎侧,Google Search Console 的「网址检查」可以直接看到「用户声明的规范网页」和「Google 选择的规范网页」是否一致,不一致就说明你的 canonical 没被采纳;「重复网页,用户已指定规范网页」这类提示则是正常的。百度搜索资源平台的「抓取诊断」与「索引量」可以用来观察收敛是否生效。
实践中的常见问答
Q:canonical 指向的页面被 noindex 了会怎样?
A:这是典型的配置冲突。canonical 说「我是副本,主版本在那边」,而主版本自己又说不许收录,结果是两边都进不了索引。排错时务必把 canonical 链上的页面状态一起看。
Q:分页的第二页要不要 canonical 到第一页?
A:不要。分页是独立内容,应自指。rel="next"/rel="prev" 已不被 Google 作为索引依据,但你仍可用它改善爬虫对序列的理解,只是别指望它解决收录。
Q:CDN 缓存了旧的 canonical 怎么办?
A:先确认源站输出正确,再刷新 CDN 缓存。验证时必须带 ?_=时间戳 或 Cache-Control: no-cache,否则看到的可能是一直没更新的旧 HTML。这一点在排查「改了没生效」类问题时几乎是万能第一步。
Q:站内搜索结果页、标签聚合页怎么处理?
A:这类页面内容由参数动态生成,价值低且容易产生海量副本,建议用 robots.txt 屏蔽抓取路径(如 Disallow: /search),同时在页面上加 noindex 作为双保险——但要注意只有允许抓取,noindex 才会被读到,所以更稳的做法是「允许抓取 + noindex」,而不是直接 Disallow。这个取舍在 robots.txt 相关文章里值得单独展开,这里记住结论即可:想让页面退出索引,用 noindex;不想让页面被抓取消耗预算,用 Disallow;两者同时用时要理解互斥关系。
小结
重复内容不是一个「要被惩罚」的问题,而是一个「信号被稀释」的问题。处理思路按优先级排序:先把域名和路径形态用 301 收敛到唯一规范形态,再用 canonical 处理无法跳转的参数副本,多语言站点用 hreflang 建立互认关系。三条纪律必须守住:canonical 写绝对地址并与最终形态一致、canonical 链上的页面不能自相矛盾、改完必须带时间戳实测渲染结果。做到这三点,你的站内权重就能收敛到真正该排名的那些 URL 上。