课程页的 GEO 细节:accessMode 无障碍声明与 AI 可读性的 45 天改造记录

2026-10-01 01:16:30 1 次浏览
GEOschema.orgaccessMode结构化数据无障碍声明

正在关注生成式搜索带来的流量变化,想让课程页在 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搜索引用

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