课程大纲被 AI 引擎截断之后:syllabusSections 的结构化写法与 30 天对照

2026-09-29 01:28:28 0 次浏览
GEOAI搜索syllabusSectionsSchema.orgJSON-LD在线教育

后台忽然多了一串奇怪的日志:站内一门在线课程的详情页,过去 30 天被各类生成式引擎引用了 47 次,可逐条点开引用原文,清一色只有课程标题那一行,十二个章节名一个字都没带出来。用户问「这门课具体讲什么」,AI 只能复述标题,再临场编两句。大纲明明白白写在页面上,进了抓取管线却像不存在。

先拆现场:引用了 47 次,章节名只出现半句

发现问题的路径很朴素。八月底的一个周五,运营同事整理引用数据,把近 30 天里引用过站内课程页的 AI 回答逐条拉出来,一共 47 条。按引用粒度手工分组:43 条只引用了页面标题和 meta description;3 条引用了讲师介绍段;只有 1 条碰到了章节列表,还截了半句,停在「第 4 章 DOM 事件与……」。

课程大纲章节清单的主题图

页面本身不缺内容。这门课有 12 个章节、每章 4 到 6 节,全部渲染在详情页第三屏的折叠面板里,旁边还有一个「展开全部章节」的按钮。拆下来问题出在两层:

  1. 折叠面板的章节文字初始状态是 display: none,要靠 JS 点击才展开。不少生成式引擎的抓取管线分两个阶段:先做纯 HTML 解析,成本高的是 JS 渲染,为控制开销很多引擎砍掉或降级第二阶段。藏在折叠里的内容在纯解析阶段权重会被压低,甚至整段丢弃;
  2. 页面的 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 主题词 蹭热词、与正文脱节

踩过的四个坑,比规范本身更值钱

  1. 只改 JSON-LD 不动 HTML,前 8 天引用数据纹丝不动。HTML 回流上线后一周才出现拐点,两条通道缺一条都瘸。
  2. 章节命名里带表情符号和全角空格,个别引擎的引用片段会把空格还原成问号。改成半角空格加名词短语后消失。
  3. 折叠面板从全收起改为默认展开前 4 章后,第三屏的渲染布局要重新过一遍移动端,别只顾抓取忘了用户。
  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、结构化数据、在线教育

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