刷题平台被 AI 搜索无视了两个月:Quiz 与 QuizQuestion 的错题解析结构化缺位复盘
刷题平台被 AI 搜索无视了两个月Quiz 与 QuizQuestion 的错题解析结构化缺位复盘适用读者做在线教育、知识付费站点的前后端工程师负责内容被 AI 搜索收录的技术负责人正在纠结该给题目页套哪种 Schema.org 类型的 SEO 同学。一个 1.2 万道题的编程刷题平台两个月里被 AI 搜索引用的次数是 0。不是内容不行题目解析写得挺扎实用户反馈也一直不错。问题出在交付方式上题干和解析全藏在前端 JS 交互里用户点「查看解析」按钮才发请求渲染内容AI 爬虫抓到的页面只有一层题目列表骨架。这篇文章把整个排查过程、结构化改造方案以及 Quiz 和 FAQPage 之间的取舍掰开讲一遍。先交代背景。这个站的核心页面结构是列表页加详情页详情页走 React SPA数据从接口异步拉。为了页面首屏快团队把解析部分做成了懒加载用户不点就不请求。这个决策本身对用户体验没毛病坏就坏在它把内容对爬虫藏起来了而且一藏就是两个月没人发现。TL;DRAI 爬虫不执行 JS懒加载解析在抓取时不可见等于不存在。日志排查是关键直接筛 AI 爬虫轨迹别只看收录数。QuizQuizQuestion 更优能表达备选解法FAQPage 做不到。结构化与正文缺一不可acceptedAnswer.text 和可见正文要同时具备。问题是怎么从访问日志里翻出来的发现的契机很偶然。运营同学在某个 AI 助手里搜「JS 数组去重的几种写法」答案推荐了竞品站我们站的题目明明排得更靠前。她把截图丢进群里我顺手查了下访问日志。日志里的情况比想象中难看。用下面这条命令把常见 AI 爬虫的轨迹筛出来# 从 Nginx 访问日志里筛出主流 AI 爬虫的请求轨迹grep-E(GPTBot|ClaudeBot|PerplexityBot|Google-Extended)access.log|awk{print $7}|sort|uniq-c|sort-rn# 再看这些请求最终拿到的状态码确认有没有被误拦grep-E(GPTBot|ClaudeBot|PerplexityBot)access.log|awk{print $9}|sort|uniq-c跑出来的结果两个月里 AI 爬虫一共只请求过 3 个静态页两个是关于订阅说明的一个是首页。1.2 万道题的详情页一次都没被碰过。状态码全是 200robots.txt 也没拦说明不是封禁问题是爬虫压根不知道这些页面值得来抓或者抓了也拿不到东西。这里有个很容易被忽略的点AI 爬虫和传统搜索引擎爬虫的耐心不一样。Googlebot 会执行 JS 渲染队列排几天也认了多数 AI 爬虫抓一次拿到什么就是什么懒得等你异步接口。所以排查不能只看 SEO 工具里的收录数得直接看日志。DOM 快照对比AI 爬虫眼里的页面长什么样光看日志还不够我用无头浏览器把 JS 禁掉模拟了一个「不执行脚本」的抓取把详情页的 DOM 快照存了下来。前后对比放在一张表里对比项JS 禁用后的快照真实浏览器渲染后题干文本存在但嵌在初始 state 里完整可见答案解析完全不存在点击「查看解析」后出现结构化数据无任何 JSON-LD同样没有可提取的语义信息仅导航和列表骨架题目 代码示例 步骤说明结论很直白对一个不执行 JS 的爬虫来说这道题的解析等于不存在。而解析恰恰是这类站最有引用价值的内容——AI 引擎在回答「怎么实现 XX」时引用的就是解析里的思路和代码。抓取与渲染机制剖析把这件事的机制拆开看。AI 搜索引用内容的链路大致是抓取 → 抽取正文 → 切块向量化 → 检索命中 → 生成答案时带引用。这条链路里每一步都依赖 HTML 里直接可读的文本。这就是生成式引擎优化Generative Engine Optimization, GEO和传统 SEO 分岔的地方传统 SEO 至少还有渲染队列兜底GEO 这条链路里 JS 懒加载的内容基本等于黑洞。另一个机制层面的缺口是结构化数据。页面当时连一份 JSON-LD 都没有爬虫只能靠启发式规则猜哪块是题目、哪块是解析。Schema.org 里为教育内容准备的类型其实早就有了Quiz用来声明整卷或整题集QuizQuestion用来声明单道题单题里的acceptedAnswer放标准答案suggestedAnswer放可接受的备选答案解析文本则可以直接放在acceptedAnswer.text里。这几个属性组合起来等于把「这是一道题、答案是什么、解析说了什么」用机器可读的方式拍在桌面上。改造后的内容暴露路径变成了这样AI 爬虫请求详情页SSR 输出完整 HTMLhead 内 JSON-LD: Quiz hasPart正文直接包含解析文本acceptedAnswer.text 中的解析可读正文切块向量化进入 AI 检索索引改造前的链路则断在第二步——爬虫拿到骨架后面全部无从谈起。整个排查阶段我们自己走了一遍完整的取证流程从日志异常到方案落地大约花了三天节奏是这样的运营反馈 AI 引用为零筛访问日志: AI 爬虫轨迹仅 3 个静态页被请求无头浏览器禁 JS 抓 DOM 快照确认解析文本不在 HTML 中选型 Quiz QuizQuestionSSR 注入 JSON-LD 与解析正文validator 校验后灰度上线每一步都留了截图和日志快照后面写改造复盘报告时直接取用省了二次取证的时间。结构化方案Quiz 与 QuizQuestion 怎么组合定了方向之后是选型。教育类内容可选的类型不少我们最终落在QuizQuizQuestion的组合上详情页顶层声明一个QuizhasPart数组里挂这道题的QuizQuestion答案和解析塞进acceptedAnswer。核心的 JSON-LD 长这样{context:https://schema.org,type:Quiz,name:JS 数组去重实现题集,// learningResourceType 明确告诉引擎这是测验类学习资源learningResourceType:Quiz,educationalLevel:Beginner,// hasPart 把题集拆成单题每道题一个 QuizQuestionhasPart:{type:QuizQuestion,name:第 1 题用 Set 实现 ES6 数组去重,// educationalUse 标注这是考核性质的交互educationalUse:assessment,// acceptedAnswer 是标准答案解析文本直接放这里acceptedAnswer:{type:Answer,text:使用 new Set(arr) 去重后用展开运算符还原为数组。Set 内部使用 SameValueZero 算法判等时间复杂度为 O(n)。},// suggestedAnswer 放可接受的备选写法比如手写 filter 实现suggestedAnswer:{type:Answer,text:arr.filter((v, i) arr.indexOf(v) i) 同样可行但时间复杂度为 O(n²)大数组不推荐。}}}配套的 SSR 改造不大因为数据本来就在服务端接口里只是原来留给前端渲染现在提前拼进 HTML。拿 Node 侧举例// 服务端渲染详情页时把题目与解析一并注入 HTMLapp.get(/quiz/:id,async(req,res){// 从题库服务取单题详情含题干、标准答案、解析与备选答案constquizawaitquizService.findById(req.params.id);// 组装 JSON-LD解析文本同时写入 acceptedAnswer.textconstjsonLdbuildQuizJsonLd(quiz);// 正文区也要输出解析文本不能只塞进 JSON-LD// AI 爬虫抽取正文时依赖可见文本两者要同时具备res.render(quiz-detail,{quiz,jsonLd});});上线前用 validator.schema.org 把每类页面模板都验了一遍顺手发现两个坑QuizQuestion必须挂在Quiz或LearningResource上下文里单独放容易被判无效acceptedAnswer和suggestedAnswer都要求是Answer类型直接塞字符串在部分校验路径下会报错。和 FAQPage 的取舍我们为什么没用 FAQ选型时内部争论过一阵FAQPage 是更常见的方案改造模板也现成。但比完之后我们放弃了对比放在这里维度Quiz QuizQuestionFAQPage语义匹配度天然对应「题目—答案—解析」结构只表达「问题—回答」备选答案suggestedAnswer 原生支持无对应属性教育场景标注learningResourceType / educationalUse无富媒体展示部分引擎支持练习题卡片FAQ 手风琴结果AI 引用倾向答案边界清晰便于切块引用整段回答粒度偏粗对刷题站来说一道题的价值密度集中在解析上QuizQuestion的acceptedAnswer恰好给了这个文本一个明确的语义槽位AI 引擎切块的时候不会把题干和解析搅在一起。FAQPage 不是不能用而是它表达不了「这道题还有别的可接受解法」这件事而备选解法恰恰是编程题内容里引用率最高的部分。改造完成后第 5 周日志里的数字变成了这样AI 爬虫日均请求详情页 600 多次AI 搜索带引用的曝光从 0 涨到日均 40 次引用来源集中在解析文本和备选解法两块。流量占比不大但都是高意图的长尾问题注册转化率比搜索来的均值高了一截。把同一道题硬套成 FAQPage 会长这样一眼就能看出语义上的别扭{context:https://schema.org,type:FAQPage,mainEntity:[{type:Question,name:用 Set 实现 ES6 数组去重怎么写,// acceptedAnswer 只能放一个标准回答没有 suggestedAnswer 的槽位acceptedAnswer:{type:Answer,text:使用 new Set(arr) 去重后用展开运算符还原为数组。}// 备选解法filter 实现在这里无处安放只能硬塞进 text 里// 一旦塞进去AI 引擎切块时会把「标准答案」和「备选解法」搅在一起// educationalUse 更是完全没有对应属性无法标注这是考核性质的交互}]}对比之下QuizQuestion的suggestedAnswer给了备选解法一个独立的语义槽位educationalUse能明确标注「assessment」考核属性这两点 FAQPage 的结构里都不存在。硬套 FAQPage 不是不能跑而是把「题目—答案—解析」压扁成了「问题—回答」丢掉了教育场景里最值钱的结构信息。几个踩坑细节值得单独记一笔改 HTML 结构的时候踩过一次回马枪。为了让正文更干净我们把解析区的 DOM 从弹窗挪进了文档流结果弹窗组件的关闭逻辑还挂在旧节点上移动端点「查看解析」会白屏。灰度时靠前端监控的错误聚合抓到的原因很朴素SSR 改造动了组件挂载点交互逻辑没跟着走。所以这类改造一定要把「渲染层」和「交互层」当成两个独立改动分开灰度。另一个坑是解析文本的长度。早期我们把完整解析含三段代码示例全塞进acceptedAnswer.text部分 AI 引擎抽取时会截断长文本引用出来的片段恰好断在代码中间观感很差。后来把代码示例留在正文 HTML 里text只放思路概述两三百字以内引用质量立刻稳定了。还有个小细节hasPart挂多题时注意position属性题集类页面 AI 引擎偶尔会按顺序引用多题没有position就会乱序。误区澄清或趋势预判常见误区有两个。一个是把结构化数据当成万能票以为塞了 JSON-LD 内容就能被引用——acceptedAnswer.text和可见正文缺一不可很多引擎会交叉校验只写结构化数据的页面反而有被判低质的风险。另一个是等搜索引擎渲染队列传统搜索或许等得起GEO 这条链路的抓取器普遍不执行 JS懒加载内容在它们眼里就是不存在。往后看教育类内容在 AI 搜索里的竞争会越来越吃「结构清晰度」的红利。题目、答案、解析、难度、知识点这些字段越是机器可读越容易被组装进 AI 的回答里。如果你手里也有内容躺在交互组件后面先去翻一遍访问日志看 AI 爬虫到底来过没有、拿走了什么——数据会告诉你该不该动手。参考与延伸Schema.org Quiz 类型定义Schema.org QuizQuestion 类型定义Schema 标记校验工具GEO、AI搜索、Schema.org、JSON-LD、Quiz、QuizQuestion、在线教育