【Next.js】App RouterでJSON-LDを出力する方法(head内に入れたいが…)
更新日:2026/03/16
JSON-LDは検索エンジンへWebページの情報を伝えることを目的とした構造化データ形式です。
多くのWebサイトで導入されているので、Next.jsが生成したページにも出力したいですね。
そこで今回は、Next.jsが生成したページにJSON-LDを出力する方法についてお伝えします。
head内にJSON-LDを出力可能?
JSON-LDはhead内に記述することが推奨されています。
そのため、Next.jsでもhead内に出力したいと思う開発者が多いはず。
扱ったことがないため確認していませんがPages Routerならできるようです。
しかし、Next.jsのApp Router構成ではJSON-LDをhead内に出力できません。
App Routerはmetadata変数またはgenerateMetadata関数で返されたmetadataオブジェクトを元にしてheadを生成します。
しかし現状では、metadataオブジェクトはscriptをサポートしていません。
そのため、JSON-LDをhead内に出力できないのです。
ここでは非推奨ですが、Next.jsのScriptコンポーネントをstrategy="beforeInteractive"と共に使用することで、head内にscriptを出力することができます。
Scriptコンポーネントの利用(非推奨)
import Script from 'next/script';
export default function Home() {
const breadcrumbJsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "ホーム",
item: "https://....rl",
}
],
}
return (<>
<Script id="ldjson"
strategy="beforeInteractive"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbJsonLd),
}}
/>
);
}
ブラウザの開発ツールで要素を確認すると、JSON-LDのスクリプトを確認できるはずです。
しかしサーバーから送信されたhtmlにJSON-LDは含まれません。
Nextのスクリプトがブラウザ上でheadに挿入するのです。
そのため、google等のbotがJSON-LDを検出しない可能性があります。
ではどうすればいいのか?
JSON-LDは、SEO的にはBody内でも問題ありません。
head内にこだわる必要はないのです。
これ以上の追及は、あまり意味がなさそうですね。
JSON-LDはscript要素で出力する
Scriptコンポーネントは、動的にscriptタグをセットするのでJSON-LD用途では利用できません。
代わりにscript要素で静的に出力します。
出力場所は、layoutでもpageでもどこでもOKです。
最適な場所を判断してコードを記述しましょう。
dangerouslySetInnerHTMLで出力
return (<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbJsonLd),
}}
/>
);
出力するオブジェクトをJSON.stringify()で文字列に変換する点がポイントです。
次のようにdangerouslySetInnerHTMLを使用しない方法で記述できます。
dangerouslySetInnerHTMLを使用しないパターン
<script
type="application/ld+json" >
{JSON.stringify(breadcrumbJsonLd)}
</script>
どちらも文字列をそのまま出力します。
そのためXSSの可能性があり十分な注意が必要です。
そのため、記述が面倒なdangerouslySetInnerHTMLが推奨されています。
サニタイズを忘れずに!
前項でも触れていますが、ユーザーが入力した文字や外部API等から取得した文字をそのまま出力するとクロスサイトスクリプティング(XSS)の標的となる可能性があります。
そのため、サニタイズを行うことでスクリプトを無効化する必要があります。
基本的にはJSON-LDをJSON.stringifyで文字列化すると、タグも文字として評価されるのでサニタイズの必要はありません。
OK
<script type="application/ld+json">
...
"name":"<h1>title</h1>"
...
</script>
ただし</script>が含まれていると、<script type="application/ld+json">の閉じタグと判定されます。
NG
<script type="application/ld+json">
...
"name":"</script><script>alert('')</script>"
...
</script>
上記のコードは次のように解釈されます。
NG
<script type="application/ld+json">
...
"name":"
</script>
<script>alert('')</script>
その結果、スクリプトが実行されます。
対策として、< を \\u003c に置き換えます。
\\u003c は < のUnicode表記です。
JSON.stringify(breadcrumbJsonLd).replace(/</g, "\\u003c")
< を < に置き換えるのはNGです。
文字列中の<は、そのまま<として解釈されるからです。
JSON-LD目的ならこれで十分ですが、次のように置き換えるとさらによいです。
JSON.stringify(breadcrumbJsonLd)
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e")
.replace(/&/g, "\\u0026")
.replace(/\u2028/g, "\\u2028") // Line Separator
.replace(/\u2029/g, "\\u2029") // Paragraph Separator
\u2028と\u2029は、JSの行終端と解釈されます。
そのまま使うと「Unterminated string constant」等のエラーになる可能性があります。
TypeScript型定義
TypeScriptでJSON-LDを使用する場合、型を自分で定義してもよいですが、Googleが管理しているTypeScript型定義パッケージを提供しているので利用しましょう。
パッケージ名は『schema-dts』です。npmでインストールします。
npm install --save-dev schema-dts
schema-dtsにはSchema.orgで定義されている800種類以上のタイプが定義されています。
例えばパンくずを定義するBreadcrumbListを、そのままの名称で利用できます。
BreadcrumbListの使用例
import type {BreadcrumbList} from "schema-dts";
const breadcrumb: BreadcrumbList = {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "ホーム",
item: "https://example.com",
}
]
}
オブジェクト内に"@context"を入れたいときは、WithContext<T>を使用します。
(TはBreadcrumbList等の型)
WithContext<T>の使用例
import type {BreadcrumbList} from "schema-dts";
const breadcrumb: WithContext<BreadcrumbList> = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "ホーム",
item: "https://example.com",
}
]
}
デモコード
Next.jsでJSON-LD出力するデモコードを作成しました。
今回はコードが多いため記事上で紹介できません。
GitHubでコードを公開しているので、そちらを参考にしてください。
デモコードは、html内に出力したJSON-LDをブラウザ画面上に表示するようにページ生成しています。
出力している項目は最小限なので、適宜追加してください。
ファイル構造は次のようになっています。
nextjs-jsonld ┣━ app ┃ ┣━ category ┃ ┃ ┣━ [category] ┃ ┃ ┃ ┣━ page ┃ ┃ ┃ ┃ ┗━ [page] ┃ ┃ ┃ ┃ ┗━ page.tsx ← カテゴリの記事一覧ページ(ページ指定) ┃ ┃ ┃ ┗━ page.tsx ← カテゴリの記事一覧ページ ┃ ┃ ┗━ page.tsx ← カテゴリページ ┃ ┣━ image ┃ ┃ ┗━ [name] ┃ ┃ ┗━ route.tsx ← 画像の動的生成 ┃ ┣━ page ┃ ┃ ┗━ [page] ┃ ┃ ┗━ page.tsx ← トップページ(ページ指定) ┃ ┣━ posts ┃ ┃ ┗━ [slug] ┃ ┃ ┗━ page.tsx ← 記事ページ ┃ ┣━ favicon.ico ┃ ┣━ globals.css ┃ ┣━ layout.tsx ┃ ┣━ not-found.js ┃ ┗━ page.tsx ← トップページ ┣━ components ┃ ┣━ categorypage.tsx ← /app/[category]および/app/[category]/page/[page]のpage.tsx実装 ┃ ┣━ jsonld.tsx ← JSON-LD出力コンポーネント&JSON-LDオブジェクトの生成 ┃ ┣━ pager.tsx ← ページャーコンポーネント ┃ ┗━ toppage.tsx ← /app/page.tsxおよび/app/page/[page]の実装 ┣━ lib ┃ ┣━ data.tsx ← サイトのデータ ┃ ┗━ purify.ts ┣━ .env.development ┣━ eslint.config.mjs ┣━ next-env.d.ts ┣━ next.config.ts ┣━ package-lock.json ┣━ package.json ┗━ tsconfig.json
各ページで、次のタイプのJSON-LDを生成しています。
| ページ種別 ファイル | JSON-LDタイプ | 備考 |
|---|---|---|
| トップページ /app/page.tsx /app/page/[page]/page.tsx | WebSite、Person、ItemList | ページ分割あり |
| 記事ページ /app/posts/[slug]page.tsx | BlogPosting、BreadcrumbList | |
| カテゴリページ /app/category/page.tsx | CollectionPage、ItemList、BreadcrumbList | |
| カテゴリの記事一覧ページ /app/category/[category]/page.tsx /app/category/[category]//page/[page]/page.tsx | CollectionPage、ItemList、BreadcrumbList | ページ分割あり |
JSON-LDの生成処理は/components/jsonld.tsxにまとめていますが、記事ページのBlogPostingだけ/app/posts/[slug]/page.tsx内でおこなっています。
移動すべきかどうかの判断を後回しにしていたら、そのまま失念していました。
とりあえず、このままでいきます。
まとめ
JSON-LDは script要素で出力しましょう。
headタグ内に出力することはあきらめましょう。
というお話でした。
更新日:2026/03/16
関連記事
スポンサーリンク
記事の内容について

こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。

