Next.js App Router × JSON-LD 完全実装ガイド:Cursor・Claude Code・Copilotでの自動生成まで

Next.js App Routerで開発しているなら、JSON-LDの実装は思ったより簡単で、 かつ動的コンテンツとの相性が抜群です。 さらに Cursor・Claude Code・GitHub Copilot などのAI開発ツール を活用すれば、 適切なプロンプトを与えるだけでスキーマコードを自動生成できます。 本章では、Next.jsでのJSON-LD実装パターンから、 AIコーディングツールに最高の出力を出させるプロンプト設計まで、 実際のコードと共に解説します。

📌 本章でカバーするトピック
  • Next.js App Router での JSON-LD の基本配置(layout.tsx vs page.tsx)
  • 動的ルート([slug])での CMS 連携 JSON-LD 生成パターン
  • TypeScript による JSON-LD の型安全な定義
  • metadata API と JSON-LD の役割分担
  • Cursor・Claude Code・GitHub Copilot に JSON-LD を自動生成させるプロンプト
  • 開発環境での確認方法とデバッグ手順

1. App Router における JSON-LD の配置戦略

Next.js の Pages Router では next/head を使っていましたが、 App Router では <script> タグを JSX 内に直接記述します。 これは Server Component で動作し、クライアントサイドの JavaScript を必要としません。どのファイルに置くかは、スキーマの「スコープ」で決まります。

layout.tsx に置くスキーマ

サイト全体に共通のスキーマ。WebSite・Organization はルートの app/layout.tsx に置くのが最適。 全ページで同じ組織情報が出力されます。

page.tsx に置くスキーマ

ページ固有のスキーマ。Article・BreadcrumbList・Product・FAQPage はそれぞれの page.tsx に置く。 動的ルートでは paramsfetch データを使って動的に生成。

app/layout.tsx:WebSite + Organization の実装(推奨パターン)TypeScript / Next.js App Router
// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: { default: 'サイト名', template: '%s | サイト名' },
  description: 'サイトの説明',
};

// JSON-LD は metadata とは別に定義
const jsonLd = {
  '@context': 'https://schema.org',
  '@graph': [
    {
      '@type': 'WebSite',
      '@id': 'https://example.com/#website',
      'url': 'https://example.com',
      'name': 'サイト名',
      'inLanguage': 'ja',
      'publisher': { '@id': 'https://example.com/#organization' },
    },
    {
      '@type': 'Organization',
      '@id': 'https://example.com/#organization',
      'name': '株式会社サンプル',
      'url': 'https://example.com',
      'logo': {
        '@type': 'ImageObject',
        'url': 'https://example.com/logo.png',
      },
      'sameAs': ['https://twitter.com/example'],
    },
  ],
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(jsonLd).replace(/</g, '\u003c')
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
💡 @graph を使って複数スキーマを1つにまとめる
@graph は JSON-LD の仕様で、複数のスキーマオブジェクトを1つの <script> タグにまとめる方法です。[...](配列)でも同じことができますが、@graph を使うと スキーマ間の @id による参照がより明確になり、Googleのナレッジグラフとの連携品質が向上します。
⚠️ Next.jsでの安全な埋め込み
JSON.stringify(jsonLd) だけでは、悪意ある文字列中の < を完全には無害化できません。 CMS由来のタイトルや説明文を埋め込む場合は、JSON.stringify(jsonLd).replace(/</g, '\\u003c') ではなく、 実際のコードでは JSON.stringify(jsonLd).replace(/</g, '\u003c') のように 追加の置換を行う実装が安全です。

2. 動的ルートでの JSON-LD 生成:CMS連携パターン

ブログや製品ページのように、コンテンツがCMSやデータベースから取得される場合、 JSON-LDも動的に生成する必要があります。Next.js の generateMetadata に似た発想で、 データフェッチと JSON-LD 生成を page.tsx 内で完結させます。

app/blog/[slug]/page.tsx:動的 Article JSON-LD の完全実装TypeScript / Next.js App Router
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

// CMSや独自データ取得関数(実装はプロジェクトに合わせる)
async function getPost(slug: string) {
  const res = await fetch(`https://your-cms.com/api/posts/${slug}`, {
    next: { revalidate: 3600 } // ISR: 1時間キャッシュ
  });
  if (!res.ok) return null;
  return res.json();
}

// Next.js の generateMetadata と JSON-LD を同じデータソースから生成
export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug);
  if (!post) return { title: '記事が見つかりません' };

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
    siteName: 'AIOGeoScan',
    images: [{ url: '/og-image.jpg', width: 1024, height: 1024 }],
      publishedTime: post.publishedAt,
      authors: [post.author.name],
    },
  };
}

