用 Claude 生成能通过 Rich Results Test 的 JSON-LD
目录
上个季度,我把一篇菜谱文章贴给 Claude,让它生成 Recipe(食谱)结构化数据。它给我返回了 60 行 JSON-LD,看起来天衣无缝:缩进规整、JSON 合法、@type 全对。我把它塞进 Google 的 Rich Results Test(富媒体结果测试),拿到的是这个:
错误:缺少字段 "recipeIngredient"
错误:字段 "recipeYield" 的值无效
警告:应至少指定 "image" 或 "video" 之一
警告:缺少字段 "author"
警告:缺少字段 "datePublished"一个页面四个问题。Claude 自己造了一个 ingredients 字段(标准 schema 里叫 recipeIngredient),把 "4 servings"(字符串)传给了一个要 integer(整数)的字段,然后因为页面 HTML 没有把作者和发布日期标得很显眼,就默默把它们丢掉了。这段 JSON 在语法上没毛病,在语义上烂透。
这就是陷阱。大语言模型能产出可以解析的 JSON,这没什么难度;但产出能通过 Google 结构化数据校验器的 JSON,是另一个问题——这个问题的本质是要让输出同时对得上两样东西:精确的 Schema.org 规范,和页面上真实存在的内容。大多数 prompt 这两样都没给,产出来的就是那种看起来很像样、第一次 Rich Results Test 就挂掉的"plausible schema"。
下面是我现在用的工作流:两段 prompt,一个校验回路,加上一份在跑校验之前可以肉眼 grep 的常见错误清单。
在 Google 眼里,"合法"到底是哪一层
值得停下来澄清,因为"合法 JSON-LD"被混用成了三个不同的概念。
- JSON 语法合法。 大括号闭合,任何 JSON 解析器都不会炸。这层最简单,LLM 几乎每次都过。
- Schema.org 规范合法。
@type存在、属性名和 Schema.org 定义对得上、值的类型正确(字符串还是整数、是 URL 还是 ISO 8601 日期)。在 json-ld.org 的 playground(在线测试)里跑就能确认这一层。 - 能拿到 rich results(富媒体结果)。 Google 的 Rich Results Test 在上面又叠了一层自己的要求——有些 Schema.org 合法的属性 Google 直接忽略,有些 Schema.org 里可选的属性 Google 又把它列成必填。这一层决定的是:你的星级评分、菜谱卡、FAQ 折叠面板、HowTo(操作步骤)轮播,到底会不会在 SERP(搜索结果页)上显示出来。
客户说"schema 不工作"的时候,他指的几乎一定是第三层,不是第一层或第二层。Claude 闭着一只眼就能搞定第一层。第二层需要 prompt 写对。第三层需要 prompt 写对而且和校验器之间有一个紧凑的回路。下面整篇要讲的,就是怎么不绕 14 圈就直接到第三层。
占 90% 错误的两种失败模式
在贴 prompt 之前,先把敌人点名。在我用 Claude 跑过的大约 200 次 schema 生成里(Recipe、Article、Product、FAQPage、HowTo、LocalBusiness、BreadcrumbList——常见的对 SERP 有用的那批),几乎所有失败都落在这两类里。
失败模式 1:自造属性名。 Claude 训练数据里见过成千上万的 JSON-LD 例子,而其中并非全都用了 Schema.org 的官方属性名。所以它会自信地写 ingredients 而不是 recipeIngredient、cookingTime 而不是 cookTime、rating 而不是 aggregateRating、imageUrl 而不是 image。看着没错。其实错了。 校验器要么默默忽略你那个字段,要么直接抛"缺少必填字段"——因为它本来要找的那个字段不在了。
失败模式 2:属性没有落在页面上。 模型会凭空造出一个 author、一个 datePublished、一个 reviewCount 是 47 的 aggregateRating,因为大多数页面会有这些东西,它在做模式补全。有时候页面真的有那个数据,只是模型看不到;有时候页面根本没有,模型就编了一个。这两种都坏,但编造那种更坏——因为它会让你吃 Google 的一记"结构化数据与可见内容不符"的人工处罚。
这两种失败模式有同一种解法:在同一个 prompt 里,用真实的规范 + 真实的页面去约束模型。
Prompt 1:先抽取,再格式化
不要让 Claude 一次性写 JSON-LD。拆开。第一次调用从页面里抽取数据点,第二次调用把数据点格式化成 JSON-LD。这听起来很冗余。这是我做过的单一改动里收益最大的一个——把第一次就过 Rich Results Test 的概率从大约 40% 提到了大约 85%。
下面是抽取 prompt 的例子,用的是 Article(文章) schema。换其他 schema 类型时改字段清单。
text你的任务是从一个 HTML 页面里抽取数据点,这些数据点之后会被标记成
Schema.org Article。下面是页面渲染出来的 HTML 和可见文本。
只抽取下列字段。对每一个字段,要么返回:
- 页面上原原本本的那个值,或者
- 字符串 "NOT_FOUND",如果页面没有这个数据。
不要推断、不要猜、不要用"合理默认值"来补。如果页面没写作者名,
返回 "NOT_FOUND" —— 不要写 "Editorial Team"(编辑部)。
字段:
- headline(必须 ≤ 110 个字符)
- description
- author.name
- author.url(作者的个人页 URL,如果有链接)
- datePublished(ISO 8601,例如 "2025-04-02T09:00:00+08:00")
- dateModified(ISO 8601,或 NOT_FOUND)
- image(文章主图的 URL,必须是绝对 URL)
- publisher.name
- publisher.logo(绝对 URL)
- mainEntityOfPage(这篇文章的 canonical URL,即权威 URL)
输出:一个 JSON 对象,key 严格用上面这些。不要任何散文。
HTML/文本:
[在这里贴]这个 prompt 里有三件事是普通"给这个页面写 JSON-LD"做不到的。
NOT_FOUND 这条规则是干掉失败模式 2 的关键。它明确允许模型承认自己没找到数据,而这是你在校验器告诉你之前知道"你的页面缺一个必填字段"的唯一办法。
"页面上原原本本的那个值"这条,干掉的是次要失败模式——值被改写。headline 应该和可见的 H1 一致。如果 Claude 为了"让标题更清晰"重写了一版,结构化数据就和页面错位了——这恰好是 Google 已知会扣分的信号。
headline 上的字符数限制是 Google 自己加的(Article 类型只有在 headline ≤ 110 个字符时才会被发 rich results)。把它写到抽取阶段去截,而不是格式化阶段——抽取阶段你看得见、能复核,格式化阶段一不小心就漏过去了。
跑完这个 prompt,你眼睛过一遍 JSON,把那些 NOT_FOUND 要么手工补,要么回头去修页面本身。然后你才去跑 Prompt 2。
Prompt 2:把规范贴进 prompt 里再格式化
第二次调用是格式化。这里的窍门是:把相关的 Schema.org 字段说明直接贴进 prompt,这样模型就不靠"训练数据里哪个属性名是官方的"那种记忆来工作。
text请把下面的数据格式化成 Schema.org Article JSON-LD,遵循 Google 的
rich results 要求,文档地址是:
https://developers.google.com/search/docs/appearance/structured-data/article
严格使用下列属性名(这些是 Schema.org 的官方名;不要用 "imageUrl"
代替 "image",其他类似情况同理):
Google 要求必填的:
- @context: "https://schema.org"
- @type: "Article"(或更具体的 "NewsArticle" / "BlogPosting")
- headline(字符串,最多 110 字符)
- image(URL,或 URL 数组 —— 优先 16:9、4:3、1:1)
- datePublished(ISO 8601,带时区)
- author(对象,@type 为 "Person" 或 "Organization",带 "name";
可选 "url")
推荐填的:
- dateModified(ISO 8601)
- description
- publisher(对象,@type 为 "Organization",带 "name" 和 "logo";
logo 必须是 ImageObject 类型,内部有 "url")
- mainEntityOfPage(URL,文章的 canonical)
规则:
- 如果某字段输入值是 "NOT_FOUND",**整个字段从输出中删除**。
不要保留这个字段然后写空字符串或 null。
- 把 JSON-LD 包在