Back to articles

在部署管道中加入 SEO 审计:API、退出码与 CI 配方

SEOReport Team·
apici-cdautomationgithub-actionstechnical-seodevops

一个可用的 CI 审计配方:三个 REST 端点,构建门可读取的分数字段,以及真实回归与基础设施波动的区别。

造成最大可见度损失的技术回归在代码审查中是不可见的。 一个 canonical 标签指向错误主机,一个 hreflang 块失去自引用,一个 Content-Security-Policy 头从反向代理配置中被丢弃,一个路由将其内容移到水合后——这些都在看似正常的 diff 中通过所有测试。 它们在几周后才显现,在流量图中,远在导致它们的提交滚动出视图之后。

修复是结构性的:在部署时运行审计,而不是等有人记得。 每个 SEOReport 报告可通过 REST 访问,响应携带一个数值分数块,shell 脚本可以将该块转换为退出码。 这就是整个机制。

只有机器能及时捕捉的四类回归

有四种失败模式特别容易逃过人工审核,值得具体命名,因为它们正是门实际在保护的内容。

Hreflang。 一个集群依赖于自引用、返回链接、合理的 x-default、格式良好的 URL,以及可索引且规范化指向自身的替代页面。 CMS 迁移重写 URL 模式可能一次性破坏所有语言的返回链接,且单独查看任何语言都看不出问题。

Canonicalization。 canonical 必须存在,指向一个不重定向的可索引目标,并且留在站点自己的主机上。 这种失败的最糟版本是将 staging canonical 推送到生产——页面悄悄指定了你不想被索引的主机。

安全头。 Content-Security-Policy、HSTS、Referrer-Policy、X-Frame-Options、X-Content-Type-Options 与 Permissions-Policy. 它们存在于基础设施配置中,而非应用代码,这正是它们在代理或边缘配置更改时消失且无人注意的原因。

渲染一致性。 页面可能在 HTML 响应中交付一个版本,而在脚本运行后呈现另一个版本:不同的标题、缺失的标题层级、延迟注入的 canonical,或仅在水合后才出现的正文文本。 当我们公开的渲染一致性数据得到裁决时,大多数网站为任何读取HTML的请求提供了明显更薄的页面。 配置文件中的一个ssr: false足以将网站移入该组。

四者皆为确定性,四者皆易于检查,且四者皆为人们仅在已怀疑时才会检查的事物。

三个端点和一个循环

API表面需要的管道很小。 API 访问包含在付费计划中,并通过 bearer token 进行身份验证:在控制台中发放密钥,并将其作为Authorization: Bearer sr__live_your_api_key发送。 同一密钥可打开REST和MCP;完整参考见开发者页面。

graph TD A[部署完成] --> B["POST /api/v1/reports"] B --> C["GET /api/v1/reports/:id/ready"] C -->|"ready: false"| C C -->|"error.code present"| D[基础设施信号,退出 75] C -->|"ready: true"| E["GET /api/v1/reports/:id"] E --> F{预算内的评分块?} F -->|是| G[退出 0] F -->|否| H[退出 1,构建失败]

提交报告是一条 POST:

