GEO-126·第 11 期·技术底座·126 / 132
把 GEO 接进 CI:构建期检查结构化数据与链接
GEO 的错不该在上线后才发现。本文把 frontmatter、内链与 canonical、hreflang、JSON-LD 归成五类构建期断言,给出 Astro 的三个挂载点、告警分级与一次被拦下的发布,并说明 CI 检查不了的边界。
本期主线规模化:SOP、组织与 2027 路线图
示例场景:十月底,一家做电动滑板车的出海团队赶在黑五前换了商品页模板。本地构建通过,CI 全绿,周四上线。周一早上,客服转来一张聊天助手截图:报价是旧价,“已售罄”来自上个月下架的旧款;德语站 hreflang 还指向改版前的 URL。技术回滚模板,四小时修好,但被重抓的页面、被重新采样的答案、发出去的渠道邮件,都回滚不了。
frontmatter、canonical、hreflang、内链与 JSON-LD 的错,都该在构建期让流水线变红;上线后才发现的问题,修得再快也要重抓、重采样、重新解释一遍。
类型检查过了,引用关系还是错的
Astro 的 content collection 用 zod 校验 frontmatter 的形状:字段在不在、类型对不对、slug 合不合正则。这层当然要有,但它只回答“这篇文档自己是否成形”,不回答“它和其他文档、和生成的路由、和页面上的价格对不对得上”。
GEO 的错几乎全在关系上:slug 在选题表里不存在、两篇的 order 撞车、中文有英文缺、内链指向一个从未生成的页面、JSON-LD 里的价格和正文不是同一个数。这些都不是类型错误,构建会安静通过。
| 错误 | zod / tsc 能发现 | 什么时候暴露 |
|---|---|---|
| frontmatter 缺字段、类型错 | 能 | 构建期 |
| slug 不在计划里、order 重复 | 不能 | 上线后 404 或目录错位 |
| 中文有、英文缺 | 不能 | hreflang 指向空页,几周后才被审计发现 |
| 内链指向未生成的路由 | 不能 | 爬虫撞 404,内链权重断 |
| JSON-LD 价格 ≠ 页面价格 | 不能 | 答案里报错价,富媒体结果告警 |
更麻烦的是放大倍数。一个模板改动会影响成百上千个 URL,一次 hreflang 写错会让整条语言对应关系错位。而 AI 侧的重抓与答案重采样各有节奏,四小时的错误窗口,可能让旧价在答案里活过一整个采样周期。
五类检查,覆盖从 frontmatter 到渲染后的 HTML
断言要落在“关系”和“输出”上。五类检查能覆盖绝大多数会在 AI 侧显形的结构性问题。
| 检查项 | 断言什么 | 失败长什么样 | 级别 |
|---|---|---|---|
| Frontmatter 语义 | code 唯一、order 与选题表一致、slug 与 code 一对一、theme 在五条车道内 | 两篇的 order 都是 126 | 阻断 |
| 链接与 canonical | 每个 /geo/ 内链命中生成的路由;canonical 自指且不是重定向目标 | 内链 404,canonical 指向旧域名 | 阻断 |
| hreflang 配对 | 每个 code 中英成对、双向声明、x-default 指向中文 | 德语页指向改版前的 URL | 阻断 |
| 结构化数据 | Article、BreadcrumbList、Product、Offer 的必填字段齐、枚举合法、URL 为绝对地址 | offer.price 与正文差 20 | 阻断 |
| 输出完整性 | sitemap、RSS、llms.txt、Pagefind 都收录新页,且无孤儿页 | 新文章只在 sitemap,搜不到 | 警告 |
内容层面的门禁(长度、H2 数量、答案单元是否独立成段)是另一套清单,见内容工厂的 GEO 质检清单。两者不重复:一个查“写对没写对”,一个查“接对没接对”。
接进 Astro:三个挂载点,四步上线
与其写一个扫全站的大脚本,不如把每条断言放到离数据最近的地方:单篇内的规则进 schema,Markdown 里的链接进 remark,产物级别的检查进构建钩子。
zod 能校验单篇字段与格式,跨篇的唯一性和配对要另读全部条目;remark 插件在编译阶段能拿到文件路径与行号,报错可以指到具体哪一行;astro:build:done 钩子拿得到最终路由表与产物目录,适合验证“那个链接真的生成了”“JSON-LD 真的出现在 HTML 里”“sitemap 真的收录了”。三处各管一段,报错信息都不会是“某个地方出错了”。
| 步骤 | 做什么 | 谁来做 | 怎么算过 |
|---|---|---|---|
| 1 列断言 | 从过去三个月的线上事故反推,先写 6 条 | 技术 + 内容 | 每条断言都有一个“已知坏样本”能触发它 |
| 2 挂三处 | zod 查单篇、remark 查内链、构建钩子查产物 | 前端 / 平台 | 故意改坏一篇,构建失败并打印文件与行号 |
| 3 分级 | 阻断 / 棘轮 / 警告,白名单写 owner 与到期日 | 技术负责人 | 连续两周没人用 --no-verify 跳过 |
| 4 进门禁 | 检查失败不部署,报告贴回 PR | 平台 | 上线后不再出现链接与结构化数据类工单 |
第 1 步的“已知坏样本”最容易被省掉。没有坏样本,你只是在验证正常情况,断言本身坏掉也不会有人知道。
告警分级:全阻断等于没有阻断
一条规则只有三种命运:阻断、棘轮、记录。全部设成阻断,团队会绕过;全部设成警告,没人会看。
一个常见的反例:把所有检查都设成 error,第一周拦下几十条历史遗留问题,第二周有人在脚本后面加了 || true,第三周全绿,但已经没人相信这个绿灯了。正确做法是按“谁引入的”分级:本次改动新增的问题阻断,历史存量走棘轮,数量只能降不能升,风格类只记录进周报。
| 级别 | 适用 | 处理方式 |
|---|---|---|
| 阻断 | 本次 PR 新增的断链、配对缺失、JSON-LD 必填缺失 | 不允许合并 |
| 棘轮 | 旧文章缺英文、旧 JSON-LD 缺字段 | 记基线,总数不得增加 |
| 警告 | 描述字数、答案单元缺失、标题过长 | 周报汇总,不阻塞发布 |
豁免也要有登记:谁批的、为什么、什么时候到期。没有到期日的白名单,一年后会长成第二套规则。
示例:一次被拦下的发布
以下路径、编号与数字均为示例,只演示断言如何报错。
一个新写的对比页准备上线:frontmatter 齐全,正文有答案单元,本地预览正常。PR 上三条检查全红。
✗ zh/geo-999-vs-page.md:18 link /geo/content/compare-template/ not found
✗ jsonld offer.price 199 ≠ page price 219 (sku AB-999)
✗ hreflang code GEO-999 has zh but no en
第一条是作者引用了还没发布的模板页;第二条是运营在页面改了价,JSON-LD 仍从旧的 offer 数据生成;第三条是英文版还没写。三条都不是代码 bug:本地构建照样通过,预览里只有恰好点到那一条时才会 404。
修复顺序也值得注意:先补英文版,再改价格来源(从同一份 SKU 数据取,不在页面里写死),最后才改链接。顺序反了,价格仍会以两种口径上线。
边界:构建期检查不了的
CI 只能保证“你自己的输出自洽”,保证不了“输出是真的”。
第一,事实本身错了。 JSON-LD 与页面一致,但两者都来自一份过期的价格表,一致性检查会把两个错一起放行。兜底要靠主数据同步,见日志、API 与数据仓库:GEO 数据管线。
第二,渲染后的差异。 构建钩子看到的是产出的 HTML,不是爬虫拿到的 HTML;CDN 改写、WAF 拦截、客户端注入都不在这层。上线后仍要做一次无 JS 抓取与状态码抽检。
第三,引用结果。 全绿只说明页面可被解析,不说明 AI 会引用你。可见度靠抽样,不靠流水线;给管理层的数字看GEO 记分卡。
第四,平台与站外。 marketplace 的 listing、feed 校验、第三方评测都不在你的构建里。
第五,维护成本。 模板一改,断言就过期;没人维护的检查会变成假警报,最后被整体关掉。
这层也不该第一天就建。先按GEO 的 90 天落地路线图把可见性与答案资产修到能跑,再用本文的断言把它们锁住;否则你只是在给一个还没成形的系统上锁。这条线归技术底座,它解决“有没有回归”,不解决“有没有增量”。
这个月先做一件事:挑一篇已发布文章,手工改坏它的一处内链,跑一次完整构建。如果流水线还是绿的,你缺的不是更多检查项,而是第一个挂载点。