schema-markup-at-scale-pseo.html
< BACK 长服务器机房走廊,闪烁的LED面板,阴郁的蓝琥珀色35mm胶片美学

大规模Schema标记:91,000个页面的JSON-LD

2021 年,一个旅游客户向 Seahawk 交付了一份迁移简报,让我有点不安。九万一千个目的地和酒店页面。每一个都需要有效、具体、经过测试的架构标记——而不是大多数插件懒洋洋地应用的通用 WebPage 类型。这个客户已经尝试过两个"自动架构"WordPress 插件。两者都生成了在技术上有效的 JSON-LD,但在实际意义上完全无用——通用名称、没有嵌套实体、缺少价格、评价聚合指向错误的对象。Google 的 Rich Results Test 礼貌地表示困惑。tested schema markup -- not the lazy one-size-fits-all WebPage type that most plugins slap on and call it a day. The client had already tried two "automatic schema" WordPress plugins. Both had produced technically valid JSON-LD that was also, in every meaningful sense, useless -- generic names, no nested entities, prices missing, review aggregates pointing at the wrong thing. Google's Rich Results Test was politely confused.

关键要点:91,000 个页面的架构是一个架构问题,而不是插件问题:在构建时从数据层生成它,并在管道中验证它。Schema for 91,000 pages is an architecture problem, not a plugin problem: generate it from the data layer at build time and validate it in the pipeline.

那个项目教会我的关于规模化schema的知识,比之前八年加起来还要多。所以这就是我真正知道的。

---

为什么"只安装插件"在规模上会崩溃

听着,我不是来贬低Yoast或Rank Math的。对于一个40页的宣传网站,它们确实没问题。但在500页左右的地方,插件生成的schema开始在自己的假设下崩溃。

核心问题是插件是围绕页面模板而不是数据模型构建的。它们读取文章标题、也许一两个自定义字段,然后构建架构块。当你的网站有 91,000 个页面跨越六种内容类型——酒店、目的地、旅游、评价、常见问题和作者档案——单一的插件配置无法表达这种多样性,除非进行大量手动覆盖工作。如果你在这种规模上进行手动覆盖,你已经失败了。page templates, not data models. They read the post title, maybe a custom field or two, and construct a schema blob. When your site has 91,000 pages across six content types -- hotels, destinations, tours, reviews, FAQs, and author profiles -- a single plugin configuration cannot express that variety without enormous manual override work. And if you're doing manual overrides at that scale, you've already lost.

关键在于:schema标记从根本上说是一个数据转换问题。你在数据库中有结构化数据;你需要将其表示为<script>标签中的JSON-LD。就这样。一旦你这样框架化,正确的架构就会清晰得多。<script>tag. That's it. The moment you frame it that way, the right architecture becomes much clearer.

我一直看到的三种失败模式

  • 在模板中硬编码的静态schema块。在产品名称改变之前还不错,改变之后你就有12,000个页面向Google撒谎。 hardcoded in templates. Fine until the product name changes, then you've got 12,000 pages lying to Google.
  • 无法处理条件逻辑的插件配置——比如仅在实际有评价时才显示 aggregateRating,或根据文章分类显示不同的 @type。 that can't handle conditional logic -- like only showing aggregateRating when there are actually reviews, or different@type per post category.
  • 批量生成的文件上传一次,永远不更新。我审计过的网站中,schema已经十八个月没有更新了。价格都错了。活动日期早就过了。 uploaded once and never updated. I've audited sites where the schema was eighteen months stale. The prices were wrong. The event dates had passed.

---

JSON-LD在规模上的实际工作原理

在讨论工具之前:快速了解一下基础知识。JSON-LD——Linked Data 的 JSON——是 Google 首选的架构格式,原因正是因为它存在于<script>块中,与你的 HTML 分离。这意味着你可以在服务器端生成它,干净地注入它,并在不触及标记的情况下更新它。当处理数万个页面时,这种分离是关键。JSON-LD -- JSON for Linked Data -- is Google's preferred schema format precisely because it lives in a<script>block, separate from your HTML. That means you can generate it server-side, inject it cleanly, and update it without touching markup. That separation is everything when you're dealing with tens of thousands of pages.

Schema.org 词汇非常庞大。大多数人只使用其中的 1%。大规模时你需要更深入——Hotel、TouristDestination、LocalBusiness、Review、AggregateRating、嵌套的 Offer 对象、BreadcrumbList。每种类型都有必需和推荐属性,Google 对"推荐"的解释基本上就是"如果你想要丰富结果,就是必需的"。Schema.org vocabulary is vast. Most people use about 1% of it. At scale you need to go deeper -- Hotel,TouristDestination,LocalBusiness,Review,AggregateRating, nested Offer objects,BreadcrumbList. Each type has required and recommended properties, and Google's interpretation of "recommended" is basically "required if you want the rich result."

