☰
刷题平台被 AI 搜索无视了两个月:Quiz 与 QuizQuestion 的错题解析结构化缺位复盘
2026/9/26 20:16:00 网站建设 项目流程

刷题平台被 AI 搜索无视了两个月:Quiz 与 QuizQuestion 的错题解析结构化缺位复盘

适用读者:做在线教育、知识付费站点的前后端工程师;负责内容被 AI 搜索收录的技术负责人;正在纠结该给题目页套哪种 Schema.org 类型的 SEO 同学。

一个 1.2 万道题的编程刷题平台,两个月里被 AI 搜索引用的次数是 0。不是内容不行,题目解析写得挺扎实,用户反馈也一直不错。问题出在交付方式上:题干和解析全藏在前端 JS 交互里,用户点「查看解析」按钮才发请求渲染内容,AI 爬虫抓到的页面只有一层题目列表骨架。这篇文章把整个排查过程、结构化改造方案,以及 Quiz 和 FAQPage 之间的取舍掰开讲一遍。
先交代背景。这个站的核心页面结构是列表页加详情页,详情页走 React SPA,数据从接口异步拉。为了页面首屏快,团队把解析部分做成了懒加载,用户不点就不请求。这个决策本身对用户体验没毛病,坏就坏在它把内容对爬虫藏起来了,而且一藏就是两个月没人发现。

TL;DR

  • AI 爬虫不执行 JS:懒加载解析在抓取时不可见,等于不存在。
  • 日志排查是关键:直接筛 AI 爬虫轨迹,别只看收录数。
  • Quiz+QuizQuestion 更优:能表达备选解法,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 万道题的详情页,一次都没被碰过。状态码全是 200,robots.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 输出完整 HTML

head 内 JSON-LD: Quiz + hasPart

正文直接包含解析文本

acceptedAnswer.text 中的解析

可读正文切块向量化

进入 AI 检索索引

改造前的链路则断在第二步——爬虫拿到骨架,后面全部无从谈起。整个排查阶段我们自己走了一遍完整的取证流程,从日志异常到方案落地大约花了三天,节奏是这样的:

运营反馈 AI 引用为零

筛访问日志: AI 爬虫轨迹

仅 3 个静态页被请求

无头浏览器禁 JS 抓 DOM 快照

确认解析文本不在 HTML 中

选型 Quiz + QuizQuestion

SSR 注入 JSON-LD 与解析正文

validator 校验后灰度上线

每一步都留了截图和日志快照,后面写改造复盘报告时直接取用,省了二次取证的时间。

结构化方案:Quiz 与 QuizQuestion 怎么组合

定了方向之后是选型。教育类内容可选的类型不少,我们最终落在Quiz+QuizQuestion的组合上:详情页顶层声明一个Quiz,hasPart数组里挂这道题的QuizQuestion,答案和解析塞进acceptedAnswer。核心的 JSON-LD 长这样:

{"@context":"https://schema.org","@type":"Quiz","name":"JS 数组去重实现题集",// learningResourceType 明确告诉引擎这是测验类学习资源"learningResourceType":"Quiz","educationalLevel":"Beginner",// hasPart 把题集拆成单题,每道题一个 QuizQuestion"hasPart":{"@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)=>{// 从题库服务取单题详情,含题干、标准答案、解析与备选答案constquiz=awaitquizService.findById(req.params.id);// 组装 JSON-LD,解析文本同时写入 acceptedAnswer.textconstjsonLd=buildQuizJsonLd(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、在线教育

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询