三层自动化:提交时(GitHub Actions 跑注册表 lint 和 Meta 校验,error 阻断合并)、每周(定时跑覆盖度和内链审计,输出到 Issue)、每季度(生成健康度报告草稿)。需要判断的环节(语义冲突、业务相关性、选题决策)保留人工,脚本只做确定性检查。
SEO/GEO 运营自动化:审计、注册表更新与报告全流程
自动化的边界在哪
十二个 Skill(第 45–56 篇)都是可用的,但如果每次都要手动跑脚本、手动组织提示词、手动整理报告,运营三个月后大概率会放弃。
但也不能全自动。这套方法论里有一批环节必须由人判断:
| 环节 | 能否自动化 | 原因 |
|---|---|---|
| 注册表字面级冲突检查 | ✅ 完全自动 | 确定性规则 |
| Meta 长度和唯一性校验 | ✅ 完全自动 | 确定性规则 |
| 内链闭环检查 | ✅ 完全自动 | 图结构计算 |
| GEO 结构项检查 | ✅ 完全自动 | HTML 结构解析 |
| 语义同义词判断 | ⚠️ 半自动 | 模型给建议,人确认 |
| Cannibalization 处置决策 | ❌ 人工 | 涉及业务价值权衡 |
| 竞品词业务相关性过滤 | ❌ 人工 | 只有你知道业务边界 |
| 选题优先级决策 | ❌ 人工 | 涉及资源分配 |
自动化的目标不是取代判断,是保证该跑的检查一定会跑。人的时间应该花在判断上,不是花在记得跑脚本上。
一、三层自动化架构
第 1 层:提交时(阻断式)
触发:PR 或 push
内容:注册表 lint、Meta 校验、Schema 引用完整性
行为:error 阻断合并,warn 只提示
耗时:< 1 分钟
第 2 层:每周(通知式)
触发:定时(每周一)
内容:覆盖度审计、内链流审计、GEO 结构审计
行为:结果写入 GitHub Issue
耗时:2–3 分钟
第 3 层:每季度(报告式)
触发:定时(季度首日)+ 手动触发
内容:完整健康度报告草稿 + 新词发现
行为:生成 Issue,等人工补判断
耗时:5–10 分钟(含 API 调用)
GEO 意图注册表接入后,三层各增加一项:
| 层 | 新增 | 为什么放这一层 |
|---|---|---|
| 第 1 层 | validate-intents.mjs(表本身的合法性:枚举、主答块唯一、答案长度上限、合规型的 confidence 门槛) |
纯静态校验,秒级,适合阻断 |
| 第 2 层 | geo-registry-audit.mjs(注册表 × 构建产物:登记未落地、复述块与主答块分叉、fact_refs 悬空) |
需要构建产物,且分叉是逐渐发生的,每周扫合适 |
| 第 3 层 | 引用实测(20–40 条 / 季度,人做) | 无法自动化,放进“待人工完成”清单 |
第 2 层那一项是三层里唯一能在事故发生前抓到“同一条答案在多页分叉”的检查——这类问题不产生任何可见指标,靠人看是看不出来的。脚本见GEO 审计升级。
二、第 1 层:提交时阻断
这一层最重要——它保证错误不会进入主分支。
# .github/workflows/seo-lint.yml
name: SEO Lint
on:
pull_request:
paths:
- 'registry.json'
- 'src/data/entities.yaml'
- 'src/content/**'
push:
branches: [main]
jobs:
registry-lint:
name: 注册表校验
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: 注册表字面级冲突检查
run: |
node scripts/registry-lint.mjs > /tmp/registry-lint.json
ERRORS=$(jq '.summary.errorCount' /tmp/registry-lint.json)
WARNS=$(jq '.summary.warnCount' /tmp/registry-lint.json)
echo "### 注册表校验结果" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- error: $ERRORS" >> $GITHUB_STEP_SUMMARY
echo "- warn: $WARNS" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "#### 需修复的 error" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
jq '.errors' /tmp/registry-lint.json >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Schema 引用完整性检查
run: |
node scripts/schema-generate.mjs
node scripts/schema-validate.mjs > /tmp/schema-validate.json || true
VERDICT=$(jq -r '.summary.verdict' /tmp/schema-validate.json)
if [ "$VERDICT" != "PASS" ]; then
echo "Schema 校验失败" >> $GITHUB_STEP_SUMMARY
jq '.errors' /tmp/schema-validate.json >> $GITHUB_STEP_SUMMARY
exit 1
fi
build-and-audit:
name: 构建后结构检查
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: GEO 致命项检查
run: |
node scripts/geo-audit.mjs > /tmp/geo.json || true
CRITICAL=$(jq '.criticalFailure' /tmp/geo.json)
if [ "$CRITICAL" = "true" ]; then
echo "### GEO 致命项失败" >> $GITHUB_STEP_SUMMARY
jq '{robots, ssr}' /tmp/geo.json >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: 内链闭环检查(warn 不阻断)
run: |
node scripts/link-flow-audit.mjs > /tmp/link.json
TOTAL=$(jq '.summary.clusterCount' /tmp/link.json)
WITH_LINK=$(jq '.summary.clustersWithLink' /tmp/link.json)
echo "### 内链闭环率" >> $GITHUB_STEP_SUMMARY
echo "$WITH_LINK / $TOTAL" >> $GITHUB_STEP_SUMMARY
if [ "$WITH_LINK" -lt "$TOTAL" ]; then
echo "以下 Cluster 缺少 Pillar 内链:" >> $GITHUB_STEP_SUMMARY
jq '[.clusterResults[] | select(.hasLink == false) | .cluster]' \
/tmp/link.json >> $GITHUB_STEP_SUMMARY
fi
为什么 GEO 致命项要阻断:robots.txt 屏蔽 AI 爬虫或页面变成客户端渲染,是会让第 40–44 篇全部努力归零的错误。这类问题必须在合并前拦住。
为什么内链只提示不阻断:新建页面时内链可能还没补全,阻断会影响正常开发流程。放在 Step Summary 里提示,开发者自己看到就会补。
三、第 2 层:每周定时审计
# .github/workflows/weekly-audit.yml
name: Weekly SEO Audit
on:
schedule:
- cron: '0 1 * * 1' # 每周一 01:00 UTC(北京时间 09:00)
workflow_dispatch: # 支持手动触发
jobs:
audit:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: 覆盖度审计
run: node scripts/coverage-audit.mjs > /tmp/coverage.json
- name: 内链流审计
run: node scripts/link-flow-audit.mjs > /tmp/link.json
- name: GEO 结构审计
run: node scripts/geo-audit.mjs > /tmp/geo.json || true
- name: 组装报告
run: |
cat > /tmp/report.md <<'HEADER'
## 本周 SEO 审计
HEADER
echo "### 覆盖度" >> /tmp/report.md
jq -r '"- 覆盖率:\(.summary.coverageRate)\n- 待建页面:\(.summary.missing)\n- 未纳管页面:\(.summary.unmanaged)"' \
/tmp/coverage.json >> /tmp/report.md
UNMANAGED=$(jq '.summary.unmanaged' /tmp/coverage.json)
if [ "$UNMANAGED" -gt 0 ]; then
echo "" >> /tmp/report.md
echo "**未纳管页面(需优先处理)**:" >> /tmp/report.md
jq -r '.unmanagedPages[] | "- \(.)"' /tmp/coverage.json >> /tmp/report.md
fi
echo "" >> /tmp/report.md
echo "### 内链闭环" >> /tmp/report.md
jq -r '"- \(.summary.clustersWithLink) / \(.summary.clusterCount) 个 Cluster 有 Pillar 内链"' \
/tmp/link.json >> /tmp/report.md
echo "" >> /tmp/report.md
echo "### GEO 结构" >> /tmp/report.md
jq -r '"- FAQ 标题问题:\(.summary.faqHeadingIssues)\n- 表格缺 caption:\(.summary.tableCaptionIssues)\n- Schema 问题:\(.summary.schemaIssues)"' \
/tmp/geo.json >> /tmp/report.md
echo "" >> /tmp/report.md
echo "---" >> /tmp/report.md
echo "需要人工判断的部分见各脚本完整输出(Artifacts)。" >> /tmp/report.md
- name: 上传完整结果
uses: actions/upload-artifact@v4
with:
name: audit-results
path: /tmp/*.json
retention-days: 30
- name: 创建或更新 Issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('/tmp/report.md', 'utf8');
const title = `SEO 周审计 ${new Date().toISOString().split('T')[0]}`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ['seo-audit'],
});
为什么用 Issue 而不是邮件:Issue 可以直接在上面讨论、勾选任务、关联 PR。审计结果和处理动作在同一个地方,不会丢。
四、第 3 层:季度报告(半自动)
季度报告需要模型参与语义判断,不能纯脚本完成。做法是:脚本生成数据 + 报告骨架,Issue 里留出人工填写的位置。
# .github/workflows/quarterly-report.yml
name: Quarterly SEO Health Report
on:
schedule:
- cron: '0 1 1 1,4,7,10 *' # 每季度首日
workflow_dispatch:
jobs:
report:
runs-on: ubuntu-latest
permissions:
issues: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: 生成健康度指标
run: node scripts/health-report.mjs > /tmp/health.json
- name: 提交指标历史
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add reports/health-history.json
git diff --staged --quiet || \
git commit -m "chore: 更新季度 SEO 指标历史"
git push
- name: 创建报告 Issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('/tmp/health.json', 'utf8'));
const m = data.metrics;
const p = data.previous;
const delta = (cur, prev) => {
if (!prev) return '首次';
const d = (Number(cur) - Number(prev)).toFixed(1);
return d > 0 ? `↑${d}` : d < 0 ? `↓${Math.abs(d)}` : '—';
};
const body = `## 季度 SEO 健康度报告(自动生成部分)
生成日期:${m.date}
### 指标总览
| 指标 | 当前 | 上期 | 变化 | 目标 |
|------|------|------|------|------|
| 注册表完整性 | ${m.registryIntegrity}% | ${p?.registryIntegrity ?? '—'}% | ${delta(m.registryIntegrity, p?.registryIntegrity)} | 100% |
| 页面覆盖率 | ${m.pageCoverage}% | ${p?.pageCoverage ?? '—'}% | ${delta(m.pageCoverage, p?.pageCoverage)} | ≥85% |
| 内链闭环率 | ${m.linkFlowIntegrity}% | ${p?.linkFlowIntegrity ?? '—'}% | ${delta(m.linkFlowIntegrity, p?.linkFlowIntegrity)} | 100% |
| GEO 就绪度 | ${m.geoReadiness} | ${p?.geoReadiness ?? '—'} | ${delta(m.geoReadiness, p?.geoReadiness)} | ≥85 |
${data.criticalFailure ? '### ⚠️ GEO 致命项未通过\n\n以下指标仅供参考,须先解决致命问题。\n' : ''}
---
## 待人工完成
按第 50 篇的 Skill 说明书,以下部分需要人工判断后补充:
- [ ] **Cannibalization 诊断**(第 47 篇)
需导出 GSC Query×Page 数据后运行,本流程无法自动获取
- [ ] **语义级冲突检查**(第 45 篇 Step 2)
在完整输出的 \`allPkw\` 列表上做同义词分组
- [ ] **Top 5 行动项排序**(第 50 篇 Step 3)
从各专项 error/warn 中筛选,注明工时和预期影响
- [ ] **判定为不需处理的项**(第 50 篇 Step 4)
记录假阳性及原因,避免下期重复分析
- [ ] **新词发现与选题日历**(第 62 篇)
需手工收集 Perplexity 相关问题
完整数据见本次运行的 Artifacts。
<details>
<summary>原始指标数据</summary>
\`\`\`json
${JSON.stringify(data.metrics, null, 2)}
\`\`\`
</details>
`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `季度 SEO 健康度报告 ${m.date}`,
body,
labels: ['seo-audit', 'quarterly'],
});
- uses: actions/upload-artifact@v4
with:
name: quarterly-data
path: /tmp/health.json
retention-days: 365
关键设计:把待人工完成的部分做成 checklist。自动化不假装能做判断,而是明确列出“这几件事需要你做”,并指向对应的 Skill 说明书。
五、GSC 数据的获取问题
第 47 篇的 Cannibalization 诊断和第 62 篇的新词发现都需要 GSC 数据,但 GSC 没有简单的 CSV 自动导出。
方案 A:手动导出(推荐起步)
每季度手动导出一次,5 分钟的事。放进 data/ 目录后本地跑脚本。
不要为了自动化而自动化——季度频率的任务,手动 5 分钟比配置 API 认证更划算。
方案 B:Search Console API(数据量大时)
如果站点页面多、需要更频繁的数据,用 API:
// scripts/fetch-gsc.mjs
import { google } from 'googleapis';
import fs from 'fs/promises';
const auth = new google.auth.GoogleAuth({
// 服务账号 JSON 放在 GitHub Secrets,运行时写入临时文件
keyFile: process.env.GSC_SERVICE_ACCOUNT_PATH,
scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
});
const searchconsole = google.searchconsole({ version: 'v1', auth });
const SITE_URL = 'sc-domain:aquaflowpumps.com';
// 计算日期范围(最近 90 天)
const end = new Date();
const start = new Date(end.getTime() - 90 * 24 * 60 * 60 * 1000);
const fmt = d => d.toISOString().split('T')[0];
const res = await searchconsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate: fmt(start),
endDate: fmt(end),
dimensions: ['query', 'page'],
rowLimit: 25000,
},
});
// 转成第 47 / 62 篇脚本期望的 CSV 格式
const header = 'Query,Page,Clicks,Impressions,CTR,Position';
const lines = (res.data.rows ?? []).map(r => {
const [query, page] = r.keys;
// CSV 转义:字段含逗号或引号时用双引号包裹
const esc = s => `"${String(s).replace(/"/g, '""')}"`;
return [
esc(query), esc(page),
r.clicks, r.impressions,
(r.ctr * 100).toFixed(2) + '%',
r.position.toFixed(1),
].join(',');
});
await fs.mkdir('data', { recursive: true });
await fs.writeFile('data/gsc-query-page.csv', [header, ...lines].join('\n'));
console.log(`Exported ${lines.length} rows`);
配置步骤:
- Google Cloud Console 创建服务账号,下载 JSON 密钥
- 在 GSC 里把服务账号邮箱添加为用户(权限:Restricted)
- JSON 密钥内容存入 GitHub Secrets(
GSC_SERVICE_ACCOUNT) - Workflow 里写入临时文件后运行脚本
六、本地开发时的快捷命令
CI 之外,本地也要能方便地跑。在 package.json 加脚本:
{
"scripts": {
"seo:lint": "node scripts/registry-lint.mjs",
"seo:coverage": "node scripts/coverage-audit.mjs",
"seo:links": "npm run build && node scripts/link-flow-audit.mjs",
"seo:geo": "npm run build && node scripts/geo-audit.mjs",
"seo:schema": "node scripts/schema-generate.mjs && node scripts/schema-validate.mjs",
"seo:health": "npm run build && node scripts/health-report.mjs",
"seo:discover": "node scripts/discover-new-keywords.mjs",
"seo:all": "npm run seo:lint && npm run seo:schema && npm run seo:links && npm run seo:geo"
}
}
新建页面前的标准动作:
# 1. 在 registry.json 加行后,立刻校验
npm run seo:lint
# 2. 写完页面,检查结构和内链
npm run seo:all
这两个命令要形成肌肉记忆——它们拦住的是后期难以发现的问题。
七、自动化的维护成本
自动化本身也需要维护,要诚实评估:
| 项目 | 初始配置 | 维护成本 |
|---|---|---|
| 第 1 层(提交时) | 1–2 小时 | 极低,脚本稳定后基本不动 |
| 第 2 层(每周) | 1 小时 | 低,偶尔调整报告格式 |
| 第 3 层(季度) | 1–2 小时 | 低 |
| GSC API(可选) | 1–2 小时 | 中,服务账号密钥需定期轮换 |
建议的落地顺序:
先做第 1 层(收益最直接:拦住错误)
↓
运行一个月,确认脚本稳定
↓
加第 2 层(每周提醒,避免遗忘)
↓
加第 3 层(季度报告骨架)
↓
GSC API 只在手动导出确实成为瓶颈时才做
不要一次配齐三层——脚本在实际运行中会暴露问题(阈值不合适、误报),逐层验证更稳。
→ 全系列 AI Skills 工具箱汇总:Prompt 合集与使用指南
RELATED / 相关推荐
接着读这些
按同一分类、系列与标签为你挑选。
GEO 审计升级:从「结构合规」到「有没有被引用」
之前那个 GEO 覆盖审计 Skill 只能查结构,查不了覆盖——因为它没有期望值可比。补上意图注册表之后,审计从一层变三层:结构、覆盖与一致性、引用实测。其中复述块与主答块的一致性检查是纯确定性的,脚本能百分之百判定,这是升级带来的最大收益。
AI Skill:Author 与 Organization Schema 批量生成
Schema 是结构化数据,最适合脚本生成而不是手写——手写容易漏字段、写错嵌套、@id 引用不一致。这个 Skill 用一份实体配置文件驱动全站 Schema 生成,脚本负责结构和引用完整性,模型只负责 description 和 knowsAbout 这类需要表达的字段。
AI Skill:Cannibalization 诊断——同站竞争页面识别
注册表审计查的是规划层面的冲突,Cannibalization 诊断查的是已发布内容的实际竞争。这个 Skill 用 GSC 数据找出同一查询下多个页面轮流排名的证据,再用页面正文比对确认原因,最后给出合并、差异化或降权的处置决策。