我工作遵循的基本规则:每个页面一个主要`@type`,根据需要嵌套其他类型。不要堆砌五个@type值,希望其中一个能有效果。选择最符合的最具体的类型,然后在其中嵌套辅助类型。one primary `@type` per page, with nested types as needed.Don't stack five@type values hoping one sticks. Pick the most specific type that fits, then nest supporting types inside it.

---

我们实际使用的架构

对于旅游客户,我们最终采用了三层系统。从白板图表的角度看不够优雅,但它行之有效。

第 1 层:模板级 Schema 类(PHP)

每种内容类型都有自己的 PHP 类负责构建其架构数组。HotelSchemaBuilder、DestinationSchemaBuilder、TourSchemaBuilder——你明白的。每个类从 ACF Pro 自定义字段、适用时的 WooCommerce 数据以及一些计算值(比如从基于 CPT 的评价系统计算 aggregateRating)提取数据。HotelSchemaBuilder,DestinationSchemaBuilder,TourSchemaBuilder -- you get the idea. Each class pulled from ACF Pro custom fields, WooCommerce data where applicable, and a few computed values (like calculating aggregateRating from a CPT-based review system).

每个类的输出是一个普通的 PHP 数组。还没有 JSON。只是数据。

这很重要,因为这意味着你可以单独对数据逻辑进行单元测试,与序列化分开。我希望从这个项目的第一天就这样做了。我没有。这让我们在测试环境中花了大约两天时间调试——当时ratingValue返回的是字符串而不是浮点数,Google的验证工具默默地忽略了整个aggregateRating块。ratingValue was returning a string instead of a float and Google's validator was silently ignoring the whole aggregateRating block.

第 2 层:中央 Schema 管理器

一个SchemaManager类被挂接到wp_head中,负责:SchemaManager class, hooked into wp_head, was responsible for:

  1. 根据当前模板/文章类型确定要调用哪个构建器类
  2. 合并站点范围的实体(Organization 图谱、包含 SearchAction 的 WebSite、BreadcrumbList)Organization graph,WebSite with SearchAction,BreadcrumbList)
  3. 使用 JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE 将最终数组编码为 JSONJSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
  4. 将其包装在<script type="application/ld+json">标签中并输出<script type="application/ld+json">tag and echoing it

面包屑逻辑是最棘手的部分。目的地有三层级结构:地区 → 国家 → 城市。要让 BreadcrumbList 动态反映这个结构,而不硬编码任何内容,意味着在渲染时遍历文章祖先。如果不小心,会很慢。我们在瞬时缓存中按文章 ID 缓存面包屑数组,TTL 为 24 小时。这把开销降到了可以忽略不计的程度。BreadcrumbList to reflect that dynamically, without hardcoding anything, meant traversing post ancestors at render time. Slow, if you're not careful. We cached the breadcrumb arrays per post ID in a transient with a 24-hour TTL. That brought the overhead down to negligible.

第3层:验证和监控

生成schema是第一步。知道它何时出现问题是第二步,而大多数团队根本跳过了这一步。

我们设置了一个 Google Search Console 属性,每周查看 Rich Results 报告。但那是被动的——GSC 在 Google 爬取页面后告诉你有关错误。为了进行主动检查,我们每月在前 2,000 个页面的爬取上运行 SchemaApp。它显示了 GSC 报告隐含的属性级错误。after Google has crawled the page. For proactive checks, we ran SchemaApp on a crawl of the top 2,000 pages monthly. It surfaces property-level errors that the GSC report obscures.

另外:Google 的富媒体结果测试有 API。我们编写了一个小脚本,每晚用 50 个 URL 的随机样本调用 API,并记录任何验证失败。便宜的保险。Google's Rich Results Test has an API. We wrote a small script that would hit the API with a random sample of 50 URLs nightly and log any validation failures. Cheap insurance.

---

在不牺牲性能的前提下处理动态数据

这就是大多数规模化实现失败的地方。引用实时数据的 Schema——价格、库存、评价数——必须保持新鲜。但为 91,000 个页面的每次页面加载都重新生成 JSON-LD 并不是免费的。

我的做法,我已经在十几个大型网站上完善过:

积极缓存,智能失效。

