网上大多数内容审计教程描述的流程重到你只会做一次,然后再也不碰。独立站要的恰恰相反:一个轻到每季度都能跑的审计,用脚本把 90% 的活自动化掉。目标不是把站做到完美,而是在退化还便宜修的时候抓到它,并且把每个决定写下来留档。
一句话结论
把内容审计当成一次大 join——文件系统里生成的 URL 清单 + 28 天的 Google Search Console 数据 + 五条启发式规则——每一步都写成 scripts/ 里的脚本。200-500 篇文章第一次跑要 4-8 小时;之后每次只要 1-3 小时,因为工具已经搭好了。把每个 keep/refresh/merge/delete 决定写进一个 commit 进仓库的 CSV,下个季度就从 diff 开始,而不是从零开始。
什么时候真的需要审
下面有两条以上成立就该审:
- 上次审计超过 6 个月了,或者从来没审过。
- Search Console 的 Pages 报告(旧称”索引覆盖”)里,已索引 / 已提交的比例低于 90%。
- 文章 100+ 篇,已经无法不查就说清站上有什么。
- 内链检查很久没跑过了。
- 怀疑有重复意图页,但说不出具体是哪几篇。
如果只成立一条,跳过完整审计,单跑坏链检查(第 6 步)就行——那是性价比最高的 10 分钟。
开始前需要准备什么
- Search Console API 访问(OAuth)。 没有展示数据,审计基本靠猜。新建的属性要积累约 28 天历史,数字才有意义。
- 文件式内容层(Astro/Hugo 的 content collection、一个 Markdown 文件夹等),这样清单是生成的,不靠手维护。
- 一种 commit 进仓库并长期复用的 CSV 格式。 每次审完都成为下一次 diff 的基线。
关于 Search Console 的限制(截至 2026 年 6 月):性能数据只回溯 16 个月(Google 每天删掉最旧的一天),API 每次请求最多返回 25,000 行、每种搜索类型每天最多 50,000 行。对于 URL 数在 2 万以内的站,一次 28 天、按 page 维度的拉取远在这些上限之内。
实操步骤
1. 生成 URL 清单
约 30 行 Node 脚本读取每个 MDX 文件,导出一份 CSV。这是整个审计的脊柱——后面所有步骤都 join 在它上面。
// scripts/audit-step1-inventory.mjs
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import matter from 'gray-matter';
const rows = [];
for (const lang of ['en', 'zh']) {
for (const cat of readdirSync(`src/content/articles/${lang}`)) {
for (const f of readdirSync(`src/content/articles/${lang}/${cat}`)) {
if (!f.endsWith('.mdx')) continue;
const { data, content } = matter(
readFileSync(`src/content/articles/${lang}/${cat}/${f}`, 'utf8'));
rows.push({
url: `https://yourdomain.com/${lang}/articles/${data.urlSlug}/`,
lang, category: cat,
slug: data.urlSlug,
title: data.title,
primaryKeyword: data.primaryKeyword || '',
publishedAt: data.publishedAt,
words: content.split(/\s+/).length,
});
}
}
}
writeFileSync('audit-inventory.csv',
'url,lang,category,slug,title,primaryKeyword,publishedAt,words\n' +
rows.map(r => Object.values(r).map(v => `"${v}"`).join(',')).join('\n'));
2. 拉 28 天 Search Console 数据并 join
每季度都用固定的 28 天窗口,季度间的数字才能对比。page 维度给你每个 URL 的点击、展示和平均位置。
curl -X POST "https://www.googleapis.com/webmasters/v3/sites/$SITE/searchAnalytics/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data '{
"startDate":"2026-04-22","endDate":"2026-05-22",
"dimensions":["page"],"rowLimit":25000
}' \
| jq -r '.rows[] | [.keys[0],.clicks,.impressions,.position] | @csv' \
> gsc-28d.csv
如果单个属性有排名的 URL 超过 25,000 个,第二次请求加 "startRow":25000 分页,再把结果拼起来。然后用五行 Python 把 GSC join 到清单上:
python3 -c "
import csv
gsc = {r[0]:r for r in csv.reader(open('gsc-28d.csv'))}
out = csv.writer(open('audit-joined.csv','w'))
for row in csv.reader(open('audit-inventory.csv')):
url = row[0]; m = gsc.get(url, ['','0','0','0'])
out.writerow(row + m[1:4])
"
现在 audit-joined.csv 有清单的第 1-8 列,加上点击(第 9 列)、展示(第 10 列)、位置(第 11 列)。下面每个标记都是对这个文件的一行 awk。
3. 标记死页
上线超过 90 天、点击和展示都为零——Google 实际上已经无视它们了:
awk -F, 'NR>1 && $9==0 && $10==0 {print $4, $5}' audit-joined.csv \
| awk -F'"' '{print $2}'
# 待决定的 slug:合并 / 刷新 / noindex / 删除
4. 标记接近排名的页(ROI 最高的一张清单)
平均位置 8-20、展示数超过 100。这些页已经在排了——刷新一下往往就能推进首页,比从头写一篇便宜得多:
awk -F, 'NR>1 && $11>=8 && $11<=20 && $10>100' audit-joined.csv \
| sort -t, -k10 -rn | head -30
# 刷新清单,按展示数排序(最大收益排在前面)
5. 按意图标记重复
按 primary keyword 分组;任何大小 ≥ 2 的组都在跟自己抢同一个查询:
awk -F, 'NR>1 {print $7}' audit-joined.csv | sort | uniq -c \
| awk '$1 > 1' | sort -rn
# 每个 count > 1 都是要合并或做差异化的重复意图簇
6. 对构建后的站点跑坏链检查
linkinator(截至 2026 年 6 月为 v7.6.1)会爬取渲染后的站点、报告非 200 的链接。默认并发是 100;调低一点别把自己的主机压垮,加 --retry 让偶发的 429 不会变成误报:
npx linkinator https://yourdomain.com \
--recurse --retry --concurrency 10 --skip 'http(s)?://[^/]+/$' \
--format CSV > linkinator-report.csv
awk -F, '$2 != "200"' linkinator-report.csv | head
外部站点屏蔽爬虫返回的 403 当成警告而非失败处理(linkinator 支持像 403 或 4xx 这样的状态码模式来忽略)。内部的非 200 是真 bug——必须修。
7. 标记薄页
字数低于 400 且无特殊原因(术语表条目或跳转落地页可能本就该短):
awk -F, 'NR>1 && $8<400 {print $4, $8}' audit-joined.csv
8. 把决策写回 CSV
加一列 decision,取值之一:keep、refresh、merge:<目标-slug>、noindex、delete。把文件 commit 进仓库。这份 CSV 才是审计的全部意义所在——它是下季度 diff 的基线,也是某个 URL 为什么被撤掉的记录。
五个标记一览
| 标记 | 规则(列) | 常见动作 |
|---|---|---|
| 死页 | 点击=0 且展示=0,上线 > 90 天 | 合并、刷新、noindex 或删除 |
| 接近排名 | 位置 8-20,展示 > 100 | 刷新——ROI 最高 |
| 重复意图 | 同一 primaryKeyword 出现 ≥ 2 次 | 合并或做差异化 |
| 坏链 | linkinator 状态 ≠ 200(内部) | 修目标或修链接 |
| 薄页 | 字数 < 400 且无理由 | 扩写或合并 |
每次审计能预期什么
- 第一次: 200-500 篇要 4-8 小时——大头是把脚本写出来这一次性成本。
- 之后每次: 1-3 小时;只重跑脚本、看新出来的标记。
- 健康的退役比例是每次审退 5-10%。如果一次要退 30%+,说明问题在早期内容策略本身,不在这次审计。
怎么验证审计起效了
- 4-8 周后,Pages 报告里的已索引数上升,因为死页要么修了要么撤了。
- 重跑审计,季度间死页清单越来越短。
- 最新一次构建的 linkinator 报告里,内部非 200 链接为零。
容易踩的坑
- 审完不写决定。 下个季度会从零重新发现同样的问题。
- 什么都不退役。 审计退化成一张”以后修”的清单,而不是一组决定。
- 想一次性修完。 审计是诊断;动手术分摊到接下来几周。
- “看起来还行”就不审。 Search Console 永远会暴露首页视角藏住的东西。
- 决策放 Google Sheets,而仓库里的 CSV 才可 diff、可脚本化、跟内容一起版本管理。
FAQ
- 一次要花多久? 200-500 篇第一次 4-8 小时,脚本到位后每次 1-3 小时。
- 能用 AI 自动做决策吗? 用 AI 做分流——从 CSV 里 flag 死页、薄页、重复候选,对任何模型都是干净的活。但留 / 退役的决策交给人:判断一篇文章在互联站点里的作用,正是模型最容易猜错的地方。
- 没接 Search Console 怎么办? 先接上属性、等约 28 天。没有展示数据的审计基本靠猜,因为第 3、4 步都依赖它。
- Search Console 数据能回溯多久? 截至 2026 年 6 月是 16 个月——Google 每天丢掉最旧的一天。想要更长的基线,就每季度把 API 拉取结果导出进仓库;进了 git 的历史永不过期。
- 翻译内容怎么审? 每种语言分开审。同一篇文章的中英版本服务不同市场,不算重复;只在同语言内对比。