2021年のこと、あるトラベルクライアントがSeahawkに移行案件を持ち込んできた。91,000のランドマークとホテルページ。それぞれに有効で特異的で検証済みのスキーママークアップが必要だった。ほとんどのプラグインが使うような怠け者の万能型WebPageスキーマではなく。そのクライアントはすでに2つの「自動スキーマ」WordPressプラグインを試していた。両方とも技術的には有効なJSON-LDを生成していたが、実質的には全く役に立たなかった。汎用的な名前、ネストされたエンティティなし、価格なし、レビュー集約が間違ったものを指していた。Googleのリッチリザルトテストはただ困惑していた。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.
そのプロジェクトは、過去8年間を合わせたよりも多くのことを、大規模スキーマについて教えてくれました。だからここが、私が実際に知っていることです。
---
「プラグインをインストールすればいい」が大規模では破綻する理由
聞いて、YoastやRank Mathにケンカを売るつもりはありません。40ページの企業サイトなら、本当に問題ありません。ですが500ページ前後のどこかで、プラグインが生成したスキーマは、独自の仮定の重みで軋み始めます。
根本的な問題はプラグインがページテンプレートを中心に設計されており、データモデルを中心に設計されていないことだ。投稿タイトルを読み込み、カスタムフィールドを1、2個読む程度で、スキーマのかたまりを構築する。91,000ページが6つのコンテンツタイプ――ホテル、目的地、ツアー、レビュー、FAQ、著者プロフィール――にまたがっている場合、単一のプラグイン設定ではその多様性を、膨大な手動上書き作業なしに表現することはできない。そしてこのスケールで手動上書きをやっていたら、すでに負けている。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.
ここが要点です。スキーママークアップは根本的にはデータ変換の問題です。構造化データはデータベースに存在し、それを<script>タグ内のJSON-LDで表現する必要があります。それだけです。そのように枠組みを捉えた瞬間に、正しいアーキテクチャが非常に明確になります。<script>tag. That's it. The moment you frame it that way, the right architecture becomes much clearer.
私が何度も見ている3つの失敗パターン
- テンプレートにハードコードされた静的なスキーマの塊。製品名が変わるまでは問題ないが、その時点で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
aggregateRatingwhen there are actually reviews, or different@typeper post category. - 一度アップロードされて二度と更新されないバッチ生成ファイル。スキーマが18ヶ月前のままのサイトを監査したことがある。価格が間違ってた。イベント日程は過ぎてた。 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――JSON for Linked Data――が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."
私が働く基本的なルール:ページあたり1つのプライマリ`@type`で、必要に応じてネストされたタイプを含める。5つの@typeを積み重ねて1つがくっつくことを祈るな。最も具体的でフィットするタイプを選んで、その中にサポート用タイプをネストさせろ。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.
---
実際に使用したアーキテクチャ
旅行クライアント向けに、3層システムを採用しました。ホワイトボード図のように優雅ではありませんが、機能しました。
レイヤー1:テンプレートレベルスキーマクラス(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が文字列の代わりにfloatを返してるのに気付いて、Googleのバリデータが黙ってaggregateRatingブロック全体を無視してるのに気付いた時、ステージングでのデバッグに約2日かかった。ratingValue was returning a string instead of a float and Google's validator was silently ignoring the whole aggregateRating block.
レイヤー2:中央スキーママネージャー
単一のSchemaManagerクラスは、wp_headにフックされて、次の責任があった:SchemaManager class, hooked into wp_head, was responsible for:
- 現在のテンプレート/ポストタイプに基づいて、呼び出すビルダークラスを決定する
- サイト全体のエンティティをマージ(Organization グラフ、SearchAction を備えた WebSite、BreadcrumbList)
Organizationgraph,WebSitewithSearchAction,BreadcrumbList) - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE を使用して最終配列を JSON としてエンコード
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - <script type="application/ld+json">タグでラップして出力する
<script type="application/ld+json">tag and echoing it
ブレッドクラムのロジックが最も複雑でした。目的地は3階層の構造を持っていました:地域 → 国 → 都市。BreadcrumbList をハードコーディングすることなく動的に反映させるには、レンダリング時に投稿の祖先をトラバースする必要がありました。注意しないと遅くなります。投稿 ID ごとにブレッドクラム配列をキャッシュし、24時間の TTL を設定したトランジェントに保存しました。これにより、オーバーヘッドはほぼ無視できるレベルまで低下しました。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:検証とモニタリング
スキーマの生成はステップ1です。それが壊れたときに気づくのはステップ2であり、ほとんどのチームはこれを完全にスキップしています。
Google Search Consoleのプロパティを設定し、リッチリザルトレポートを毎週確認した。だがそれは受動的だ――GSCはGoogleがページをクロール後のエラーを知らせてくれる。積極的なチェックのために、月1回、上位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.
---
動的データを扱いながらパフォーマンスを損なわない方法
ほとんどのスケール実装がここで失敗する。ライブデータを参照するスキーマ――価格、在庫、レビュー数――は新しく保つ必要がある。だが91,000ページのすべてのページロードのたびにJSON-LDを再生成するのは無料ではない。
私のアプローチだが、ここ数年で十数サイトの大規模実装を通じて洗練させてきた。
キャッシュは積極的に、無効化はスマートに。
ホテルページの場合、スキーマblob はポストメタとして保存されており——シリアル化された JSON-LD 文字列として——次の場合にのみ再生成されていました:
- ポスト自体が更新されたとき
- そのポストに新しいレビューが投稿されたとき
- 価格カスタムフィールドが変更されました(これに関しては ACF の save_post アクションにフックしました)
save_postaction for this)
その他すべてはキャッシュされた文字列を配信した。非常に高速だ。そして無効化フックが具体的だったので、スキーマは常に正確だった。
当初、私が誤ったこと:開始タグと終了タグを含む完全な <script> タグをキャッシュしていました。その後、1つのコンテンツタイプの @context URL を変更する必要がありました。すべてのキャッシュエントリを削除しなければなりませんでした。今は JSON 文字列だけをキャッシュし、レンダリング時にそれをラップしています。コードが 5分増えましたが、頭を悩ませる時間が 1時間節約できました。<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.
リアルタイム価格設定についてはどうか?
1日に何度も変更されるツアー料金の場合、別のアプローチを採用しました。基本スキーマはキャッシュされましたが、Offer ブロックはリクエスト時に新たに生成され、シリアル化前にマージされました。はい、リクエストごとにわずかなオーバーヘッドが追加されました。しかし、ページロードあたり 12回ではなく 1回のデータベースクエリです。許容できるトレードオフです。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 と呼んでいます——で、コンテンツタイプ固有のロジックなしにマネージャー/ビルダーのスカフォールディングを提供します。クライアントプロジェクトは自分たち自身のビルダークラスで拡張します。コアスキーマロジックにプラグイン依存性はありません。サードパーティプラグインのアップデートがサイト全体のリッチリザルトを吹き飛ばすリスクもありません。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 のアップデートがカスタムスキーマのオーバーライドをサイレントに壊すのを見てきました。Rank Math が悪いからではありません——そうではありません——ですが、大規模サイトが必要とするレベルで出力をカスタマイズしているとき、プラグインが処理するよう設計された範囲外で動作しているのです。コードを所有し、リスク プロファイルを所有してください。
---
この規模でのテスト:実用的なチェックリスト
91,000 URLを手動でテストすることはできません。したがって、インテリジェントにテストします。
- テンプレートタイプ別にサンプルを取得します。コンテンツタイプあたり 10 URL を選択します。それをテストします。1 つのホテルページでビルダーが正しければ、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).
- エッジケースを特に重点的にテストする。レビューがないページ。カスタムフィールドが不完全なページ。タイトルに特殊文字(&、"、アクセント文字)が含まれるページ。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. - 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.
- GSC の Enhancements タブを監視してください。有効なアイテムが週ごとに 5% 以上低下する場合はアラートを設定してください。何かが壊れています。48 時間以内に対応してください。Set a threshold alert -- if valid items drop by more than 5% week-over-week, something broke. Act within 48 hours.
- デプロイのたびにスポットチェックを行います。スキーマコードが変わらなくても。データベースマイグレーション、プラグインアップデート、テーマの変更——それらのいずれもがスキーマ出力を破損させる上流のデータ問題を引き入れる可能性があります。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.
不正なデータは静かに忍び寄る殺し屋である
トラベルサイトのコンテンツチームは 3 つの国にまたがる 12 人でした。一部のデスティネーションページの説明フィールドに不正な形式の HTML がありました——おそらく Word からペーストされたものです。そのフィールドがスキーマの description プロパティに供給されたとき、JSON は技術的には有効でしたが、説明には エンティティと不要な<span>タグが含まれていました。Google はそのプロパティを無視しました。すべてのビルダークラスにサニタイゼーションステップを追加し、値がスキーマ配列に到達する前にタグをストリップし 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 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.
---
エンティティグラフ:それを無視するな
平凡なスキーマ業務から本当に優れた技術 SEO を分ける要素の 1 つは、エンティティグラフです——特に、すべてのページに表示され、すべてをリンクで結ぶべきサイト全体の Organization および WebSite エンティティです。Organization and WebSite entities that should appear on every page and link everything together.
ほとんどのサイトはこれらを貧弱に持っています。名前、URL、ロゴかもしれません。完全なOrganizationタイプはWikidataエントリ、ソーシャルプロフィール、および他の信頼できるソースへのsameAsリンクをサポートしています。その相互リンク構造が、Googleがナレッジグラフ内のあなたのOrganizationエンティティがページスキーマに表示される同じエンティティであることを確信する方法です。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 hadcontactPointに構造化された電話番号と部門情報を含めるwith structured phone and department infofoundingDate と numberOfEmployees(おおよその範囲——これはどのみち公開情報です)andnumberOfEmployees(rough range -- this is public info anyway)
それで一夜にしてランキングが動きましたか?いいえ。スキーマは単独ではほぼ動くことはありません。しかし、これはインフラストラクチャです。一度きちんと構築すれば、時間をかけて複利で成長していきます。
---
FAQ
このスケールでのスキーマ実装にはどのくらいの期間がかかりますか?
91,000 ページのトラベルサイトの場合、完全な実装——アーキテクチャ、ビルダークラス、キャッシングレイヤー、テスト、GSC 監視セットアップ——は 2 人の開発者で約 6 週間かかりました。多く聞こえます。しかし、その時間の半分は既存のデータ品質の監査であり、スキーマコードの記述ではありません。データがクリーンなら、より速く進めることができます。
大規模サイトではプラグインを使うべきか、カスタム構築を検討すべきか?
数百ページ程度までであれば、プラグインで十分に対応できる。Rank Mathのスキーマモジュールは堅牢で、カスタムスキーマブロックは合理的な柔軟性を提供する。数千ページ以上で複数の異なるコンテンツタイプがある場合は、毎回カスタム構築を選ぶ。その制御能力は構築コストに見合う価値がある。
大規模運用で最も一般的なスキーマの間違いは何か?
レビューが存在する場合に aggregateRating がない、または存在しない場合に含める。Google はこれに厳しい。スキーマが 843 件のレビューから 4.7 の aggregateRating を主張しているのに、ユーザーがページにアクセスしてレビューが見当たらない場合、それは手動対応を待つ状況だ。ビルダークラスの条件付きロジックは絶対に必要だ。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.
スキーマは直接的にランキングを向上させるか?
直接的には?ほとんどのクエリタイプでは大きな効果はない。効果があるのはリッチリザルト――スター評価、FAQ ドロップダウン、レビュースニペット、SERP のパンくずリスト――をアンロックすることで、これらの機能はクリックスルーレートを測定可能なほど改善する。旅行クライアントはホテルページで完全実装後 4 ヶ月で CTR が 22% 増加した。それはエンゲージメントシグナルに繋がり、ランキングに影響する。つまり:間接的にだが、実質的に効果がある。
スキーマ作業で実際に日常的に使うツールは何か?
クロールレベルの監査には Screaming Frog。スポットチェックには Google の Rich Results Test。プロパティレベルの検証には 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.
---
大規模でのスキーマは、プラグインの問題に見えるが、内部に入ると実はSEOの装いをした設計アーキテクチャの問題だと気づく類の課題だ。データモデルを正しく設計する。キャッシュ戦略を賢く実装する。検証を容赦なく行う。マークアップ自体はほぼ簡単な部分だ。
