内容站的”重复”很少是两篇一模一样的文章。几乎都是两篇文章在打同一个意图:标题不同、措辞不同,但底层 query 一样。Google 挑一篇当 canonical,把另一篇降权。在快速扩量的站点上——尤其是用 AI 辅助批量出稿的站——只要没有一套能赶在 Googlebot 之前发现问题的脚本化流程,这种事每个月都会发生。
一句话结论
- 重复分三种:完全相同、近重复、重复意图。最后一种人眼看不出来,伤害也最大。
- 给每篇文章打上
primaryKeyword,两篇撞同一个关键词时直接让 prebuild 失败。 - 近重复用 301 重定向合并(按 Google 的说法,这是强 canonical 信号),不要只靠 canonical 标签。
- 用 embedding + cosine 相似度在发布前抓近重复。
text-embedding-3-small的价格是 每 100 万 token 0.02 美元(2026 年 6 月),扫 2000 篇也就几美分。 - canonical 改动通常要 2 到 6 周 Google 才会认,所以清理不是立竿见影的。
三种重复,三种修法
| 类型 | 是什么 | 常见程度 | 修法 |
|---|---|---|---|
| 完全相同 | 同一篇发在两个 URL 上 | 少见 | 把弱的那个 301 掉 |
| 近重复 | 同一篇被改写过 | AI 出稿下很常见 | 把独特内容搬过去,再 301 |
| 重复意图 | 不同文章打同一个 query | 最坏的——人眼看不出 | 重新定角度(拆分)或把弱的那篇 noindex |
过了大约 200 篇之后,这些都没法手工做。每种类型都有自己的检测信号和修法,所以必须自动化。
怎么判断自己有重复问题
- 两篇文章在 Search Console 上对同一 query 反复抢位置。
- Pages 报告里 Indexed 数明显低于 Submitted——干净站点上差距超过 5% 就值得查一下。
- “Why pages aren’t indexed” 里出现 “Duplicate without user-selected canonical” 或 “Duplicate, Google chose different canonical”。前者表示 Google 发现了重复而你没指定偏好;后者表示 Google 推翻了你设的 canonical。
- 两篇文章的 H1 去掉修饰语后讲的是同一件事。
- sitemap 里的条数超过你独立主关键词的数量。
开始前准备
- 这是一次 content-ops 清理,不是上线。预留 1 到 2 小时专注时间。
- 先备份内容库。跑任何合并或重定向脚本之前,
git status必须干净。 - 确认托管层支持 301 重定向:Astro 静态用
_redirects、Firebase Hosting 在firebase.json里写redirects、或 Vercel 的vercel.json。
实操步骤
1. 给每篇文章加 primaryKeyword 字段
这是一个字符串,说明这篇文章到底服务什么。后面所有重复检查都以它为锚点。
---
title: "How to Submit a Sitemap to Search Console"
urlSlug: "submit-sitemap-search-console"
primaryKeyword: "submit sitemap search console"
category: "indie-dev"
---
2. 跑一份”重复关键词报告”
约 30 行的 Node 脚本扫一遍内容库,打出任何被两篇文章共享的 keyword:
// scripts/find-duplicate-keywords.mjs
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import matter from 'gray-matter';
const ROOT = 'src/content/articles/zh';
const byKw = new Map();
for (const cat of readdirSync(ROOT)) {
for (const file of readdirSync(join(ROOT, cat))) {
if (!file.endsWith('.mdx')) continue;
const { data } = matter(readFileSync(join(ROOT, cat, file), 'utf8'));
const kw = (data.primaryKeyword || '').toLowerCase().trim();
if (!kw) continue;
if (!byKw.has(kw)) byKw.set(kw, []);
byKw.get(kw).push(`${cat}/${file}`);
}
}
for (const [kw, files] of byKw) {
if (files.length > 1) console.log(`DUP "${kw}":\n ${files.join('\n ')}`);
}
挂到 npm run audit:content 里,让重复关键词直接卡 prebuild,而不是溜进生产环境。
3. 近重复用 301 合并
选权重高的那篇(Search Console 里看 impressions),把另一篇的独特段落搬过去,再加重定向。按 Google 的 canonicalization 文档,重定向是强 canonical 信号——比 rel="canonical" 标签强,更比 sitemap 收录强得多。Firebase Hosting 写法:
{
"hosting": {
"redirects": [
{ "source": "/articles/scale-ai-content-safely",
"destination": "/articles/scale-content-with-ai-safely",
"type": 301 }
]
}
}
Astro 静态 + Netlify 风格 _redirects 文件:
/articles/scale-ai-content-safely /articles/scale-content-with-ai-safely 301
4. 重复意图:重新定角度或 noindex
要么拆角度——一篇写新手、一篇写进阶,改 H1 和 primaryKeyword,让两篇真正打不同的 query;要么把弱的那篇标 draft: true,meta 加 noindex。文章布局里写:
{frontmatter.noindex && <meta name="robots" content="noindex,follow" />}
用 noindex,follow(不是 noindex,nofollow),让这个页面留在索引之外的同时仍然传递链接信号。
5. 全站默认设 self-canonical
所有页面先自指 canonical,只在你确定目标更强时才跨页指向。Astro 布局里:
<link rel="canonical" href={`${Astro.site}${Astro.url.pathname}`} />
千万别用 robots.txt 或 URL 移除工具来”修”重复——Google 文档明确反对这两种做法。被 robots 屏蔽的 URL 仍可能在没有内容的情况下被收录,而移除工具会把一个 URL 的所有版本都从 Search 里藏掉。
6. AI 批量出稿前过一遍相似度检查
对 title + 首段跑一遍 cosine 相似度(用 OpenAI embedding),能在发布前抓出大部分近重复。截至 2026 年 6 月,text-embedding-3-small 是每 100 万输入 token 0.02 美元(用 Batch API 是 0.01),1536 维,8192 token 上下文窗口。扫一个 2000 篇的内容库也就几美分,没理由手工做。
// scripts/similarity-check.mjs(节选)
import OpenAI from 'openai';
const client = new OpenAI();
async function embed(text) {
const r = await client.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
return r.data[0].embedding;
}
function cosine(a, b) {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
// cosine > 0.85 的对,标记为人工复核。
如果想让相近主题之间的区分更锐利,更大的模型成本更高,但对这个场景几乎不会改变判断:
| 模型(2026 年 6 月) | 价格 / 100 万 token | 维度 | 备注 |
|---|---|---|---|
text-embedding-3-small | $0.02 | 1,536 | 默认;最便宜,去重够用 |
text-embedding-3-large | $0.13 | 3,072 | 更细腻,但贵 6.5 倍 |
gemini-embedding-001 | $0.15 | 最高 3,072 | 支持 Matryoshka 截断;官方推荐 768 维为甜点 |
做去重,3-small 基本永远是对的选择:阈值比模型重要得多。
7. 清理上线后,让 Google 重新校验
用 URL Inspection 检查被合并的 URL,确认它返回 301,再对存活的那个 URL 点 “Request indexing”。批量重定向后重新提交 sitemap。预期大约 2 到 6 周 Google 才会认这个 canonical 改动,具体取决于你的站被抓取的频率。
执行检查清单
- 每篇文章都有
primaryKeyword;audit 脚本在 prebuild 阶段标记重复。 - 301 重定向写在
firebase.json/vercel.json/_redirects,不是只在文章正文里说说。 - 默认全站 self-canonical;跨页 canonical 是 per-article 显式开启。
- 相似度检查接进 AI 内容流水线里,不是手动跑。
上线后验证
- 用 Search Console URL Inspection 重抓被合并的 URL,确认返回码是 301、目标是主 canonical URL。
- 2 到 4 周后再看 Pages 报告:“Duplicate” 类原因数应该下降。
- 确认 sitemap 里不再列被合并掉的 URL(
grep一下 build 输出)。
容易踩的坑
- 相信”标题不同 = 文章不同”。 意图比措辞重要得多。征兆是两篇文章在 Search Console 里对同一 query 互抢曝光。
- 用 canonical 标签”掩盖”重复但不真正解决。 Google 不认同你的 canonical 时会推翻它——这正是 Pages 报告里的 “Google chose different canonical” 状态。301 更强,因为它直接把重复页消掉了。
- 生成”10 best X for [职业]“这种各版本 90% 相似的页面。 这种模式会被读成规模化的低价值内容,可能把整个 cluster 一起拖下去。
- 把去重当一次性清理。 每批稿子都会新长出重复——把检查接进 prebuild。
- 301 到一个本身也 301 的 URL。 链式重定向会损耗信号;永远只跳一次,直接跳到最终 canonical。
- 合并后忘了从 sitemap 移除老 URL。 Google 会一直抓、一直再次标记它。
FAQ
- 同一篇文章的中英文版本算重复吗?: 不算,前提是用
hreflang配对。EN 和 ZH 是两个 URL,服务两类受众。两边都要用<link rel="alternate" hreflang>标签互指。 - Google 会因为重复扣分吗?: 大多数情况下不会有 manual action,但弱的那篇会被压制,重复比例一高,站级质量信号也会受影响。
- 直接 noindex 重复页不行吗?: 可以,但浪费了写作精力。301 合并能保留链接权重;只在没东西可合的时候才用
noindex,follow。 - 怎么大规模检测近重复?: embedding + 对
title + 首段跑 cosine 相似度,效果很好。text-embedding-3-small每 100 万 token 0.02 美元,全库扫一遍便宜到可以忽略。 - 相似度脚本阈值设多少?: 人工复核线设 0.85,CI 自动阻断线设 0.92。如果总在相近但确实不同的题上误报,就把复核线往下调。
- Google 多久才会认 canonical 改动?: 通常 2 到 6 周,取决于抓取频率和站点规模。重新提交 sitemap 加请求索引能加速,但不保证当周就重抓。
相关阅读
- 如何安排 pillar page 和 cluster page
- 如何做站内内容审计
- 用 AI 批量扩内容要注意什么
- Canonical URL 是什么
- Search Console canonical 解释
- 内容站季度复盘节奏:真能发现问题
- 内容站团队配置:从 1 人到 5 人
外部参考:Google Search Central — Consolidate duplicate URLs 和 OpenAI embeddings 定价。