在线客服脚本把 JSON-LD 洗掉了:一次结构化数据渲染丢失的排查复盘

2026-09-30 01:28:19 0 次浏览
GEOAI搜索JSON-LD踩坑前端调试

9 月 12 日下午,运营同事拿着截图来找我:Perplexity 回答里连续两周引用我们官网的次数从每周 17 次掉到了 6 次,而 Rich Results Test 时好时坏——上午测还是绿的,下午再测就报「页面未检测到结构化数据」。那段时间仅有的页面变更,是 9 月 10 日上线的第三方在线客服 SDK。这篇文章完整复盘这次排查:客服脚本的 DOM 重渲染逻辑把 <script type="application/ld+json"> 节点洗掉了,以及我们怎么定位、怎么修复、修复前后 AI 引用数据差了多少。

一、背景:改版上线了两样东西

8 月底官网改版收尾,为了做 GEO,我们把整站的 JSON-LD 结构化数据(Organization、WebSite、FAQPage、BreadcrumbList 四类)从模板里输出到了 <head>,并在 9 月 5 日用 Google Rich Results Test 逐页验收通过。9 月 10 日,业务方要求接入某第三方在线客服 SaaS,接入方式很简单——市场部从后台复制了一段 <script src="https://kf.example-saas.com/loader.js"> 贴到了所有页面的 </body> 前,没有任何时序控制。

在线客服脚本把 JSON-LD 洗掉了:一次结构

上线第三天,怪事出现了。

二、现象:忽好忽坏的验证结果

三个症状叠加在一起,起初很容易误判:

  1. Rich Results Test 有时通过、有时报「页面未检测到结构化数据」,同一个 URL 隔几小时重测结果会翻转;
  2. 用浏览器打开页面,F12 看 Elements 里 JSON-LD 明明存在;但对着 view-source: 看,节点也在——两边看似都对不上症状;
  3. 换了几台设备、换了网络出口测,结果也不一致,像是玄学。

先把三个症状和排查动作整理成表,后面逐个展开:

# 症状 初步怀疑 排查动作 真实原因
1 富媒体测试忽好忽坏 缓存不一致 带 Cache-Control: no-cache 请求对比 客服 SDK 异步重渲染 body
2 渲染后 DOM 无 JSON-LD 模板缺失 view-source 对比 DevTools 渲染后 DOM SDK 清理了「陌生」script 节点
3 部分出口测正常 CDN 边缘节点版本差异 curl 直连源站 vs 走 CDN CDN 缓存了旧版无客服脚本的页面

三、排查路径:三步锁定元凶

3.1 view-source 与渲染后 DOM 对比

第一件事是把「源码」和「运行时」分开看。view-source: 拿到的是服务器返回的原始 HTML,这里 JSON-LD 完整无缺;切到 DevTools 的 Elements 面板,搜索 ld+json,十次里有六七次搜不到,偶尔又能搜到。源码有、运行时无,说明节点是在页面生命周期里被删掉的,不是模板问题。

3.2 Rich Results Test 的报错口径

Google 官方文档明确说 Rich Results Test 渲染的是执行 JavaScript 之后的 DOM。这解释了症状 1:无头浏览器渲染耗时不同,客服 loader.js 拿到焦点、执行重渲染的时机和测试抓取快照存在竞态,所以结果忽好忽坏。这也意味着只有 view-source 通过不能证明 GEO 语义可用,AI 爬虫与测试工具看到的都是渲染后的页面。

3.3 CDN 缓存干扰

症状 3 差点把我们带偏。走 CDN 的部分边缘节点命中的是 9 月 10 日改版部署前的旧缓存(还没接客服脚本),测出来全是绿的;直连源站的新版本页面才是坏的。我们对重点 URL 提交了缓存刷新,curl -H "Cache-Control: no-cache" 复测后症状 3 消失,剩下的才是真正的主因。

排查链路用一张图收拢:

