网上大多数”内容审计”文章描述的流程重到你只会做一次。独立站需要的是一个轻到每个季度都能跑的审计——脚本化把活基本自动了。目标不是完美,是在问题复利之前抓到它。
问题背景
内容审计本质是一次 join:你的 URL 清单 + Search Console 数据 + 几条启发式规则。把 join 和规则写成脚本,之后每次审几小时,不是几天。本文就给你这套脚本。
判断标准
- 上次审计已经超过 6 个月(或者从来没有)。
- Search Console “Submitted vs Indexed” 比低于 90%。
- 100+ 篇,已经无法不查就知道站上有什么。
- 内链检查很久没跑过。
- 怀疑有重复但说不出具体是哪几篇。
快速结论
每季度跑一次轻审,不要一年才跑一次重审。频繁的小审在问题还便宜修时就发现它。
开始前准备
- Search Console API 访问(OAuth)——没有数据等于猜。
- 内容用 collection / 文件式内容层。
- 选一种 CSV 格式长期复用——每次审完都成新的基线。
实操步骤
- 生成 URL 清单。 30 行 Node 脚本:
// scripts/audit-step1-inventory.mjs
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
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'));
- 拉 28 天 Search Console 并 join。 每 URL:impressions / clicks / 平均位置:
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
5 行 Python 做 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])
"
- 标记死页。 上线 > 90 天且零 impressions:
awk -F, 'NR>1 && $9==0 && $10==0 {print $4, $5}' audit-joined.csv \
| awk -F'"' '{print $2}'
# 待决定的 slug(合并 / 刷新 / noindex / 删除)
- 标记接近排名。 位置 8-20、月展示 > 100:
awk -F, 'NR>1 && $11>=8 && $11<=20 && $10>100' audit-joined.csv \
| sort -t, -k10 -rn | head -30
# 刷新清单,按 impressions 排序
- 标记重复。 按 primaryKeyword 分组:
awk -F, 'NR>1 {print $7}' audit-joined.csv | sort | uniq -c \
| awk '$1 > 1' | sort -rn
# count > 1 都是重复意图组
- 对构建后站点跑坏链检查。 用 linkinator 或自写 walker:
npx linkinator https://yourdomain.com \
--recurse --concurrency 5 --skip 'http(s)?://[^/]+/$' \
--format CSV > linkinator-report.csv
awk -F, '$2 != "200"' linkinator-report.csv | head
- 标记薄页。 字数 < 400 且无特殊原因:
awk -F, 'NR>1 && $8<400 {print $4, $8}' audit-joined.csv
- 决策写回 CSV。 加一列
decision:keep/refresh/merge:<目标-slug>/noindex/delete。把 CSV commit 到仓库;这就是下季度的基线。
执行检查清单
- 脚本全在
scripts/,npm run audit一键跑。 - inventory CSV 由文件系统生成,不手维护。
- Search Console 拉法固定 28 天窗口。
- 决定先写进 CSV 再动内容。
- 本季度 CSV 与上季度可 diff 比较。
上线后验证
- 4-8 周后 Search Console Pages indexed 数上升(死页要么修要么撤)。
- 重跑审计死页列表变短。
- 最新一次 linkinator 报告零非 200 内链。
容易踩的坑
- 审完不写决定。下个季度会重新发现同样的问题。
- 什么都不退役。审计就变成”以后修的清单”而不是决定。
- 想一次性修完。审计是诊断不是手术,修分摊到接下来几周。
- “看起来还行”就不审。Search Console 永远有惊喜。
- 用 Google Sheets 而不是仓库 CSV——后者可 diff、可脚本化。
FAQ
- 一次要花多久?: 200-500 篇第一次大约 4-8 小时。工具铺好后下次大约一半时间。
- 能用 AI 做吗?: 分流(flag 候选)可以用 AI,留 / 退役决策仍要人工。AI 在跨文章语境判断上不可靠。
- 没接 Search Console 怎么办?: 先接,等 28 天。没有 impression 数据的审计基本是猜。
- 退役要多激进?: 健康站每次审退役 5-10% 是正常的。一次退 30%+ 说明早期内容策略本身有问题。
- 翻译内容怎么审?: 每种语言分开审。不同市场 ≠ 重复。