2021年のことです。Seahawkに旅行関連のクライアントがマイグレーションの依頼をくれたんですが、その内容を見たときは正直ぞっとしました。91,000ページの目的地とホテルページ。それぞれに有効で、具体的で、テストされたスキーママークアップが必要でした。ほとんどのプラグインが投げやりに当てがうような、万能型のWebPageタイプじゃなくてね。そのクライアントは既に2つの「自動スキーマ」WordPressプラグインを試していました。両方とも技術的には有効なJSON-LDを出力していましたが、実質的にはまったく役に立たないものでした。汎用的な名前、ネストされたエンティティなし、価格情報が欠落、レビューの集計が間違った対象を指しているという状態。Googleのリッチリザルトテストはもう困惑しきってました。
重要なポイント:91,000ページ分のスキーマはプラグインの問題ではなく、アーキテクチャの問題である。データレイヤーからビルド時に生成し、パイプラインで検証する必要がある。
そのプロジェクトは、過去8年間を合わせたよりも多くのことを、大規模スキーマについて教えてくれました。だからここが、私が実際に知っていることです。
---
「プラグインをインストールすればいい」が大規模では破綻する理由
聞いて、YoastやRank Mathにケンカを売るつもりはありません。40ページの企業サイトなら、本当に問題ありません。ですが500ページ前後のどこかで、プラグインが生成したスキーマは、独自の仮定の重みで軋み始めます。
根本的な問題は、プラグインがページテンプレートの周辺に設計されていることです。データモデルではなく。投稿タイトルを読んで、せいぜいカスタムフィールドを1、2個読み込んで、スキーマのブロブを構築する。あなたのサイトが6つのコンテンツタイプ(ホテル、目的地、ツアー、レビュー、FAQ、著者プロフィール)にまたがる91,000ページを持っているときは、単一のプラグイン設定ではそうした多様性を膨大な手作業によるオーバーライドなしに表現できません。その規模で手作業オーバーライドをやってたら、もう負けたも同然です。
ここが要点です。スキーママークアップは根本的にはデータ変換の問題です。構造化データはデータベースに存在し、それを<script>タグ内のJSON-LDで表現する必要があります。それだけです。そのように枠組みを捉えた瞬間に、正しいアーキテクチャが非常に明確になります。
私が何度も見ている3つの失敗パターン
- テンプレートにハードコードされた静的なスキーマの塊。製品名が変わるまでは問題ないが、その時点で12,000ページがGoogleに嘘をついてる。
- 条件分岐ロジックに対応していないプラグイン設定。例えば、実際にレビューが存在するときだけ
aggregateRatingを表示するとか、投稿カテゴリーごとに異なる@typeを使い分けるといったことができません。 - 一度アップロードされて二度と更新されないバッチ生成ファイル。スキーマが18ヶ月前のままのサイトを監査したことがある。価格が間違ってた。イベント日程は過ぎてた。
---
JSON-LDが規模で実際にどう機能するか
ツール類に入る前に、まず基礎知識を整理しましょう。JSON-LD、つまりJSON for Linked Dataは、Googleが推奨するスキーマ形式です。理由は<script>ブロック内に存在し、HTMLから分離しているからです。つまりサーバー側で生成でき、きれいに挿入でき、マークアップに手を入れずに更新できます。その分離が、数万ページを扱うときの全てです。
Schema.orgの語彙は膨大です。ほとんどの人はその1%程度しか使いません。規模を扱うときはもっと深く掘り下げる必要があります。Hotel、TouristDestination、LocalBusiness、Review、AggregateRating、ネストされたOfferオブジェクト、BreadcrumbList。各タイプには必須と推奨のプロパティがあり、Googleの「推奨」の解釈は基本的には「リッチリザルトが欲しければ必須」です。
私が働く基本的なルール:ページあたり1つのプライマリ`@type`で、必要に応じてネストされたタイプを含める。5つの@typeを積み重ねて1つがくっつくことを祈るな。最も具体的でフィットするタイプを選んで、その中にサポート用タイプをネストさせろ。
---
実際に使用したアーキテクチャ
旅行クライアント向けに、3層システムを採用しました。ホワイトボード図のように優雅ではありませんが、機能しました。
レイヤー1:テンプレートレベルスキーマクラス(PHP)
各コンテンツタイプ用に独自のPHPクラスを作り、スキーマ配列の構築を担当させました。HotelSchemaBuilder、DestinationSchemaBuilder、TourSchemaBuilder、こんな具合です。各クラスはACF Proのカスタムフィールド、必要に応じてWooCommerceのデータ、それに計算値(CPTベースのレビューシステムからaggregateRatingを計算するなど)を引っ張ってきました。
各クラスの出力はプレーンなPHP配列でした。JSONはまだありません。ただのデータです。
これが重要な理由は、シリアル化から離れてデータロジックを単体テストできるということだ。このプロジェクトで初日からそうやってれば良かった。やらなかった。ratingValueが文字列の代わりにfloatを返してるのに気付いて、Googleのバリデータが黙ってaggregateRatingブロック全体を無視してるのに気付いた時、ステージングでのデバッグに約2日かかった。
レイヤー2:中央スキーママネージャー
単一のSchemaManagerクラスは、wp_headにフックされて、次の責任があった:
- 現在のテンプレート/ポストタイプに基づいて、呼び出すビルダークラスを決定する
- サイト全体のエンティティをマージ(
Organizationグラフ、SearchActionを備えたWebSite、BreadcrumbList) JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODEを使用して最終配列を JSON としてエンコード<script type="application/ld+json">タグでラップして出力する
ブレッドクラムのロジックが最も複雑でした。目的地は3階層の構造を持っていました:地域 → 国 → 都市。BreadcrumbList をハードコーディングすることなく動的に反映させるには、レンダリング時に投稿の祖先をトラバースする必要がありました。注意しないと遅くなります。投稿 ID ごとにブレッドクラム配列をキャッシュし、24時間の TTL を設定したトランジェントに保存しました。これにより、オーバーヘッドはほぼ無視できるレベルまで低下しました。
レイヤー3:検証とモニタリング
スキーマの生成はステップ1です。それが壊れたときに気づくのはステップ2であり、ほとんどのチームはこれを完全にスキップしています。
Google Search Consoleのプロパティを設定して、毎週リッチリザルトレポートを監視しました。でもこれはリアクティブです。GSCはGoogleがページをクロールした後のエラーを教えてくれます。積極的なチェックのため、月1回、上位2,000ページのクロール結果に対してSchemaAppを実行しました。GSCレポートが曖昧にしているプロパティレベルのエラーが浮かび上がります。
また、Google のリッチリザルトテストには API があります。毎晩 50 個の URL のランダムサンプルでその API を叩く小さなスクリプトを書き、検証エラーをログに記録しています。安価な保険です。
---
動的データを扱いながらパフォーマンスを損なわない方法
ほとんどの大規模実装がつまずくのはここです。ライブデータを参照するスキーマ、価格、在庫状況、レビュー件数は、常に最新の状態を保つ必要があります。でも91,000ページ全てのページロードのたびにJSON-LDを再生成するのはタダではありません。
私のアプローチだが、ここ数年で十数サイトの大規模実装を通じて洗練させてきた。
キャッシュは積極的に、無効化はスマートに。
ホテルページについては、スキーマブロブがポストメタとして保存され、シリアル化されたJSON-LD文字列として格納され、次の場合にのみ再生成されていました:
- ポスト自体が更新されたとき
- そのポストに新しいレビューが投稿されたとき
- 価格カスタムフィールドが変更されました(これに関しては ACF の
save_postアクションにフックしました)
その他すべてはキャッシュされた文字列を配信した。非常に高速だ。そして無効化フックが具体的だったので、スキーマは常に正確だった。
当初、私が誤ったこと:開始タグと終了タグを含む完全な <script> タグをキャッシュしていました。その後、1つのコンテンツタイプの @context URL を変更する必要がありました。すべてのキャッシュエントリを削除しなければなりませんでした。今は JSON 文字列だけをキャッシュし、レンダリング時にそれをラップしています。コードが 5分増えましたが、頭を悩ませる時間が 1時間節約できました。
リアルタイム価格設定についてはどうか?
1日に何度も変更されるツアー料金の場合、別のアプローチを採用しました。基本スキーマはキャッシュされましたが、Offer ブロックはリクエスト時に新たに生成され、シリアル化前にマージされました。はい、リクエストごとにわずかなオーバーヘッドが追加されました。しかし、ページロードあたり 12回ではなく 1回のデータベースクエリです。許容できるトレードオフです。
---
複数サイトへのスケーリング:Seahawkの視点
Seahawkは12,000を超えるサイトを構築しており、スキーマ実装はそのかなりの部分で課題となります。旅行クライアントは極端なケースでした。しかし、91,000ページであろうと4,000ページであろうと、同じアーキテクチャの原則が適用されます。
再利用可能なパターンとして落ち着いたのは、seahawk-schema-coreと呼ぶ小さな内部WordPressプラグインで、コンテンツタイプ固有のロジックなしでマネージャー/ビルダースキャフォルディングを提供します。クライアントプロジェクトは独自のビルダークラスで拡張します。コアスキーマロジックに対するプラグイン依存関係はありません。サードパーティプラグインの更新がサイト全体のリッチリザルトを壊すリスクもありません。
最後のポイントは人々が認めるより現実的です。Rank Mathの更新がカスタムスキーマオーバーライドを無言で破裂させるのを見てきました。Rank Mathが悪いからではなく、大規模なサイトが必要とするレベルで出力をカスタマイズしているとき、プラグインが処理するよう設計されたもの以外で動作しているからです。コードを所有し、リスクプロファイルを所有してください。
---
この規模でのテスト:実用的なチェックリスト
91,000 URLを手動でテストすることはできません。したがって、インテリジェントにテストします。
- テンプレートタイプ別にサンプルを取得します。コンテンツタイプごとに10個のURLを選び、テストしてください。ビルダーが1つのホテルページに対して正しければ、3,000個のホテルページすべてに対して正しいです(不正なデータがない限り、詳細は以下を参照)。
- エッジケースを特に重点的にテストする。レビューがないページ。カスタムフィールドが不完全なページ。タイトルに特殊文字(
&、"、アクセント文字)が含まれるページ。JSON のシリアライゼーションはこれらの多くを処理するが、すべてではない。 - Screaming Frog でフルの構造化データクロールを実行してください。Screaming Frog SEO Spider には構造化データ抽出モードがあり、クロールするすべての URL から JSON-LD をプルして検証します。エラーをエクスポートし、テンプレートタイプでグループ化し、ソースで修正します。
- GSCのEnhancementsタブをモニタリングしてください。閾値アラートを設定し、有効な項目が週比で5%以上減少した場合は、何か問題が発生しています。48時間以内に対応してください。
- すべてのデプロイ後にスポットチェックを実施してください。スキーマコードが変更されていなくても、データベースマイグレーション、プラグイン更新、テーマ変更のいずれでも、スキーマ出力を破損させるアップストリームデータの問題が生じる可能性があります。
不正なデータは静かに忍び寄る殺し屋である
トラベルサイトは3つの国にまたがる12人のコンテンツチームを持っていました。一部の目的地ページは説明フィールドに不正な形式のHTMLが含まれていました。おそらくWordから貼り付けられたものです。そのフィールドがスキーマのdescriptionプロパティに入ると、JSONは技術的に有効でしたが、説明には エンティティと迷い込んだ<span>タグが含まれていました。Googleはそのプロパティを無視しました。すべてのビルダークラスにサニタイゼーションステップを追加して、値がスキーマ配列に入る前にタグを削除し、HTMLエンティティをデコードしました。恒久的に解決しました。
---
エンティティグラフ:それを無視するな
平凡なスキーマ作業と本当に優れた技術SEOを分ける1つのことは、エンティティグラフ、具体的には、すべてのページに表示され、すべてを一緒にリンクすべき、サイト全体のOrganizationとWebSiteエンティティです。
ほとんどのサイトはこれらを貧弱に持っています。名前、URL、ロゴかもしれません。完全なOrganizationタイプはWikidataエントリ、ソーシャルプロフィール、および他の信頼できるソースへのsameAsリンクをサポートしています。その相互リンク構造が、Googleがナレッジグラフ内のあなたのOrganizationエンティティがページスキーマに表示される同じエンティティであることを確信する方法です。
旅行クライアント向けに、Organizationブロックを以下のように構築しました:
sameAsはCrunchbaseプロフィール、LinkedInページ、および彼らが持っていたWikipediaスタブを指すcontactPointに構造化された電話番号と部門情報を含めるfoundingDateとnumberOfEmployees(大まかな範囲、これはどうせ公開情報です)
それで一夜にしてランキングが動きましたか?いいえ。スキーマは単独ではほぼ動くことはありません。しかし、これはインフラストラクチャです。一度きちんと構築すれば、時間をかけて複利で成長していきます。
---
FAQ
このスケールでのスキーマ実装にはどのくらいの期間がかかりますか?
91,000ページのトラベルサイトについて、完全な実装、アーキテクチャ、ビルダークラス、キャッシング層、テスト、GSCモニタリング設定には、2人の開発者で約6週間かかりました。多く聞こえます。しかし、その時間の半分はスキーマコードを書くのではなく、既存のデータ品質を監査するのに費やしました。データがクリーンであれば、より速く進めることができます。
大規模サイトではプラグインを使うべきか、カスタム構築を検討すべきか?
数百ページ程度までであれば、プラグインで十分に対応できる。Rank Mathのスキーマモジュールは堅牢で、カスタムスキーマブロックは合理的な柔軟性を提供する。数千ページ以上で複数の異なるコンテンツタイプがある場合は、毎回カスタム構築を選ぶ。その制御能力は構築コストに見合う価値がある。
大規模運用で最も一般的なスキーマの間違いは何か?
レビューが存在するのにaggregateRatingが欠落している、またはレビューがないのに含まれているケースです。Googleはこの点に厳格です。スキーマが843件のレビューから4.7の aggregateRatingを主張しているのに、ユーザーがページに着陸してレビューが一件も見当たらない——これは手動対応の予兆です。ビルダークラス内の条件分岐ロジックは必須事項です。
スキーマは直接的にランキングを向上させるか?
直接的には?ほとんどのクエリタイプではそこまで大きくありません。本当の価値はリッチリザルト、スター評価、FAQドロップダウン、レビュースニペット、SERPのパンくずリストをアンロックすることです。こうした機能はクリックスルーレートを目に見えて改善します。あるホテルクライアントは完全実装から4ヶ月以内にホテルページのCTRが22%上昇しました。それはエンゲージメントシグナルに流れ込み、ランキングに影響を与えます。つまり:間接的にはそうです。かなり大きな影響です。
スキーマ作業で実際に日常的に使うツールは何か?
クロールレベルの監査にはScreaming Frogを使います。抜き打ち検査にはGoogleのRich Results Testを。プロパティレベルの検証にはSchema Markup Validatorをvalidator.schema.orgで。そして正直に言えば、Schema.orgのドキュメント自体——Hotelタイプのページと他いくつかをブックマークしていて、常に参照しています。高額なサブスクリプションツールは不要です。
---
大規模でのスキーマは、プラグインの問題に見えるが、内部に入ると実はSEOの装いをした設計アーキテクチャの問題だと気づく類の課題だ。データモデルを正しく設計する。キャッシュ戦略を賢く実装する。検証を容赦なく行う。マークアップ自体はほぼ簡単な部分だ。
