讲义被转载站抢了署名:license、copyrightHolder 与 creditText 的 GEO 修复复盘
适用读者:做课程站或知识付费内容站的技术与内容负责人;负责站内结构化数据(JSON-LD / Schema.org)落地的开发;发现自家讲义被 AI 搜索引用时出处被安到转载站头上的站长。需要你至少能手改页面模板、跑得动 Python 脚本。
事情是怎么暴露的
我们是做后端技能类课程的,站上有两百多篇免费讲义,原创为主,页面结构三年没大动过。八月中旬有个学员在群里贴了一张截图:他在豆包里问「HNSW 和磁盘索引的取舍」,回答里的核心段落和我们一篇讲义几乎逐句一致,但引用出处挂的是某个转载站的域名。那家站连作者名都改了,写成了他们编辑的名字。

我第一反应是去投诉,投诉了两次,对方响应很慢,而且删一篇他们还有几十篇。真正让我坐下来查问题的是另一件事:同样的问题去问 DeepSeek 和 Google 的 AI Overview,出处归给转载站的比例相当高。也就是说这不是单家引擎的 bug,而是我们的页面给不出足够的署名信号,让生成式引擎在归因的时候只能靠域名权重猜。
这里说的 GEO,指的是生成式引擎优化(Generative Engine Optimization, GEO):让内容在 AI 搜索的生成式回答里被正确引用、正确署名。它和传统 SEO 最大的差别在于,传统 SEO 抢的是排名位置,GEO 抢的是「回答里这一段算谁的」。
我们花了两周定位、一周修复、四十五天观测,把讲义段落被 AI 引用时带出本站的归属比例从两成左右拉回到七成上下。这篇文章把整个修复过程、字段选择和踩过的坑摊开写,数据是我自己抽样统计的,口径后面会讲清楚。
归因机制剖析:生成式引擎凭什么决定出处是谁
修复之前必须搞清楚一件事:AI 搜索的出处归因不是查重,不是「谁先发布算谁的」。以 AI Overview 这类产品为例,它的链路大致是这样:
flowchart TD
A[引擎抓取课程讲义页] --> B[抽取正文与结构化数据]
B --> C{页面有没有署名信号}
C -->|license、copyrightHolder、creditText 齐全| D[把出处归给原站并带出署名]
C -->|署名信号缺失| E[退化为按域名权重与外链数量归因]
E --> F[转载站权重更高时被当成原始出处]
关键在第三个节点。当页面缺署名信号时,引擎退化到类 PageRank 的启发式判断:谁的域名权重高、被外链引用多、页面更新频繁,谁就更像「原始出处」。转载站往往靠批量搬运把外链和收录量做得很肥,这种退化归因几乎必然输给转载站。我们的讲义页当时只有一个最简版的 JSON-LD,author 字段填的是公司名,没有 license、没有 copyrightHolder、没有 creditText,页面底部也没有可见的作者署名区块——引擎想替我们说话,都找不到词。
creditText 的设计初衷就是解决这个问题的。它是 Schema.org 专门给「片段被复用时应当附带的署名文本」准备的字段,生成式引擎做引用拼接时可以直接把它带进回答。copyrightHolder 回答「版权算谁的」,license 回答「别人能怎么用」。三个字段加可见署名区块,构成一组能被机器交叉验证的署名信号。
结论先放这里:署名信号必须是结构化数据和页面可见文案互相印证的一套东西,只改 JSON-LD 不改可见区块,效果会打折;只改可见文案不给结构化数据,引擎甚至不一定能抽出来。
修复方案:署名三件套怎么落地
动手之前我们先盘了家底。用站内检索加搜索引擎的 intitle 加片段比对,把被整段或大段搬运的讲义列了个清单,最后确认 43 篇,全部集中在流量最高的后端和数据库分类。清单里顺手记了两列:一篇讲义被几家搬、我们页面当时的 JSON-LD 有哪些字段。后一列的结论很一致——200 多篇讲义页全是最简模板,author 写的是公司名,署名相关字段为零。这个盘点的意义在于把修复范围从「被抄袭了怎么办」收敛成一个明确的工程任务:给 43 篇高价值讲义优先补齐署名信号,其余长尾页面排第二批。
字段选择与常见误用
动手之前我们把三个字段的语义对着 Schema.org 的定义捋了一遍,也踩过几个想当然的坑:
| 字段 | 作用 | 我们犯过的错 |
|---|---|---|
| license | 声明内容的授权协议,给复用行为定规则 | 一开始填了第三方协议托管页 URL,后来改回本站授权页 |
| copyrightHolder | 声明版权主体,法务口径上「内容算谁的」 | 最初和 author 混用,写了讲师个人,法务要求改成公司主体 |
| creditText | 内容被引用时应带出的署名原文 | 写成了营销 slogan,太长且不含作者名,等于白填 |
三个字段的分工要想清楚:license 面向「允许怎么用」,copyrightHolder 面向「版权归属」,creditText 面向「引用时怎么说」。归因场景里 creditText 权重最高,因为它是三者之中直接给引擎喂「署名成句」的字段。文案要短、要含作者名和站点名、不要口号。
JSON-LD 模板
讲义页统一用 Article 类型,署名三件套直接挂在顶层。下面是我们最终定稿的模板(真实域名做了替换):
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "向量数据库选型:从 HNSW 到磁盘索引",
"url": "https://example-course.com/lecture/vector-db-101",
"datePublished": "2026-08-14",
"author": {
"@type": "Person",
"name": "林一舟",
"url": "https://example-course.com/teachers/linyizhou"
},
"license": "https://example-course.com/license/cc-by-nc-nd",
"copyrightHolder": {
"@type": "Organization",
"name": "示例课程站"
},
"creditText": "向量数据库选型:从 HNSW 到磁盘索引 — 示例课程站讲义,作者林一舟",
"copyrightYear": "2026"
}
注意 creditText 里那句署名是逐字设计过的:讲义标题、站点名、作者名都在,控制在三十个字以内,去掉了所有形容词。早期版本写的是「精品原创讲义,请尊重版权」,观测期内完全没被引擎带出来过。
可见署名区块
光有结构化数据不够,页面底部要有一块人眼可见、引擎可抽取的署名区,而且文案要和 creditText 对得上:
<!-- 讲义页底部可见署名区块,文案与 creditText 保持逐字一致 -->
<!-- 区块只用纯文本加一个授权链接,别塞头像、按钮之类的东西 -->
<footer class="lecture-credit">
<!-- 可见署名是引擎做交叉比对的锚点,别写成营销文案 -->
<p>本文讲义由 示例课程站 出品,作者 林一舟,转载请遵循 <a href="/license/cc-by-nc-nd">CC BY-NC-ND 4.0</a> 授权协议。</p>
<!-- 授权链接用本站相对路径,指向站内授权页而不是第三方托管页 -->
<!-- 站点名与作者名之间用空格分隔,方便引擎做分词匹配 -->
</footer>
这块区块上线前有个争论:要不要放讲师头像和「举报搬运」按钮。最后都没放——头像会稀释文本抽取,举报按钮是给版权投诉用的,和署名信号是两码事。保持区块纯粹,只做署名陈述。
校验脚本:把署名当成 CI 的一道断言
两百多篇讲义手工改不现实,我们写了个脚本批量生成 JSON-LD 并做断言校验。环境是 Python 3.11+,只用标准库,不需要装任何包:
# 依赖与环境:Python 3.11+,仅标准库,无需 pip 安装任何包
# 用途:给课程讲义页的 JSON-LD 注入署名三件套,并做断言校验
import json
from pathlib import Path
from datetime import datetime, timezone
# 脚本不依赖任何第三方库,服务器上裸跑也行
# 讲义页核心信息,实际项目里从 CMS 的文章元数据读出来
LECTURE = {
# headline 与页面 H1 保持一致,引擎会做正文比对
"headline": "向量数据库选型:从 HNSW 到磁盘索引",
# url 必须是收录后的正式地址,测试环境地址不要混进来
"url": "https://example-course.com/lecture/vector-db-101",
"author_name": "林一舟",
"author_url": "https://example-course.com/teachers/linyizhou",
"publisher": "示例课程站",
}
def build_lecture_jsonld(info: dict) -> dict:
# 组装 Article 类型的 JSON-LD,重点是署名三件套
data = {
"@context": "https://schema.org",
"@type": "Article",
# headline 与页面 H1 一致,别在这里写 SEO 变体标题
"headline": info["headline"],
"url": info["url"],
# datePublished 带当天日期,引擎解析时不容易歧义
"datePublished": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"author": {
"@type": "Person",
# 类型写 Person 而非 Organization,讲义署名到讲师个人
"name": info["author_name"],
"url": info["author_url"],
},
# license 指向本站授权协议页,给复用行为定规则
"license": "https://example-course.com/license/cc-by-nc-nd",
# copyrightHolder 是版权主体,法务口径写站点主体而非个人
"copyrightHolder": {
"@type": "Organization",
"name": info["publisher"],
},
# creditText 是引用拼接时应当带出的署名原文,必须含作者名
# 句式用「标题 — 站点名 讲义,作者 X」,逗号用全角方便中文分词
"creditText": f"{info['headline']} — {info['publisher']} 讲义,作者 {info['author_name']}",
}
# copyrightYear 从 datePublished 截取,避免两处年份对不上
data["copyrightYear"] = data["datePublished"][:4]
# 返回前不做深拷贝,调用方拿到后别再改里面的值
return data
def validate_sign_fields(data: dict) -> None:
# 三件套缺一个都不行,断言失败直接暴露给 CI
assert data["license"].startswith("https://"), "license 必须是完整 URL"
# 第三方协议托管页地址在这里会被拦下,历史上我们栽过这个坑
assert "schema.org" not in data["license"], "license 别指向 schema.org 示例地址"
assert data["copyrightHolder"]["name"], "copyrightHolder.name 不能为空"
# creditText 控制长度,太长引擎拼接时会被截断
assert 0 < len(data["creditText"]) <= 110, "creditText 必填且不超过 110 字符"
# 可见署名与结构化数据要一致,作者名必须出现在 creditText 里
assert data["author"]["name"] in data["creditText"], "creditText 里必须包含作者名"
if __name__ == "__main__":
# 生成并通过校验后写入静态目录,供页面模板 include
ld = build_lecture_jsonld(LECTURE)
validate_sign_fields(ld)
out = Path("dist/lecture-jsonld/vector-db-101.json")
# 目录不存在时自动创建,首次接入不需要手工准备
out.parent.mkdir(parents=True, exist_ok=True)
# ensure_ascii=False 保证中文署名原样落盘
out.write_text(json.dumps(ld, ensure_ascii=False, indent=2), encoding="utf-8")
# 打印 creditText 方便人工核对署名文案
print("creditText:", ld["creditText"])
脚本跑通后挂进发布流水线:任何讲义页的 JSON-LD 缺三件套之一,或者 creditText 和可见署名区块文案不一致,构建直接失败。这一步把「署名」从一次性整改变成了长期约束,后面新讲义不可能再裸奔上线。
修复动作整体是按这条线推进的:
flowchart LR
A[盘点被搬运的 43 篇讲义] --> B[批量补齐 JSON-LD 三件套]
B --> C[页面底部加可见署名区块]
C --> D[断言脚本进发布流水线]
D --> E[每周抽样 30 问做归属统计]
E --> F{归属比例是否回升}
F -->|回升| G[继续覆盖长尾讲义页]
F -->|不回升| H[回查渲染后 DOM 而非源码]
修复前后 45 天的归属数据
统计口径先交代清楚,免得误导:从修复上线那天起,每周固定用 30 个讲义相关的技术提问(每周换一批新问题,避免引擎缓存污染)分别去问 AI Overview、豆包、DeepSeek,人工核对回答里命中讲义原文段落时,引用出处带没带本站域名。45 天共六轮,180 问 × 3 引擎。
| 引擎 | 修复前带出本站比例 | 第 6 轮带出本站比例 | 变化 |
|---|---|---|---|
| AI Overview | 约 18% | 约 71% | 明显回升 |
| 豆包 | 约 22% | 约 69% | 明显回升 |
| DeepSeek | 约 19% | 约 73% | 明显回升 |
几个有意思的细节。第一,回升不是匀速的:前两周几乎没变化,第三周开始爬坡,我们怀疑是引擎侧的抓取和索引刷新周期在起作用,所以修复后至少要给四周观测期再下结论。第二,被引用时带出 creditText 原句的情况集中在 DeepSeek 上,AI Overview 更多是把站点名挂在出处链接上,creditText 文案本身出现得少,但它大概率参与了归因判断。第三,转载站并没有因此消失,它们的内容还在被引用,只是当问题命中的是我们讲义原文时,出处开始归给我们了。
这段数据是单站点的小样本一手观测,不构成任何引擎行为的统计结论,但趋势方向和 Schema.org 对这三个字段的语义定义是对得上的。
还没解决的尾巴
复盘不能只写好的。三个遗留问题记录在此:一是那家转载站还在,投诉流程依然慢,结构化数据救不了「被抄袭」本身,只能救「被冒名」;二是站内还有一批 2023 年前的老讲义,模板已经换过两代,批量补三件套时要处理旧模板的字段兼容,进度刚过半;三是 creditText 的文案 AB 还没做,「示例课程站讲义,作者林一舟」和「示例课程站的林一舟」哪种更容易被引擎原样带出,样本量还不足以下判断。有做过类似归因修复的同学,欢迎在评论区交换观测数据。
参考与延伸
- Schema.org license 属性定义:https://schema.org/license
- Schema.org copyrightHolder 属性定义:https://schema.org/copyrightHolder
- Schema.org creditText 属性定义:https://schema.org/creditText
- Google 结构化数据入门文档:https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
GEO · AI搜索 · Schema.org · JSON-LD · 版权署名 · creditText · 内容归因