bash
JOB_ID=$(curl -sS -X POST https://seoreport.dev/api/v1/reports \
-H "Authorization: Bearer $SEOREPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "forceRerun": true}' \
| jq -r '.report.jobId')

关于该主体的两件事在 CI 中尤为重要。

forceRerun存在是因为API在已有快照时会重用它——对点击按钮的人来说合理,但对部署门来说不合适,否则会评估前一次发布。 响应会告诉你在submission.reusedSnapshot中发生了什么。 强制全新运行是付费账户功能;没有该权限的密钥会收到403,说明原因,而不是静默返回陈旧数据。

审计范围是从 URL 路径推断出来的。 一个裸起点会执行一次完整站点审计;一个带路径的 URL 只审计该页面。 因此,流水线可以在每次部署时对首页进行门控,并添加该变更触及的具体路由,而不需要任何额外参数。

然后轮询。 GET /api/v1/reports/:id/ready 是一个廉价的状态端点——它返回 ready、status、stage、pollAfterMs,并且当一次运行失败时,返回一个 error 对象:

bash
for _ in $(seq 1 60); do
READY=$(curl -sS -H "Authorization: Bearer $SEOREPORT_API_KEY" \
"https://seoreport.dev/api/v1/reports/$JOB_ID/ready")
CODE=$(echo "$READY" | jq -r '.error.code // empty')
if [ -n "$CODE" ]; then
echo "audit did not complete: $CODE"
exit 75
fi
[ "$(echo "$READY" | jq -r '.ready')" = "true" ] && break
sleep 5
done

那个 exit 75 是有意的,也是大多数集成最容易出错的部分。

区分一次回归与一次从未发生的运行

error.code 携带一个机器可读的原因和一个 retryable 布尔值。 DNS_FAILURE、CRAWLER_BLOCKED、REDIRECT_LOOP、TLS_ERROR、NO_CONTENT 和 STALE_ENGINE_VERSION 被标记为不可重试,因为重试会得到相同的答案。 该列表的大部分描述了站点上的一种状态,且其中几项——重定向循环、TLS 错误、现在阻止爬虫的机器人规则——是真正的部署缺陷,值得因其导致构建失败。 该集合之外的代码是瞬态的,值得再次尝试。

将“站点回归”与“审计无法运行”合并为同一红色构建的门控会在一个月内被禁用。 在退出码中保持它们分开:1 用于你要求门控执行的判决,75——传统的 EX_TEMPFAIL——用于完全没有判决的运行。 大多数 CI 系统可以配置为重试第二种情况,并让人类处理第一种情况。

阅读分数块

一旦 ready 为真,GET /api/v1/reports/:id 返回报告对象。 score 块是门控读取的部分:

bash
REPORT=$(curl -sS -H "Authorization: Bearer $SEOREPORT_API_KEY" \
"https://seoreport.dev/api/v1/reports/$JOB_ID")
echo "$REPORT" | jq '.report.score
| {overall, totalChecks, passedChecks, failedChecks,
warnChecks, inconclusiveChecks, failingCriticalChecks}'

这三个字段携带了大部分信号。

failingCriticalChecks 是引擎将其分类为关键的失败计数——那些能够从索引中移除页面或完全隐藏内容给爬虫的失败。 对于第一个门控,这就是你需要的唯一数字,> 0 是第一天可辩护的阈值。

overall 是综合分数,作为一个锁定器使用:存储前一次部署的值,并在新值下降超过你选择的容差时失败。 这捕捉了单个检查未能标记的慢性侵蚀。

domainScores 按 seo、ai、performance、security 和 brand 将分数拆分,每个都有自己的 pass、fail 和 warn 计数。 每域最低值允许不同团队拥有不同预算——安全分数是一个有意义的门控,适用于任何拥有边缘配置的人,而不受内容团队发布的任何影响。

inconclusiveChecks 值得单独制定规则:永远不要基于它进行门控。 当引擎无法得出可辩护的结论时,检查结果被判定为不确定,并将其视为失败会让每个人都学会忽略该门。

分数块是报告摘要的一部分,因此构建门可以在不获取完整发现的情况下读取它。 当你想要完整的负载以供归档时,GET /api/v1/reports/:id/result 返回完整结果,GET /api/v1/reports/:id/download?format=json 返回相同的对象作为可下载的工件——值得附加到构建中,以便以后可以检查两次部署之间的差异。

将其集成到工作流中

上述内容均不涉及 CI 供应商特定。 在 GitHub Actions 中,整个门是一个步骤,调用你已经拥有的脚本:

yaml
- name: SEO audit gate
env:
SEOREPORT_API_KEY: ${{ secrets.SEOREPORT_API_KEY }}
AUDIT_URL: https://example.com
run: ./scripts/seo-gate.sh

seo-gate.sh 提交、轮询并以决策结束:

bash
CRITICAL=$(echo "$REPORT" | jq -r '.report.score.failingCriticalChecks // 0')
SECURITY=$(echo "$REPORT" | jq -r \
'.report.score.domainScores[] | select(.domain == "security") | .score')
if [ "$CRITICAL" -gt 0 ]; then
echo "::error::$CRITICAL critical checks failing"
exit 1
fi
echo "clean — security domain at $SECURITY"

在部署完成后运行,而不是针对预览环境。 预览 URL 通常位于基本身份验证或机器人挑战后,无法获取页面的审计会报告 CRAWLER_BLOCKED 而不是分数。 对真实源进行后部署既更简单,也更接近爬虫的体验。

两个相邻的形状值得了解。 如果你的编排已经托管在抓取平台上,我们的 Apify 角色 在计划上运行相同的引擎,而无需任何此类粘合。 对于代理,使用相同的 bearer key 在 https://seoreport.dev/mcp 处验证 MCP 连接,报告工具在 connect 上公布——正在调查流量下降的代理可以运行审计并自行阅读发现,这在 开发者页面 上的 REST 表面旁边有文档说明。

门不涵盖的内容

部署门回答一个问题:此发布是否引入了回归。 它对所有不通过部署而发生的变化保持沉默——过期证书、在仪表板中编辑的 CDN 配置、开始阻止渲染的第三方脚本、改变你规范冲突的竞争对手迁移。 每域监控是门的始终开启补充:SEO 监控 API 每周使用相同账户和相同密钥重新审核已注册域。

从窄版本开始。 一个端点,一个字段,failingCriticalChecks > 0,以及当审计根本无法运行时的诚实 exit 75。 每年触发两次且两次都被信任的门,比每个人都学会跳过的全面门更有价值。

获取您网站的完整诊断

一份有据可依的报告和优先级行动计划,采用每月积分的订阅方案。