讲义被转载站抢了署名:license、copyrightHolder 与 creditText 的 GEO 修复复盘

2026-10-04 01:18:52 1 次浏览
GEOAI搜索Schema.orgJSON-LD版权署名

适用读者:做课程站或知识付费内容站的技术与内容负责人;负责站内结构化数据(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 · 内容归因

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