课程大纲被 AI 引擎截断之后:syllabusSections 的结构化写法与 30 天对照
后台忽然多了一串奇怪的日志:站内一门在线课程的详情页,过去 30 天被各类生成式引擎引用了 47 次,可逐条点开引用原文,清一色只有课程标题那一行,十二个章节名一个字都没带出来。用户问「这门课具体讲什么」,AI 只能复述标题,再临场编两句。大纲明明白白写在页面上,进了抓取管线却像不存在。
先拆现场:引用了 47 次,章节名只出现半句
发现问题的路径很朴素。八月底的一个周五,运营同事整理引用数据,把近 30 天里引用过站内课程页的 AI 回答逐条拉出来,一共 47 条。按引用粒度手工分组:43 条只引用了页面标题和 meta description;3 条引用了讲师介绍段;只有 1 条碰到了章节列表,还截了半句,停在「第 4 章 DOM 事件与……」。

页面本身不缺内容。这门课有 12 个章节、每章 4 到 6 节,全部渲染在详情页第三屏的折叠面板里,旁边还有一个「展开全部章节」的按钮。拆下来问题出在两层:
- 折叠面板的章节文字初始状态是
display: none,要靠 JS 点击才展开。不少生成式引擎的抓取管线分两个阶段:先做纯 HTML 解析,成本高的是 JS 渲染,为控制开销很多引擎砍掉或降级第二阶段。藏在折叠里的内容在纯解析阶段权重会被压低,甚至整段丢弃; - 页面的 JSON-LD(JavaScript Object Notation for Linked Data,用 JSON 承载结构化数据的写法)里只有
Course的名称、描述和hasCourseInstance,没有任何承载大纲结构的属性。引擎拿到的是一份「有课无纲」的骨架,想引用章节也没有字段可引。
这等于把「课程讲什么」这道题完全交给了引擎自由发挥。自由发挥的代价肉眼可见:人工抽查 50 条相关 AI 回答,有 9 条在编不存在的章节,比如把讲师的博客文章标题当成了第 7 章。
syllabusSections 是什么,为什么轮到它出场
schema.org 在 Course 类型下有一个专为大纲准备的属性:syllabusSections,类型是 Text,可以重复出现,每条对应课程的一个章节标题。它和 coursePrerequisites、educationalLevel 属于同一批面向课程信息搜索体验的字段,定位就是给机器读的「课程目录」。
改造前先给 Course 常用属性分了工,整理成速查表:
| 属性 | 承载内容 | 对 AI 回答的意义 |
|---|---|---|
| name | 课程标题 | 引用频率最高,但信息量薄 |
| description | 课程简介 | 回答「学完能做什么」 |
| syllabusSections | 章节标题列表,可重复 | 回答「具体讲什么」,补齐大纲 |
| hasCourseInstance | 开课时间、形式、授课人 | 回答「什么时候、以什么形式上」 |
| about | 课程主题词 | 关联知识点,辅助主题召回 |
| provider | 主办机构 | 信任信号 |
分工理清之后结论很直接:标题负责被记住,大纲负责被引用。 AI 回答「这门课都有哪些内容」这类问题时,最靠谱的来源就是结构化的大纲字段;靠正文折叠面板去赌引擎渲染,赌输一次就丢一批引用。
原理机制:生成式引擎到底怎么读一份课程页
这一节讲底层机制,不是操作罗列。GEO 和传统 SEO 的分水岭在「片段可引用性」:传统引擎排完序给一个链接,用户点进去自己读全文;生成式引擎要把回答拆成一句一句的证据,每句尽量对应页面上一段结构清晰、边界分明的文本。章节标题天然就是这种文本——短、平行、编号化,和模型训练语料里的目录体高度一致,模型引用它们几乎不费力气。
flowchart TD
A[AI 爬虫抓取课程页 HTML] --> B{能否解析出 JSON-LD}
B -- 能 --> C[还原 Course 实体与全部字段]
B -- 不能 --> D[仅凭标题与正文文本猜测]
C --> E[syllabusSections 提供章节序列]
C --> F[hasCourseInstance 提供开课信息]
C --> G[about 提供主题词关联]
E --> H[回答时可逐条引用章节名]
F --> H
G --> H
D --> I[只能概括标题与简介]
I --> J[大纲被截断或自行编造]
再看折叠面板为什么会拖后腿。主流引擎的抓取渲染器对「初始不可见」内容的处理策略各不相同,有的直接跳过,有的压低权重。把大纲同时放进 JSON-LD 和首屏可抓取 HTML,等于给同一段信息铺两条通道:一条给解析器走,一条给渲染器走,断一条另一条还在。这也是后面改造两条腿走路的原因——第 8 天的数据证明,只改其中一条几乎纹丝不动。
动手改造:JSON-LD、HTML、自检脚本三件套
改造在第 0 天的周三上线,动的是课程详情页模板。下面 JSON-LD 里的章节做了精简示范,实际站点写全了 12 章。
给 Course 加上 syllabusSections
依赖与环境:无需额外依赖,直接改页面模板里的 <script type="application/ld+json"> 块。原有 JSON-LD 只动两处:Course 增加 syllabusSections 数组;about 换成和正文一致的主题词数组。
{
"@context": "https://schema.org",
"@type": "Course",
"name": "Web 前端工程师训练营:从布局到上线",
"description": "覆盖 HTML、CSS、JavaScript 与工程化工具链的在线实战课程",
"syllabusSections": [
"第 1 章 HTML 语义与页面结构",
"第 2 章 CSS 布局:Flex 与 Grid",
"第 3 章 JavaScript 核心语法",
"第 4 章 DOM 事件与交互",
"第 5 章 异步编程与接口请求",
"第 6 章 工程化:打包与部署"
],
"about": ["前端开发", "响应式布局", "JavaScript"],
"hasCourseInstance": {
"@type": "CourseInstance",
"courseMode": "online",
"courseWorkload": "P6W"
},
"provider": { "@type": "Organization", "name": "站点教学中心" }
}
大纲文本回流为可抓取 HTML
依赖与环境:纯静态 HTML 改动,不依赖框架版本;折叠组件改成默认展开前 4 章,其余点击展开。
<!-- 课程大纲区块:默认展开,不再依赖 JS 点击后才可见 -->
<section class="syllabus" aria-label="课程大纲">
<h2>课程大纲</h2>
<!-- 列表顺序与 JSON-LD 的 syllabusSections 保持一致 -->
<ol>
<li>第 1 章 HTML 语义与页面结构</li>
<li>第 2 章 CSS 布局:Flex 与 Grid</li>
<li>第 3 章 JavaScript 核心语法</li>
<!-- 章节后保留一小段摘要,给引擎提供可引用短句 -->
<li>第 4 章 DOM 事件与交互(含事件委托与防抖实战)</li>
<li>第 5 章 异步编程与接口请求</li>
<!-- 第 5 章起仍走折叠组件,展开后内容同在 DOM 中 -->
<li>第 6 章 工程化:打包与部署</li>
</ol>
</section>
上线前的字段自检脚本
依赖与环境:Python 3.9+,无第三方库,命令行直接运行。
# 依赖与环境:Python 3.9+,无第三方库,命令行直接运行
# 用途:课程页上线前校验 JSON-LD 关键字段是否齐全
# 背景:改造第 0 天全站 34 个课程页用它扫了一遍,扫出 3 个缺字段
import json
import re
import sys
def check_course_jsonld(html_path):
# 读入页面源码,统一按 UTF-8 处理
html = open(html_path, encoding="utf-8").read()
# 用正则提取全部 JSON-LD 块,一个页面可能带多个实体
# 兼容单引号与双引号写法的 script 标签
blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
# 页面连结构化数据都没有,直接判失败
if not blocks:
print("未找到 JSON-LD 块")
return False
ok = True
for block in blocks:
# 逐块解析成字典再校验
data = json.loads(block)
# 只处理 Course 类型,其他实体跳过
if data.get("@type") != "Course":
continue
# 大纲字段:至少 3 条章节
# 顺序必须和页面可见列表一致,引擎会交叉核对
sections = data.get("syllabusSections", [])
if len(sections) < 3:
print("syllabusSections 少于 3 条")
ok = False
# 开课实例缺失,AI 回答里就没有时间与形式
if "hasCourseInstance" not in data:
print("缺少 hasCourseInstance")
ok = False
# 主题词留空会削弱知识关联,至少写一条
if not data.get("about"):
print("about 字段为空")
ok = False
# 命名检查:带全角空格的章节名,在部分引擎引用片段里会变成问号
# 这是改造第一周真实踩到的坑
for s in sections:
# 逐条检查,命中就标失败
if "\u3000" in s:
print(f"章节名含全角空格:{s}")
ok = False
# 三类检查全部通过才返回 True
return ok
if __name__ == "__main__":
# 命令行参数传入课程页 HTML 文件路径
# 退出码 0 表示校验通过,可挂在发布流水线上当卡点
sys.exit(0 if check_course_jsonld(sys.argv[1]) else 1)
第 0 天晚上用这份脚本扫了全站 34 个课程页,3 个页面报「缺少 hasCourseInstance」,两个是老课,一个是模板分支漏改,当天一起补掉。字段级检查放在上线前,比上线后再从引用数据里反推便宜得多。上线完成后又用 shell 手动确认了一遍引擎侧拿到的是新模板:
# 依赖与环境:任意 Unix shell,curl 任意版本
# 抓渲染后的课程页,喂给上一节的自检脚本
curl -s "$PAGE_URL" -o /tmp/course.html
# 运行字段校验,看输出有没有报错行
python check_course.py /tmp/course.html
# 校验不通过时退出码非 0,发布流水线据此卡住
# 第 0 天扫出的 3 个缺字段页面,就是这条命令拦下来的
30 天对照:数据怎么走的
观察期定义为上线后第 1 天到第 30 天,统计口径沿用改前的手工分组方法,每周固定周五汇总一次。
timeline
title 改造上线后的 30 天
第 0 天 : 上线新模板与自检脚本 : 主动提交重抓请求
第 1 周 : 基线不变 : 各引擎陆续重抓
第 2 周 : 长尾课程先见效 : 回答里开始出现章节名
第 3 到 4 周 : 章节引用稳定增长 : 对照表定型
对照表如下,同一统计口径下的手工计数:
| 指标 | 上线前 30 天 | 上线后 30 天 | 说明 |
|---|---|---|---|
| 生成式引擎引用课程页次数 | 47 | 71 | 引用总量本身在涨 |
| 回答中出现章节名的条数 | 1 | 26 | 增量大头来自长尾问题 |
| 章节名被完整引用(未截断)条数 | 0 | 21 | 命名规范化后截断明显变少 |
| 回答附带开课时间或线上形式 | 4 | 19 | hasCourseInstance 起效 |
| 抽查 50 条中编造章节的次数 | 9 | 2 | 结构化越齐,幻觉越少 |
几点观察是写进周报的原话:引用总量的增长不完全是大纲带来的,也有内容更新的自然波动;但章节名从 1 条到 26 条,时间点恰好压在 HTML 回流那一周,归因链路是干净的;编造章节的下降幅度最大,说明引擎在结构化字段齐备时,宁可少引用也不愿意瞎编。
和 hasCourseInstance、about 的配合写法
单写 syllabusSections 不够,三个字段要当成一套来讲,各管一个回答维度。
syllabusSections 管「讲什么」。每条写「第 N 章 + 名词短语」,别带营销后缀。改之前这门课的章节名有类似「小白也能上手的布局魔法」这种写法,第 2 周替换成「第 2 章 CSS 布局:Flex 与 Grid」之后,完整引用率才爬起来。命名越像目录,越接近模型语料里的目录体,截断率也就越低。
hasCourseInstance 管「怎么上」。online 模式、每周工作量、开课批次都塞进 CourseInstance。AI 回答「这门课要学多久」时,答案就从这里来。对照表里开课信息引用从 4 条涨到 19 条,靠的是这个字段,跟大纲无关。
about 管「属于哪个知识域」。写法要点是和课程页正文用同一套主题词,别去蹭不相关的热词。曾有同事想加「职场效率」这种泛词博召回,被审掉了:主题词与大纲内容对不上,反而稀释实体一致性。
| 字段 | 管什么 | 常见写法错误 |
|---|---|---|
| syllabusSections | 章节序列 | 加营销后缀、顺序与页面列表不一致 |
| hasCourseInstance | 开课形式与时间 | 多批次课程只写一个实例 |
| about | 主题词 | 蹭热词、与正文脱节 |
踩过的四个坑,比规范本身更值钱
- 只改 JSON-LD 不动 HTML,前 8 天引用数据纹丝不动。HTML 回流上线后一周才出现拐点,两条通道缺一条都瘸。
- 章节命名里带表情符号和全角空格,个别引擎的引用片段会把空格还原成问号。改成半角空格加名词短语后消失。
- 折叠面板从全收起改为默认展开前 4 章后,第三屏的渲染布局要重新过一遍移动端,别只顾抓取忘了用户。
- sitemap 更新后各引擎的重抓节奏差异很大,快的 3 天,慢的两周多。评估窗口至少留满 30 天,第 1 周的数据不下任何结论。
参考与延伸
- schema.org Course 类型定义:https://schema.org/Course
- syllabusSections 属性定义:https://schema.org/syllabusSections
- Google 搜索中心课程结构化数据文档:https://developers.google.com/search/docs/appearance/structured-data/course
- 百度搜索资源平台:https://ziyuan.baidu.com/
结尾聊两句
误区先澄清:结构化数据不只是给富摘要(Rich Results)用的。在生成式引擎这里,它更像给回答准备的素材库,字段越齐,AI 搜索越有机会把你的内容当成证据原样引用,而不是绕过你的大纲自由发挥。
趋势上可以预判:教育类实体被要求提供的字段会越来越细,课程大纲这种「平行短句集合」是幻觉率最低的可引用素材之一,早补早受益。你们站点上的课程页,AI 引用的是标题还是章节?评论区贴一条引用原文看看。
关键词:GEO、AI 搜索、syllabusSections、Course Schema、JSON-LD、结构化数据、在线教育