在部署管道中加入 SEO 审计:API、退出码与 CI 配方
一个可用的 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;完整参考见开发者页面。
提交报告是一条 POST:
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 对象:
for _ in $(seq 1 60); doREADY=$(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" ]; thenecho "audit did not complete: $CODE"exit 75fi[ "$(echo "$READY" | jq -r '.ready')" = "true" ] && breaksleep 5done
那个 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 块是门控读取的部分:
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 中,整个门是一个步骤,调用你已经拥有的脚本:
- name: SEO audit gateenv:SEOREPORT_API_KEY: ${{ secrets.SEOREPORT_API_KEY }}AUDIT_URL: https://example.comrun: ./scripts/seo-gate.sh
seo-gate.sh 提交、轮询并以决策结束:
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 ]; thenecho "::error::$CRITICAL critical checks failing"exit 1fiecho "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。 每年触发两次且两次都被信任的门,比每个人都学会跳过的全面门更有价值。