课程页的 GEO 细节:accessMode 无障碍声明与 AI 可读性的 45 天改造记录
正在关注生成式搜索带来的流量变化,想让课程页在 AI 搜索里更可读、更容易被正确引用。 对 schema.org 有基本概念,能看懂 JSON-LD 就够了。
8 月中旬我们复盘站点日志时发现一个扎心的现象:站内 340 多门课,被 AI 引擎抓取后真正在回答里引用过的,不到 20 门。被引用的那十几门有个共同点——课程页结构化数据里写了比较完整的 CreativeWork 字段,而没被引用的大多数,页面除了 title 和 description,AI 引擎几乎拿不到任何关于内容形态的信息。这门课是视频还是图文?要不要登录才能看?是不是付费内容?AI 引擎靠猜,猜不准就干脆不引。
后来我们花了 45 天,给全部课程页补齐了 schema.org 的 accessMode、accessModeSufficient 和 conditionsOfAccess 三个属性。这事儿本质上属于生成式引擎优化(Generative Engine Optimization, GEO)的范畴:不是讨好传统搜索排名,而是让 AI 引擎抓取、理解并愿意引用你的内容。这篇是完整的改造记录,包括字段怎么填、坑在哪、前后变化。
这三个属性到底是干嘛的
先说结论:这三个字段解决的都是「内容形态与获取边界」的声明问题,但侧重点完全不同。很多课程站连 isAccessibleForFree 都没写,更别提这三个了。