flowchart TD
    A[Rich Results Test 忽好忽坏] --> B{view-source 有 JSON-LD?}
    B -- 有 --> C{DevTools 渲染后 DOM 有?}
    C -- 偶尔有 --> D[怀疑竞态: 监听 DOM 变更]
    C -- 多数没有 --> E[锁定: 运行时被删除]
    B -- 没有 --> F[查模板与部署]
    A --> G[部分出口正常]
    G --> H{绕过 CDN 直连源站}
    H -- 结果翻转 --> I[先刷新 CDN 缓存]
    I --> D
    E --> J[MutationObserver 记录删除者]
    J --> K[定位客服 SDK 重渲染]

四、原理/机制剖析:客服 SDK 是怎么洗掉节点的

为了拿到证据,我们在测试页注入了一段 MutationObserver,监听 childList 和 subtree 变更,把每次 removeChild 的节点序列化后上报。结果很清楚:客服 loader.js 初始化后会执行一次「全量重渲染」——它把自己的挂载逻辑包在 document.body.innerHTML = sanitize(document.body.innerHTML) 这样的重写里,sanitize 函数只保留白名单标签和带自家前缀 id 的 script,其余 <script> 节点(包括 JSON-LD)一律当垃圾清掉。这正是它设计上的「防注入」策略,只是把合法的结构化数据也误伤了。

另一个分支是部分版本 SDK 会把 body 末尾的脚本节点「挪走」再重建挂载容器,挪动过程中没有保留原节点属性,type="application/ld+json" 变成 type="text/javascript",搜索引擎解析时直接忽略。

结论:第三方脚本的 DOM 全量重写是 JSON-LD 丢失的高发场景,任何 innerHTML 级别的 body 重写都会把 head/body 里不被它认识的节点洗掉。

五、修复方案

5.1 客服脚本加载时序改造

第一原则:让客服脚本晚于一切对它有依赖的渲染动作,且不要让它有机会在 DOMContentLoaded 之前动 DOM。改造后的加载代码(依赖:原生 JavaScript,Chrome 120 / Edge 120 验证,无框架版本要求):

// 环境说明: 原生 JS, 无需构建, Chrome 120+ 验证通过
// 改造点一: 客服脚本从内联直接加载改为 onload 后动态注入
window.addEventListener('load', function () {
  // load 事件晚于 DOMContentLoaded, 此时首屏渲染与爬虫快照已基本完成
  var s = document.createElement('script');
  s.src = 'https://kf.example-saas.com/loader.js?v=20260910';
  // async 保证不阻塞; 绝不能改成 defer+同步位置, 否则它会更早介入 DOM
  s.async = true;
  // 记录注入时间, 方便与后端监控里的快照时间做关联
  s.dataset.injectedAt = Date.now();
  document.body.appendChild(s);
});

// 改造点二: 兜底监视器, 发现 JSON-LD 被删立即原样恢复
var guard = new MutationObserver(function (muts) {
    muts.forEach(function (m) {
      // 只关心 childList 移除事件
      m.removedNodes.forEach(function (node) {
        var isLd = node.tagName === 'SCRIPT'
          && node.type === 'application/ld+json';
        // 未命中结构化数据节点: 直接放行
        if (!isLd) return;
        // 深拷贝内容后重新插回 head, 保证爬虫下次快照能读到
        if (!document.contains(node)) {
          var clone = document.createElement('script');
          clone.type = 'application/ld+json';
          clone.textContent = node.textContent;
          document.head.appendChild(clone);
        }
      });
    });
});
// subtree: true 覆盖整棵文档树, 保证 head 内变更也能捕获
guard.observe(document.documentElement, {
  childList: true, subtree: true
});

guard 逻辑上线一周,监控日志里捕获到 43 次「SDK 删除 JSON-LD 后被恢复」事件,说明这不是偶发竞态,而是每次初始化必现。

5.2 JSON-LD 注入时机前移 + 服务端直出校验

我们的 JSON-LD 本来就是服务端模板直出的,问题不在注入时机而在「注入后会被洗」。但对部分由前端框架渲染的落地页,我们额外加了一层:DOMContentLoaded 之后由脚本主动检查 document.querySelectorAll('script[type="application/ld+json"]').length,数量不足模板约定值就补注入,并上报告警。