对于酒店页面,schema blob 被存储为文章 meta——一个序列化的 JSON-LD 字符串——并且仅在以下情况下重新生成:

  • post本身被更新时
  • 为该post提交了新评论时
  • 价格自定义字段改变了(我们通过 ACF 的 save_post 操作钩取这个)save_post action for this)

其他所有情况都提供缓存的字符串。快得要命。因为失效钩子很具体,schema保持准确。

我最初犯的一个错误:我缓存了完整的 <script> 标签,包括开始和关闭元素。然后我们需要为一种内容类型更改 @context URL。必须清除每个缓存条目。现在我只缓存 JSON 字符串,在渲染时包裹它。额外写五分钟代码,省了一小时的头疼。<script>tag, including the opening and closing elements. Then we needed to change the@context URL for one content type. Had to bust every cache entry. Now I cache only the JSON string and wrap it at render time. Five minutes of extra code, saved an hour of head-scratching.

关于实时价格怎么办?

对于一天内多次变化的旅游价格,我们采取了不同的方法。基础schema被缓存,但 Offer 块在请求时新鲜生成,在序列化前合并。是的,这给每个请求增加了一点开销。但这是每个页面加载一次数据库查询,而不是十二次。可以接受的权衡。Offer block was generated fresh at request time and merged in before serialisation. Yes, it added a small overhead per request. But it was one database query per page load, not twelve. Acceptable trade-off.

---

扩展到多个网站:Seahawk 的方式

Seahawk 已经建立了超过 12,000 个网站,其中许多网站都涉及模式实现。旅游客户是一个极端案例。但无论你处理的是 91,000 页还是 4,000 页,相同的架构原理都适用。

我采用的可复用模式是一个小型内部 WordPress 插件——我们称之为 seahawk-schema-core——它提供管理器/构建器脚手架,不包含任何特定于内容类型的逻辑。客户项目用他们自己的构建器类扩展它。核心 schema 逻辑没有插件依赖。不存在第三方插件更新摧毁网站整个富文本搜索结果存在的风险。seahawk-schema-core -- that provides the manager/builder scaffolding without any content-type-specific logic. Client projects extend it with their own builder classes. No plugin dependencies for the core schema logic. No risk of a third-party plugin update blowing up a site's entire rich results presence.

最后这一点比人们承认的更真实。我见过 Rank Math 更新静默破坏自定义 schema 覆盖。不是因为 Rank Math 不好——它不是——而是因为当你在大规模网站需要的级别定制输出时,你在操作插件设计处理范围之外的东西。拥有代码,拥有风险。

---

这个规模的测试:实用清单

你无法手动测试 91,000 个网址。所以你要聪明地测试。

  1. 按模板类型进行采样。每种内容类型选择 10 个 URL。测试这些。如果构建器对一个酒店页面是正确的,那么它对所有 3,000 个酒店页面都是正确的(除非有坏数据——稍后详述)。Pick 10 URLs per content type. Test those. If the builder is correct for one hotel page, it's correct for all 3,000 hotel pages (unless there's bad data -- more on that below).
  2. 特别测试边界情况。没有评论的页面。自定义字段不完整的页面。标题中包含特殊字符的页面(&、"、重音字符)。JSON序列化会清除其中很多,但并非全部。Pages with no reviews. Pages with incomplete custom fields. Pages with special characters in titles (&,", accented characters). JSON serialisation eats a lot of these, but not all of them.
  3. 用 Screaming Frog 进行完整的结构化数据爬取。Screaming Frog SEO Spider 有一个结构化数据提取模式,可以从它爬取的每个 URL 中拉取和验证 JSON-LD。导出错误,按模板类型分组,在源头修复。The Screaming Frog SEO Spider has a structured data extraction mode that'll pull and validate JSON-LD from every URL it crawls. Export the errors, group by template type, fix at the source.
  4. 监控 GSC 的增强功能标签。设置一个阈值警报——如果有效项目周环比下降超过 5%,说明有问题。在 48 小时内采取行动。Set a threshold alert -- if valid items drop by more than 5% week-over-week, something broke. Act within 48 hours.
  5. 每次部署后进行现场检查。即使 schema 代码没有改变。数据库迁移、插件更新、主题更改——它们中的任何一个都可能引入导致 schema 输出损坏的上游数据问题。Even if the schema code didn't change. Database migrations, plugin updates, theme changes -- any of them can introduce upstream data issues that corrupt schema output.

坏数据是隐形杀手