| 属性 | 类型 | 回答的问题 | 课程页典型取值 |
|---|---|---|---|
| accessMode | Text | 内容通过哪种感官通道消费? | textual / visual / auditory |
| accessModeSufficient | ItemList | 只靠某一种通道能否完整消费? | textual(图文完整)、auditory(纯音频完整) |
| conditionsOfAccess | Text | 消费内容需要什么前提条件? | 无需注册 / 需免费注册 / 付费会员 |
| isAccessibleForFree | Boolean | 是否免费?(配合字段) | true / false |
accessMode 是 CreativeWork 级别的属性,值来自 schema.org 定义的一组固定词表:textual、visual、auditory、tactile 等等。一门视频课通常写 auditory 加 visual;图文讲义写 textual;带字幕的视频课可以三个都写,用数组形式。
accessModeSufficient 更微妙,它声明的是「充分条件」。比如一门课是视频配完整文字稿,那 accessModeSufficient 写 textual 就是告诉机器:用户只用读文字,也能完整学到这门课的内容。这个字段对 AI 引擎特别有用,因为它直接回答了「把这门课引用给一个不便看视频的用户,行不行」。W3C 在 EPUB 3 的可访问性规范里对这对概念的区分有详细说明,schema.org 直接沿用了这套语义。
conditionsOfAccess 是纯文本字段,没有枚举词表,你想写什么都行。我们最终统一成三个短语:无需注册、需免费注册、需付费订阅。写了它的页面,AI 引擎在生成「哪些课程免费」这类回答时才有依据——不写,引擎只能猜,猜错一次可能就把你从引用列表里踢掉了。
一门视频课配齐后的声明大概长这样(JSON 标准不支持注释,下面 // 行是讲解标注,实际部署时删掉):
{
// 上下文与实体类型,课程资源用 LearningResource
"@context": "https://schema.org",
"@type": "LearningResource",
"name": "SQL 入门到实战",
// 内容形态:视频课含音频轨道和画面
"accessMode": ["auditory", "visual"],
// 全文讲义齐备,只读文字也能学完整门课
"accessModeSufficient": [{ "itemListElement": ["textual"] }],
// 消费门槛用固定短语,不写长句
"conditionsOfAccess": "需免费注册",
// 与 conditionsOfAccess 口径必须一致
"isAccessibleForFree": false
}
AI 引擎怎么消化这些字段:机制剖析
这一节拆开讲原理。AI 引擎(不管是回答式搜索还是 RAG 管道)处理一个课程页,大致分四步,结构化数据在其中两步起作用。
flowchart TD
A[爬虫抓取课程页] --> B{发现 JSON-LD 结构化数据?}
B -- 是 --> C[解析 CreativeWork/Course 实体]
B -- 否 --> D[只能依赖正文文本推断]
C --> E[读取 accessMode 判断内容形态]
C --> F[读取 conditionsOfAccess 判断引用边界]
E --> G[生成索引: 内容可索引表示]
F --> G
G --> H[用户提问时按形态与边界匹配引用]
D --> I[形态不确定 边界未知]
I --> J[引用置信度低 大概率不引用]
第一步是抓取与实体识别。JSON-LD 是 AI 引擎解析成本最低的格式,一段 script 标签就能把页面内容映射成知识图谱里的实体。第二步是形态与边界判断,accessMode 决定这门课在索引里以什么形态出现——audio 类内容在「播客推荐」「听书」场景才有机会被引用,textual 类内容则在几乎所有知识问答场景可用。第三步是引用决策,conditionsOfAccess 在这里起过滤作用:用户问「有哪些免费的 React 课程」,没写条件声明的页面,引擎不敢标成免费,宁可少引。
第四步是生成引用。我们后来看了一些引用了自家课程的 AI 回答快照,发现引擎经常复述结构化数据里的原话,比如「该课程需免费注册后观看」——这句话就是我们 conditionsOfAccess 里写的原文。结构化数据不只是给机器分类用的,它实际上成了生成内容的素材来源之一。
关键结论:AI 引擎对「拿不准的内容形态和获取边界」的处理方式是保守回避,而不是猜一个。 这和传统搜索引擎用排序兜底不确定性完全不同,也是 GEO 和传统 SEO 在工程上分叉的地方。
45 天改造记录
改造分四个阶段,用 gantt 图还原当时的项目计划,实际执行比计划拖了三天。
gantt
title 课程页可访问性元数据 45 天改造计划
dateFormat YYYY-MM-DD
section 盘点与设计
摸底现有结构化数据覆盖 :done, t1, 2026-08-17, 7d
定义三字段取值规范 :done, t2, 2026-08-24, 5d
section 开发与填充
模板层注入 JSON-LD 字段 :done, t3, 2026-08-31, 10d
340 门课按类目分批补录 :active, t4, 2026-09-10, 14d
section 验证与观测
富媒体测试工具校验与观测期 :t5, 2026-09-24, 21d
第一阶段摸底花了一周。我们写了个脚本扫描全站课程页,结果 340 门课里只有 61 门有 JSON-LD,其中有 accessMode 的一门都没有。条件声明的覆盖情况更差,所有页面只有付费墙的 HTML 结构能暗示内容是不是收费的,机器想读懂得先解析支付组件。
第二阶段定规范是最容易扯皮的。内容团队想给所有课都标 textual,理由是「都有讲义」。但实际盘点下来,有 40 多门课的文字稿只覆盖了前两节,属于引流性质——这种标 textual 就是在骗机器,后面观测期也验证了会被识别出来。最后定下的规则是:accessModeSufficient 的 textual 只有全文讲义齐备才写;conditionsOfAccess 用固定的三个短语,不许自由发挥。
第三阶段是模板层注入。我们的课程详情页是服务端渲染,把字段的生成逻辑放在模板数据组装层,核心代码长这样:
依赖:Python 3.11、Jinja2 3.x;环境:Ubuntu 22.04 / 课程站渲染服务
# -*- coding: utf-8 -*-
# 课程详情页 JSON-LD 组装模块
from jinja2 import Template
# 消费条件到 conditionsOfAccess 文案的映射
# 统一三个短语,禁止内容团队自由发挥
ACCESS_TEXT = {
"open": "无需注册", # 游客可直接消费
"register": "需免费注册", # 登录后可看
"paid": "需付费订阅", # 付费会员内容
}
# 判断讲义是否覆盖全部章节
# 只有全文齐备才允许声明 textual 充分
def lecture_complete(course) -> bool:
chapters = course["chapters"]
# 每一章都必须有非空讲义正文
return all(c.get("lecture_body") for c in chapters)
# 组装 accessMode 数组
# 视频+字幕+讲义的课最多可声明三种模式
def build_access_mode(course) -> list:
modes = []
if course["has_video"]:
modes.append("auditory") # 视频含音频轨道
modes.append("visual")
if lecture_complete(course):
modes.append("textual") # 全文讲义齐备才加
return modes
# 拼出整个 CreativeWork JSON-LD
def build_jsonld(course) -> str:
is_free = course["pay_type"] != "paid"
# isAccessibleForFree 与 conditionsOfAccess 必须口径一致
tpl = Template(
'{"@context":"https://schema.org",'
'"@type":"LearningResource",'
'"name":"{{ name }}",'
'"accessMode":{{ modes }},'
'"accessModeSufficient":{{ sufficient }},'
'"conditionsOfAccess":"{{ cond }}",'
'"isAccessibleForFree":{{ free }}}'
)
return tpl.render(
name=course["title"],
# accessMode 用 JSON 数组序列化后内联
modes=str(build_access_mode(course)).replace("'", '"'),
# 充分条件只在图文完整时声明 textual
sufficient='["textual"]' if lecture_complete(course)
else '["auditory","visual"]',
# 消费条件取统一映射表
cond=ACCESS_TEXT[course["pay_type"]],
# 布尔值转小写字面量
free=str(is_free).lower(),
)
第四阶段是校验和观测。Google 的富媒体测试工具会报 accessModeSufficient 里手写 ItemList 结构的告警,我们对每类模板都跑了一遍。观测期从 9 月底开始,前两周几乎没动静,第三周开始在一些长尾问题上看到引用,比如「适合听的视频课」「不用注册就能学的 SQL 入门」。这类问题以前我们一次都没进过引用列表,因为引擎根本不知道哪些课符合条件。
三个字段的组合细节与踩坑
改到一半的时候踩了不少坑,挑三个有代表性的说。
第一个坑是 accessMode 和 accessModeSufficient 的语义混用。有同事在 accessModeSufficient 里写了个数组 ["textual","auditory"],想表达「文字或音频都行」。schema.org 里这个属性的值是 ItemList,列表内是「合取」语义——按 W3C 文档的说法,列表整体描述一种充分方式,不是几个备选。想表达「或」,应该写两个独立的 ItemList。我们后来在文档里把这个区别画了张表,防止再写错:
| 想表达的意思 | 错误写法 | 正确写法 |
|---|---|---|
| 读文字就能学完整门课 | accessModeSufficient: "textual" | 单个 ItemList,只含 textual |
| 文字或音频任一即可 | 一个列表写 textual、auditory | 两个 ItemList,各含一项 |
| 视频必须看画面才能学 | accessMode 写 visual,充分条件不写 textual | accessModeSufficient 声明 visual+auditory |
第二个坑是 conditionsOfAccess 和 isAccessibleForFree 口径不一致。早期有十几门课,页面标了 isAccessibleForFree 为 true,conditionsOfAccess 却写着需付费订阅——因为试看章节免费,整课收费。这两个字段打架的时候,AI 引擎抓到的是矛盾信号,比不写还糟。结论是两个字段必须描述同一层语义:isAccessibleForFree 说「内容本身收不收费」,conditionsOfAccess 说「消费它要跨过什么门槛」,试看模式的课统一标 false 加需付费订阅,另开 freePreview 字段描述试看范围。
第三个坑是渲染时机。我们有两套详情页模板,老模板的 JSON-LD 是前端 JS 补渲染的,爬虫拿到源码时字段是空的。改成服务端直出之后,抓取日志里结构化数据的解析成功率从 74% 涨到了接近全覆盖。AI 引擎的爬虫对 JS 渲染的容忍度比想象中低,这个细节很多团队会漏。
sequenceDiagram
participant U as 用户
participant E as AI 引擎
participant S as 课程站
U->>E: 提问(有哪些需付费的数据分析课)
E->>S: 抓取课程页源码
S-->>E: 返回含 JSON-LD 的 HTML(服务端直出)
E->>E: 解析 conditionsOfAccess=需付费订阅
E->>E: 匹配 accessModeSufficient 判断形态
E-->>U: 生成回答并附课程引用链接
改造后的变化与几个误区
观测到 10 月初,数据是这样的,口径是「我们站点的实测」,样本就这 340 门课,别外推:
| 指标 | 改造前(8 月中) | 改造后(10 月初) |
|---|---|---|
| 有完整 JSON-LD 的课程页 | 61 / 340 | 340 / 340 |
| 带三字段声明的页面 | 0 | 332(8 门下架中) |
| AI 引擎周均引用次数 | 约 14 | 约 47 |
| 条件类长尾问题的引用命中 | 0 次 | 11 次 |
最后说三个容易走偏的理解,都来自我们内部讨论时真出现过的争论。
误区一是把这套字段当成无障碍合规清单。accessMode 系列确实源自 W3C 的可访问性工作,但它的机器可读性同样重要——写它是给屏幕阅读器和 AI 引擎两拨读者看的,别只当合规材料填了就完事。
误区二是觉得字段越多越好。conditionsOfAccess 没有词表,有同事想写长句描述会员规则,引擎引用时经常截取得乱七八糟。短语够用,机器吃得下,人也读得懂。
误区三是把 GEO 理解成一次性改造。AI 引擎的解析能力在更新,schema.org 的词表也在扩,这套声明要跟着内容变化维护——新增一门纯音频课就得补 auditory 声明,不然等于新内容对 AI 引擎不透明。趋势上看,内容形态与获取边界的机器声明会从「加分项」变成课程站的基础设施,早补早受益。
你在课程站或知识内容上做过 accessMode 之类的声明吗?被 AI 引擎引用的效果怎么样,评论区聊聊踩过的坑。
参考与延伸
- schema.org CreativeWork 属性定义(含 accessMode、accessModeSufficient、conditionsOfAccess):https://schema.org/CreativeWork
- W3C EPUB 3 可访问性规范中对 accessMode 与 accessModeSufficient 的语义说明:https://www.w3.org/TR/epub-a11y-11/
- schema.org 资格速查表与结构化数据通用指南:https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
- Google 搜索中心结构化数据标记验证工具(富媒体测试):https://search.google.com/test/rich-results
关键词:GEO、accessMode、accessModeSufficient、conditionsOfAccess、schema.org、课程被AI推荐、AI搜索引用