5.3 CSP 与 SRI 注意点

给第三方脚本配内容安全策略(Content Security Policy, CSP)时踩了两个坑:一是给客服域名开了 unsafe-eval 才能跑,这会削弱防护,建议尽量只放开它的具体 host;二是想给 loader.js 加子资源完整性(Subresource Integrity, SRI)校验,结果发现该 SaaS 的 loader 会 302 到版本化地址,hash 随版本变化,硬编码会直接加载失败。妥协方案是 CSP 里用 hash 限定它加载的 worker 脚本,主 loader 保持可变,另用 Referrer-Policy 和域名白名单收敛风险。相关配置示例:

# 环境说明: Nginx 1.24, 直接加在 server 块的 add_header 区
add_header Content-Security-Policy "script-src 'self' https://kf.example-saas.com 'sha256-xxxxxxx';" always;
# always 关键字保证 4xx/5xx 响应也带上 CSP 头
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 不给 loader 加 SRI: 它经 302 跳转到版本化地址, hash 不可稳定预知

六、修复前后数据对比

9 月 18 日全量上线修复(时序改造 + guard 监视器 + CDN 刷新),观察两周(9/19–9/30)与出问题两周(9/10–9/21,取重叠可比的 9/12–9/21 共 10 天做基线)对比。数据来自我们自己的服务端日志统计(AI 爬虫 User-Agent 访问)与各平台站长后台,均为第一方观测值:

指标 修复前(9/12–9/21) 修复后(9/19–9/30) 变化
Rich Results Test 通过率(每日抽测 20 个 URL) 35%–70% 波动 100%,连续 12 天稳定 消除波动
渲染后 DOM 中 JSON-LD 存在率 约 62% 100% +38pp
AI 搜索引擎每周引用官网次数 17 → 6(两周下滑) 第 2 周回升至 14,第 3 周 19 恢复并超过基线
FAQ 富媒体结果展示条数(日均) 0–3 条 5 条(与改版期持平) 恢复

整个事故时间线也可以用一张时序图概括:

sequenceDiagram
    participant U as 用户浏览器
    participant P as 页面(含JSON-LD)
    participant K as 客服SDK loader.js
    participant G as Guard监视器
    U->>P: 请求页面
    P-->>U: 返回HTML(head内含ld+json)
    U->>K: load后动态注入loader
    K->>P: innerHTML全量重渲染body
    P-->>K: sanitize丢弃ld+json节点
    G->>P: MutationObserver捕获移除
    G->>P: 深拷贝并重新注入ld+json
    Note over P: 渲染后DOM恢复完整结构化数据

七、复盘结论

三点带走的经验:

1. 验证结构化数据必须看渲染后 DOM。 view-source 通过不等于 GEO 可用,AI 爬虫、富媒体测试工具看到的都是执行 JS 后的页面;接了任何重 DOM 的第三方脚本后,每天固定抽测比一次性验收可靠。

2. 对第三方 SDK 的 DOM 写入方式要过一遍。 接入前问一句「你们会不会重写 body」,接入后用 MutationObserver 挂个监视器做护栏,成本几十行代码,这次帮我们兜住了全部事故。

3. 缓存会制造假象。 排查「忽好忽坏」类问题,先用 no-cache 请求剥掉 CDN 干扰,再谈别的,否则排查方向会被边缘节点的旧版本带偏。

如果你也在做 AI 搜索优化时遇到过「数据明明写了却不被引用」的问题,欢迎在评论区贴出你的排查线索,我们可以一起对照看看是不是同类 DOM 覆盖问题。

参考与延伸

  1. Google 搜索中心:结构化数据入门与富媒体结果测试说明 —— https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
  2. Google Rich Results Test 工具页 —— https://search.google.com/test/rich-results
  3. schema.org 官方词汇表(JSON-LD 语法) —— https://schema.org/docs/gs.html
  4. MDN:Content Security Policy (CSP) 与 Subresource Integrity —— https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP

关键词:GEO、AI搜索、JSON-LD、DOM覆盖、结构化数据、在线客服SDK、排查复盘

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。