站内内容审计怎么做:脚本化的季度审计流程

脚本化的季度内容审计:URL 清单、Search Console join、死页与接近排名页标记、重复检测、linkinator 坏链检查,以及可 diff 的决策记录。

网上大多数内容审计教程描述的流程重到你只会做一次,然后再也不碰。独立站要的恰恰相反:一个轻到每季度都能跑的审计,用脚本把 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 支持像 4034xx 这样的状态码模式来忽略)。内部的非 200 是真 bug——必须修。

7. 标记薄页

字数低于 400 且无特殊原因(术语表条目或跳转落地页可能本就该短):

awk -F, 'NR>1 && $8<400 {print $4, $8}' audit-joined.csv

8. 把决策写回 CSV

加一列 decision,取值之一:keeprefreshmerge:<目标-slug>noindexdelete。把文件 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 的历史永不过期。
  • 翻译内容怎么审? 每种语言分开审。同一篇文章的中英版本服务不同市场,不算重复;只在同语言内对比。

相关阅读

标签: #独立开发 #内容运营 #SEO #建站策划 #Technical SEO #工作流