export default async function BlogPostPage(
  { params }: { params: { slug: string } }
) {
  const post = await getPost(params.slug);
  if (!post) notFound();

  // JSON-LD を動的データから生成
  const jsonLd = [
    {
      '@context': 'https://schema.org',
      '@type': 'Article',
      '@id': `https://example.com/blog/${params.slug}#article`,
      'headline': post.title,
      'description': post.excerpt,
      'image': post.ogpImage,
      'datePublished': post.publishedAt,
      'dateModified': post.updatedAt,
      'author': {
        '@type': 'Person',
        '@id': `https://example.com/author/${post.author.slug}#person`,
        'name': post.author.name,
        'url': `https://example.com/author/${post.author.slug}`,
      },
      'publisher': { '@id': 'https://example.com/#organization' },
      'mainEntityOfPage': {
        '@type': 'WebPage',
        '@id': `https://example.com/blog/${params.slug}`,
      },
      'inLanguage': 'ja',
      'keywords': post.tags.join(', '),
    },
    {
      '@context': 'https://schema.org',
      '@type': 'BreadcrumbList',
      'itemListElement': [
        { '@type': 'ListItem', 'position': 1, 'name': 'ホーム', 'item': 'https://example.com' },
        { '@type': 'ListItem', 'position': 2, 'name': 'ブログ', 'item': 'https://example.com/blog' },
        { '@type': 'ListItem', 'position': 3, 'name': post.title, 'item': `https://example.com/blog/${params.slug}` },
      ],
    },
  ];

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd).replace(/</g, '\u003c')
        }}
      />
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

3. TypeScript で JSON-LD を型安全に書く

JSON-LD を素の object 型で書くと、プロパティ名のミスや 必須フィールドの漏れをコンパイル時に検出できません。schema-dts パッケージを使うと、schema.org の型定義を TypeScript で利用でき、 Cursor や Claude Code のコード補完精度も大幅に向上します。

schema-dts のインストールと使い方Terminal & TypeScript
# インストール
npm install schema-dts
# または
pnpm add schema-dts

# --------------------------------------------------------
# 使用例:型安全な Article スキーマの定義
# --------------------------------------------------------
import { Article, WithContext } from 'schema-dts';

const articleJsonLd: WithContext<Article> = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: '記事タイトル',
  description: '記事の説明',
  datePublished: '2026-04-12',
  author: {
    '@type': 'Person',
    name: '山田 太郎',
  },
  // 誤ったプロパティ名はTypeScriptエラーになる
  // publisedDate: '...'  ← コンパイルエラー(typoを検出)
};

// JSX での使用
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(articleJsonLd).replace(/</g, '\u003c')
  }}
/>
💡 schema-dts と Cursor / Claude Code の相乗効果
schema-dts の型定義があると、CursorやClaude Codeが「このオブジェクトは Article 型だ」と認識し、 必要なプロパティを正確に補完・提案するようになります。 型定義なしで書くより提案精度が格段に上がるため、 AI開発ツールを使っている場合は特に導入を推奨します。

4. Cursor・Claude Code で JSON-LD を自動生成するプロンプトパターン

