App RouterでJSON-LDを組み込むなら、Server Componentから`<script type="application/ld+json">`を直接出力する方法が最も確実です。構造化データがサーバーの返す初期HTMLに含まれるため、AIクローラーはJavaScriptを実行しなくても読み取れます。一方、useEffectやGoogle Tag Managerで後から挿入するJSON-LDは、回答エンジンにとって事実上存在しないも同然です。多くのクローラーは、ページのハイドレーションを待たずに巡回を終えて離脱します。
なぜサーバー側で出力する必要があるのか
決定的な違いは、出力されるタイミングです。Server Componentは、リクエスト時にJSON-LDをHTML文字列へ書き込みます。GPTBot、ClaudeBot、PerplexityBot、Google-Extendedなどのクローラーが最初に取得するファイルに、完全なスキーマが含まれることになります。クライアント側で挿入する場合は、ブラウザがバンドルをダウンロードし、実行するまで待たなければなりません。多くのAIクローラーは、この処理自体を行いません。ここには見落としやすい落とし穴があります。GoogleのリッチリザルトテストはJavaScriptをレンダリングするため、クライアント側で挿入したデータも正常に見えます。その結果、検証を通過したと思い込んでしまいます。しかし、回答エンジンが実際に取得するのはレンダリング前の初期HTMLであり、そこにスキーマはありません。当社がクライアント向けに実施する技術監査でも、これは特に多く、しかも見逃されやすい減点要因です。
最小構成の実装例:page.tsxだけで完結
実装そのものはごくシンプルです。追加パッケージもAPIルートも必要ありません。記事をレンダリングするServer Component内で完結します。要点は、データの取得、オブジェクトの組み立て、scriptの出力という3つだけです。
- `app/blog/[slug]/page.tsx`はデフォルトでServer Componentです。まず`await`で対象記事のデータを取得します。
- 純粋なJavaScriptオブジェクトとして`jsonLd`を組み立てます。先頭に`"@context": "https://schema.org"`と`"@type": "Article"`を置き、残りのフィールド(headline、datePublished、author、image)には記事データを動的に設定します。
- 返却するJSXの先頭に、`<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }} />`を配置します。
- クライアント向けライブラリはimportせず、ファイル先頭に`"use client"`も追加しないでください。追加した時点でClient Componentとなり、それまでの対応が無意味になります。
@graphで複数のエンティティを関連付ける
通常、1ページで必要になるスキーマは1つではありません。ブランド自体はOrganization、記事はArticle、ナビゲーション経路はBreadcrumbList、Q&AセクションはFAQPageです。4つの独立したscriptタグを出力するより、`@graph`配列を使って同じJSON-LDオブジェクトへまとめ、`@id`で相互に参照させるほうが整理しやすくなります。たとえば、Articleの`publisher`からOrganizationの`@id`を参照し、BreadcrumbListの最後の項目から現在のページの`@id`を参照させます。回答エンジンは1回の解析でエンティティ間の関係を把握でき、矛盾したノードや重複ノードを拾う可能性も抑えられます。
- OrganizationとWebSite:`app/layout.tsx`に配置し、サイト全体で共通利用します。各ページで繰り返し宣言しないでください。
- ArticleまたはBlogPosting:記事の`page.tsx`に配置し、各フィールドへ現在のページのデータを動的に設定します。
- BreadcrumbList:ルーティング階層に沿って構成します。AIがサイト構造を理解するうえで、特に有効です。
- FAQPage:画面上に実際のQ&Aセクションがある場合に限って追加します。`acceptedAnswer`の文面は、画面に表示される内容と一致させてください。スキーマのためだけに架空のQ&Aを作ってはいけません。

JSON-LDを保守しやすくする
スキーマオブジェクトを各ページへ分散させると、3か月後には誰も変更できない状態になりがちです。当社では`lib/schema.ts`へ切り出し、`schema-dts`の型(`import type { Article, WithContext } from "schema-dts"`)を使って、各生成関数の戻り値を制約しています。フィールド名や型に誤りがあれば、TypeScriptがコンパイル段階でエラーを報告します。ページ側では`buildArticleSchema(post)`を呼び出し、型安全なオブジェクトを受け取って出力するだけです。修正は1か所で済み、サイト全体へ反映されます。URLには必ず絶対パスを使い、`SITE_URL`定数へ集約します。これにより、本番サイトとプレビュー用ドメインの間で`@id`が食い違う事態を防げます。
よくあるミスと公開前の検証
- JSON-LDの`url`、`image`、`datePublished`が、画面上の実際のコンテンツと一致していないと、回答エンジンに差異を検出され、ページ全体の信頼性が低下します。
- `@id`や`url`に相対パスを使うと、プレビュー用ドメインへ切り替えた際に誤った場所を指すことがあります。
- `<`のエスケープを忘れる、またはページ先頭へ誤って`"use client"`を追加すると、scriptがクライアント側からの出力になります。
- 公開前には、まずGoogleのリッチリザルトテストで構文を確認します。続いて`curl`でページの初期HTMLを直接取得し、scriptがブラウザのDevToolsだけに表示されるのではなく、初期レスポンスへ実際に含まれていることを確かめます。この確認はAIクローラーの見ている内容をそのまま再現するため、最も重要な工程です。
当社が監査したWebサイトでは、JSON-LDの問題の多くはスキーマの記述ミスではなく、クライアント側にしか存在しないことでした。サーバーの返すHTMLに含まれていなければ、AIには存在しないものとして扱われます。— Tenten GEO Technical Audit
実装から引用される状態へ
JSON-LDをServer Componentへ移すことは、コンテンツを「機械可読」にするための最低条件です。その先では、エンティティ名、内部リンク、ページ間の整合性もそろえ、AIが信頼できる情報源として安定的に引用できる状態をつくる必要があります。自社サイトの構造化データが、クローラーの見るHTMLへ本当に含まれているか判断できない場合は、30分のGEO診断をご予約ください。レンダリング後の画面だけで判断せず、実際に取得したソースコードを使って課題を洗い出します。



