跳到主要内容
IM智引科技

研究 / 外贸与 B2B 增长

SEO/GEO 运营自动化:审计、注册表更新与报告全流程

十二个 Skill 各自可用,但手动逐个执行很快会被放弃。这篇讲如何用 GitHub Actions 把审计流程自动化——每次提交跑注册表校验,每周跑覆盖度检查,每季度自动生成健康度报告,并把需要人工判断的部分明确留给人。

BLKTECH 编辑部2026年8月4日13 分钟难度 实战免费GitHub ActionsNode.jsn8n

三层自动化:提交时(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`);

配置步骤

  1. Google Cloud Console 创建服务账号,下载 JSON 密钥
  2. 在 GSC 里把服务账号邮箱添加为用户(权限:Restricted)
  3. JSON 密钥内容存入 GitHub Secrets(GSC_SERVICE_ACCOUNT
  4. 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 合集与使用指南

NEXT ACTION / 下一步

继续系列:AI 外贸站建设全系列

把读到的方法变成一个小行动,完成后再回来迭代。

继续

RELATED / 相关推荐

接着读这些

按同一分类、系列与标签为你挑选。