AI開発ツールを使ってJSON-LDを生成する際、プロンプトの質が出力品質を大きく左右します。 曖昧な指示では不完全なスキーマが出力されますが、 以下のようなコンテキスト豊富なプロンプトを使うと、すぐに使える完全なコードが生成されます。

プロンプト① Cursor でのコンテキスト指示

Cursor の Chat / Composer に貼り付けるプロンプト(Article 生成)Prompt Template
@Codebase

このプロジェクトはNext.js 14 App Routerを使ったブログサイトです。
app/blog/[slug]/page.tsx に以下の要件でJSON-LDを追加してください:

要件:
1. @type は "Article"(または BlogPosting)を使う
2. headline, description, datePublished, dateModified, author, publisher を含める
3. author は "Person" 型でネストし、name と url を持つ
4. publisher は app/layout.tsx の Organization の @id を参照する形式で書く
5. BreadcrumbList も同じ <script> タグに配列形式で追加する
6. パンくずは「ホーム > ブログ > [記事タイトル]」の3階層
7. データは既存の post オブジェクトから動的に取得する
8. TypeScript で型安全に書く(schema-dts を使用、未インストールなら通常の型でOK)

既存のコードを壊さないように追加してください。

プロンプト② Claude Code での一括実装指示

Claude Code に渡すプロンプト(サイト全体のJSON-LD設計)Prompt Template
このNext.js App Routerプロジェクトにサイト全体のJSON-LD構造化データを実装してください。

現在の構成:
- app/layout.tsx:ルートレイアウト(全ページ共通)
- app/page.tsx:トップページ
- app/blog/[slug]/page.tsx:ブログ記事ページ
- app/about/page.tsx:会社情報ページ
- app/services/[id]/page.tsx:サービス詳細ページ

実装してほしいJSON-LD:
1. layout.tsx → WebSite + Organization(@graph形式)
2. app/page.tsx → WebPage スキーマを追加
3. app/blog/[slug]/page.tsx → Article + BreadcrumbList(動的生成)
4. app/about/page.tsx → Organization + Person(代表者情報)
5. app/services/[id]/page.tsx → Service + BreadcrumbList

制約:
- schema.orgの最新仕様に準拠すること
- @id によるエンティティの相互参照を使うこと(sameAs / publisher など)
- TypeScript の型エラーが出ないこと
- Google Rich Results Testでエラーが出ないこと(必須プロパティを漏らさない)
- サイトURL は https://example.com を使う(後で置き換えるため)

まず実装計画を日本語で説明してから、コードを書いてください。

プロンプト③ GitHub Copilot でのインラインコメント活用

GitHub Copilot のコメント駆動コード生成(FAQPage)TypeScript コメント
// このページに FAQPage スキーマを追加する
// @type: FAQPage
// mainEntity: 以下のQ&Aを Question/Answer 形式で記述
// Q1: JSON-LDとは? A: schema.orgを使った構造化データ...
// Q2: リッチリザルトが表示されない原因は? A: 主な原因は...
// Q3: Next.jsでの実装方法は? A: scriptタグにdangerouslySetInnerHTML...
// inLanguage: "ja"
// すべてのテキストはプレーンテキストで(HTMLタグなし)
const faqJsonLd = // ← ここで Copilot が補完を提案する

5. metadata API と JSON-LD の役割分担(混同しがちなポイント)

Next.js の metadata API(titledescriptionopenGraph など)と JSON-LD はしばしば混同されますが、役割が異なる全く別の技術 です。 両方を正しく実装することが重要です。

項目Next.js metadata APIJSON-LD
出力先<title><meta> タグ<script type="application/ld+json">
主な読み手ブラウザ・SNSクローラー(OGP)・検索エンジン(基本情報)Googleナレッジグラフ・AI検索エンジン・リッチリザルトエンジン
リッチリザルトなし(基本的なタイトル・説明のみ)あり(FAQ・評価星・パンくずなど)
E-E-A-T対応限定的高い(著者・組織の詳細情報を記述可能)
AI検索対応部分的(OGPタグを参照することあり)高い(構造化された機械可読データとして優先的に利用)