这个旅游网站有一个跨越三个国家的十二人内容团队。一些目的地页面在描述字段中有格式不正确的 HTML——可能是从 Word 粘贴的。当该字段输入到 schema description 属性时,JSON 在技术上是有效的,但描述包含 &nbsp; 实体和杂散的 <span> 标签。Google 忽略了该属性。我们在每个构建器类中添加了一个清理步骤,在值进入 schema 数组之前,删除标签并解码 HTML 实体。永久解决了。description field -- pasted from Word, presumably. When that field fed into the schema description property, the JSON was technically valid but the description included&nbsp;entities and stray<span>tags. Google ignored the property. We added a sanitisation step in every builder class that strips tags and decodes HTML entities before the value hits the schema array. Solved it permanently.

---

实体图:不要忽视它

将平庸的 schema 工作与真正优秀的技术 SEO 分开的一件事是实体图——具体来说,是应该出现在每个页面上并将所有内容链接在一起的网站级 Organization 和 WebSite 实体。Organization and WebSite entities that should appear on every page and link everything together.

大多数网站都有这些,但做得很差。名称、URL,也许还有一个logo。完整的Organization类型支持指向你的Wikidata条目、社交媒体资料和其他权威来源的sameAs链接。这种交叉链接是Google如何建立信心,确认你的Organization实体在其Knowledge Graph中与出现在你页面schema中的实体是同一个的方式。Organization type supports sameAs links to your Wikidata entry, social profiles, and other authoritative sources. That cross-linking is how Google builds confidence that your Organization entity in its Knowledge Graph is the same entity appearing in your page schema.

对于这个旅游客户,我们构建了Organization块,包含:Organization block with:

  • sameAs指向他们的Crunchbase资料、LinkedIn页面和他们拥有的Wikipedia条目 pointing to their Crunchbase profile, LinkedIn page, and a Wikipedia stub they had
  • 包含结构化电话和部门信息的contactPoint with structured phone and department info
  • foundingDate 和 numberOfEmployees(粗略范围——这本来就是公开信息) and numberOfEmployees(rough range -- this is public info anyway)

它一夜之间改变了排名吗?没有。Schema几乎从不单独产生这样的效果。但它是基础设施。你构建一次,做得正确,随着时间推移就会产生复利效应。

---

FAQ

在这样的规模上实现schema需要多长时间?

对于那个91000页的旅游网站,完整实现——架构、生成器类、缓存层、测试、GSC监控设置——用了大约六周时间,两个开发人员。听起来很长。但其中一半的时间用在审计现有数据质量上,而不是编写schema代码。如果你的数据是干净的,你可以更快地推进。

我应该为大型网站使用插件还是自定义构建?

对于几百页以下的任何网站,插件完全可以。Rank Math的schema模块很可靠,自定义schema区块提供了合理的灵活性。超过几千页且有多种不同内容类型的情况下,我每次都会选择自定义。控制能力值得这笔开发成本。

大规模情况下最常见的schema错误是什么?

当存在评论时缺少aggregateRating——或者当不存在时包含它。Google在这方面要求很严格。如果你的schema声称有843条评论的4.7分聚合评分,但用户登陆页面时看不到任何评论,那就是一个手动操作等着发生。在你的生成器类中使用条件逻辑是必不可少的。aggregateRating when reviews exist -- or including it when they don't. Google is strict about this. If your schema claims an aggregateRating of 4.7 from 843 reviews and a user lands on the page and sees no reviews, that's a manual action waiting to happen. Conditional logic in your builder classes is non-negotiable.

schema直接改善排名吗?

直接影响?对大多数查询类型来说可能不多。它的作用是解锁富媒体结果——星级评分、常见问题下拉菜单、评论片段、SERP中的面包屑——这些功能会显著提高点击率。那个旅游客户在完全实现后的四个月内,酒店页面的CTR增长了22%。这会影响参与度信号,而参与度确实会影响排名。所以:间接地,是的。而且影响很大。

你日常的schema工作中实际使用哪些工具?

Screaming Frog用于爬取级别的审计。Google的富媒体结果测试用于抽样检查。validator.schema.org上的Schema Markup Validator用于属性级别的验证。老实说,Schema.org文档本身——我把Hotel类型页面和其他几个页面加入了书签,我经常查阅它们。不需要花钱的订阅工具。validator.schema.org for property-level validation. And honestly, the Schema.org documentation itself -- I have the Hotel type page and a handful of others bookmarked and I refer to them constantly. No fancy subscription tool needed.

---

大规模schema是那种看起来像插件问题,但当你深入其中才意识到它实际上是一个穿着SEO外衣的软件架构问题。把数据模型设计对。缓存要聪明。验证要彻底。schema标记本身几乎是最简单的部分。

< BACK