站内内容审计怎么做:抓重复、死页、坏链和结构缺失

每季度跑一次的轻量内容审计:URL 清单、Search Console join、死页扫描、重复检测、坏链检查、决策记录。

网上大多数”内容审计”文章描述的流程重到你只会做一次。独立站需要的是一个轻到每个季度都能跑的审计——脚本化把活基本自动了。目标不是完美,是在问题复利之前抓到它。

问题背景

内容审计本质是一次 join:你的 URL 清单 + Search Console 数据 + 几条启发式规则。把 join 和规则写成脚本,之后每次审几小时,不是几天。本文就给你这套脚本。

判断标准

  • 上次审计已经超过 6 个月(或者从来没有)。
  • Search Console “Submitted vs Indexed” 比低于 90%。
  • 100+ 篇,已经无法不查就知道站上有什么。
  • 内链检查很久没跑过。
  • 怀疑有重复但说不出具体是哪几篇。

快速结论

每季度跑一次轻审,不要一年才跑一次重审。频繁的小审在问题还便宜修时就发现它。

开始前准备

  • Search Console API 访问(OAuth)——没有数据等于猜。
  • 内容用 collection / 文件式内容层。
  • 选一种 CSV 格式长期复用——每次审完都成新的基线。

实操步骤

  1. 生成 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'));
  1. 拉 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])
"
  1. 标记死页。 上线 > 90 天且零 impressions:
awk -F, 'NR>1 && $9==0 && $10==0 {print $4, $5}' audit-joined.csv \
  | awk -F'"' '{print $2}'
# 待决定的 slug(合并 / 刷新 / noindex / 删除)
  1. 标记接近排名。 位置 8-20、月展示 > 100:
awk -F, 'NR>1 && $11>=8 && $11<=20 && $10>100' audit-joined.csv \
  | sort -t, -k10 -rn | head -30
# 刷新清单,按 impressions 排序
  1. 标记重复。 按 primaryKeyword 分组:
awk -F, 'NR>1 {print $7}' audit-joined.csv | sort | uniq -c \
  | awk '$1 > 1' | sort -rn
# count > 1 都是重复意图组
  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
  1. 标记薄页。 字数 < 400 且无特殊原因:
awk -F, 'NR>1 && $8<400 {print $4, $8}' audit-joined.csv
  1. 决策写回 CSV。 加一列 decisionkeep / 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%+ 说明早期内容策略本身有问题。
  • 翻译内容怎么审?: 每种语言分开审。不同市场 ≠ 重复。

相关阅读

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