📌 結論:両方を実装する。どちらか一方では不十分。
metadata API で titledescriptionopenGraph を設定し、 さらに JSON-LD でスキーマを実装することが最善策です。 Cursorや Claude Code に依頼するときは「metadata と JSON-LD の両方を実装してください」と 明示するのが重要です(どちらか一方だけ実装する誤りが多い)。

6. 開発環境でのデバッグとローカル確認方法

ブラウザの DevTools でHTML確認localhost:3000 を開き、DevToolsの「Elements」タブで <script type="application/ld+json"> を探します。 JSON が正しく出力されているか、改行・エスケープに問題がないかを目視確認します。
Google Rich Results Test の URL オプションを活用(本番環境)本番URLが公開されていない開発環境では、DevTools でHTMLをコピーして Rich Results Test の「コードのテスト」タブに貼り付けることでローカルテストが可能です。
schema.org Validator でオフライン検証validator.schema.org では JSON-LD テキストを直接貼り付けて検証できます。 Googleのツールより詳細なエラーメッセージが出るため、 開発中のデバッグに有用です。
JSON.stringify の出力を整形して確認デバッグ時は JSON.stringify(jsonLd, null, 2) の第3引数に 2(インデント幅)を渡すと 整形された JSON が出力され、構造確認が容易になります。 本番環境では第3引数を省略してサイズを最小化します。

7. よくある実装ミス:Next.js 固有の落とし穴

NG:Client Component に JSON-LD を置く

'use client' ディレクティブがあるコンポーネントでは、 JSON-LD はクライアントサイドでのみレンダリングされ、 Googleのクローラーが静的 HTML をフェッチした際に存在しない可能性があります。 JSON-LD は必ず Server Component(デフォルト状態)のファイルに書いてください。

NG:JSON.stringify なしで直接オブジェクトを渡す

dangerouslySetInnerHTMLJSON.stringify を省略して オブジェクトをそのまま渡すと [object Object] として出力されます。 必ず { __html: JSON.stringify(jsonLd) } の形式で渡してください。

NG:layout.tsx と page.tsx で同じスキーマを重複させる

Organization スキーマを layout.tsx にも page.tsx にも書くと、 同じページに同じエンティティが2つ存在することになり、Google側で混乱が生じます。 layout.tsx(全ページ共通)と page.tsx(ページ固有)でスキーマの種類を明確に分けてください。

NG:Suspense の外に JSON-LD を置き忘れる

非同期データを使う page.tsx で <Suspense> を使っている場合、 JSON-LD を Suspense の内部に置くと、ストリーミング SSR 時に JSON-LD が 初回 HTML に含まれない可能性があります。 JSON-LD は Suspense の外側(先に出力される部分)に置いてください。

実装したJSON-LDの品質を
AIOGeoScanで一括検証できます

Cursor・Claude CodeでJSON-LDを生成した後、AIOGeoScanで検証することで AIが見落としたエラーや必須プロパティの漏れを確実に検出できます。 開発→生成→検証のサイクルを自動化して、構造化データの品質を高速に向上させましょう。

今すぐ自社サイトを無料診断する
Editorial Trust Signals

このナレッジベースの編集方針

`AIOGeoScan Knowledge` は、Bennu Inc. が運営する AI検索・構造化データ・クローラー制御に関する実務ナレッジです。 Google Search Central、Schema.org、OpenAI などの一次情報を優先し、観測ベースの実務知見は本文中で区別して扱います。

運営主体
Bennu Inc. / AIOGeoScan
更新方針
仕様変更や検索機能の更新にあわせて都度改訂
優先ソース
公式ドキュメント・標準仕様・公式ヘルプ
補助ソース
実装観測・運用知見・再現性のある検証結果

あなたのサイト、AIに正しく伝わっていますか?

解説を読み終えたら、実際にあなたのサイトを診断してみましょう。
100項目以上の診断で、AI時代の構造課題を可視化します。

無料で診断を開始する