91,000ページのプログラマティックデータを活用した構造化マークアップ。タイポではありません。3つのプロダクション案件 — Deluxe Astrology(30言語、ホロスコープ、セレブリティプロフィール)、Not Another Sunday(137,000の会場リスティング)、HostList(25,000の企業プロフィール)— にわたって、ビルド時にデータベース行からJSON-LDスキーマを生成し、自動的に検証し、本番環境で監視するシステムを構築してきました。これは、実際に使用できる実装コードに抽出したすべての学習内容です。

これは「スキーママークアップとは何か」という記事ではありません。あなたはそれが何かを知っています。これは、30言語でページをサービスするSupabaseバックのNext.jsアプリに構造化データを接続し始めたときに存在していたらいいなと思う実装ガイドです。

目次

Next.jsのスキーママークアップ:2026年のJSON-LD構造化データガイド

2026年においてもスキーママークアップが重要である理由

Googleは毎日85億以上のクエリを処理しています。AI Overviewsは現在、米国の検索結果の約30%に表示されます。そして、あなたの実装の決定に関わる重要なポイントはこれです:構造化データは、マシンがあなたのページを理解する方法です。Googleだけではなく、ChatGPT、Perplexity、Claude、およびウェブを解析するLLM搭載のすべての検索ツール。

ROIのケースは明白です:

メトリック スキーマなし スキーマあり 当社が観測したデルタ
SERPからのCTR ベースライン リッチ結果で+25-35% Not Another Sundayの会場ページで+31%
AI Overviewの含有 低い 大幅に高い FAQ注釈付きページで3.2倍高い可能性
LLM引用率 最小限 測定可能 Perplexityで4倍以上引用されるFAQPageスキーマページ
リッチ結果適格性 なし 星、FAQ、パンくずなど インデックス付きページの87%でアクティブ

数万ページを持つサイトでは、手動スキーマは不可能です。システムが必要です。これがこのガイドが構築するものです。

LLM引用角度:マシンリーダブルゴールドとしてのFAQPage

ほとんどのスキーマガイドでカバーされていないものを紹介します:FAQPageスキーマは、LLM搭載検索エンジン用の最も機械が読み取り可能な形式です。ChatGPTまたはPerplexityがあなたのページをクロールするとき、彼らは明確に構造化されたQ&Aペアを探しています。FAQPageスキーマは、彼らに正確にそれを提供します — 事前解析された、あいまいでない質問応答ペアは、NLP抽出を必要としません。

このパターンに最初に気付いたのはDeluxe Astrologyです。FAQPageスキーマを持つページは、それなしの同等のページと比較して、Perplexity回答でおよそ4倍の速度で引用されていました。Q&Aペアはほぼそのまま抽出されていました。

これはもはやSEOプレイだけではありません。これは生成エンジン最適化(GEO)プレイです。AI生成回答に表示されるコンテンツが必要な場合 — 必要です。なぜなら、それが検索の向かう場所だからです — FAQPageスキーマはあなたの最高レバレッジ投資です。

Next.js App Router実装パターン

実装コードに取り組みましょう。サーバーコンポーネント内でレンダリングされる再利用可能なJsonLdコンポーネントを使用して、すべてのNext.js開発プロジェクトで一貫したパターンを使用しています。

基本コンポーネント

// components/json-ld.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify({
          '@context': 'https://schema.org',
          ...data,
        }),
      }}
    />
  );
}

シンプルです。クライアント側のJavaScriptはありません。ハイドレーションミスマッチはありません。これはサーバーコンポーネント出力にレンダリングされ、静的HTMLとして出荷されます。Googleのクローラーはすぐにそれを見ます — JavaScript実行は必要ありません。

レイアウトレベル対ページレベルのスキーマ

スキーマを2つのカテゴリに分割します:

レイアウトレベルlayout.tsxにレンダリング):Organization、WebSite、BreadcrumbList。これらはページ間またはページグループ内で一貫しています。

ページレベルpage.tsxにレンダリング):Article、FAQPage、Person、LocalBusiness、Product。これらはページごとにユニークであり、通常はデータベースコンテンツによって駆動されます。

// app/layout.tsx
import { JsonLd } from '@/components/json-ld';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <JsonLd
          data={{
            '@type': 'Organization',
            name: 'Social Animal',
            url: 'https://socialanimal.dev',
            logo: 'https://socialanimal.dev/logo.png',
            sameAs: [
              'https://twitter.com/socialanimaldev',
              'https://github.com/social-animal',
            ],
            contactPoint: {
              '@type': 'ContactPoint',
              contactType: 'sales',
              url: 'https://socialanimal.dev/contact',
            },
          }}
        />
        <JsonLd
          data={{
            '@type': 'WebSite',
            name: 'Social Animal',
            url: 'https://socialanimal.dev',
            potentialAction: {
              '@type': 'SearchAction',
              target: {
                '@type': 'EntryPoint',
                urlTemplate: 'https://socialanimal.dev/search?q={search_term_string}',
              },
              'query-input': 'required name=search_term_string',
            },
          }}
        />
        {children}
      </body>
    </html>
  );
}

これは、サイト上のすべての単一ページがページごとの作業なしで、OrganizationおよびWebSiteスキーマを取得することを意味します。サーバーレンダリング、クライアントJS のオーバーヘッドなし。

Next.jsのスキーママークアップ:2026年のJSON-LD構造化データガイド - アーキテクチャ

実装するJSONLDコード付きのすべてのスキーマタイプ

本番環境で使用するすべてのスキーマタイプは次のとおりです。これは、プロジェクトの実際のパターンです。

Organization

{
  "@type": "Organization",
  "name": "Social Animal",
  "url": "https://socialanimal.dev",
  "logo": "https://socialanimal.dev/logo.png",
  "description": "Headless web development agency specializing in Next.js and Astro",
  "foundingDate": "2022",
  "sameAs": [
    "https://twitter.com/socialanimaldev",
    "https://linkedin.com/company/socialanimaldev"
  ],
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Remote",
    "addressCountry": "US"
  }
}

WebSite

上記のレイアウト例で示されています。SearchActionは、Googleのサイトリンク検索ボックスの電源です。スキップしないでください。

Article / BlogPosting

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  return (
    <article>
      <JsonLd
        data={{
          '@type': 'Article',
          headline: post.title,
          description: post.excerpt,
          image: post.featuredImage,
          datePublished: post.publishedAt,
          dateModified: post.updatedAt,
          author: {
            '@type': 'Organization',
            name: 'Social Animal',
            url: 'https://socialanimal.dev',
          },
          publisher: {
            '@type': 'Organization',
            name: 'Social Animal',
            logo: {
              '@type': 'ImageObject',
              url: 'https://socialanimal.dev/logo.png',
            },
          },
          mainEntityOfPage: {
            '@type': 'WebPage',
            '@id': `https://socialanimal.dev/blog/${post.slug}`,
          },
        }}
      />
      {/* Article content */}
    </article>
  );
}

FAQPage

これはLLM引用の大きなものです:

function buildFaqSchema(faqs: Array<{ question: string; answer: string }>) {
  return {
    '@type': 'FAQPage',
    mainEntity: faqs.map((faq) => ({
      '@type': 'Question',
      name: faq.question,
      acceptedAnswer: {
        '@type': 'Answer',
        text: faq.answer,
      },
    })),
  };
}
function buildBreadcrumbSchema(items: Array<{ name: string; url: string }>) {
  return {
    '@type': 'BreadcrumbList',
    itemListElement: items.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: item.name,
      item: item.url,
    })),
  };
}

// Not Another SundayのVenueページの使用:
<JsonLd
  data={buildBreadcrumbSchema([
    { name: 'Home', url: 'https://notanothersunday.com' },
    { name: 'London', url: 'https://notanothersunday.com/london' },
    { name: 'Restaurants', url: 'https://notanothersunday.com/london/restaurants' },
    { name: venue.name, url: `https://notanothersunday.com/venue/${venue.slug}` },
  ])}
/>

Service

{
  "@type": "Service",
  "name": "Next.js Development",
  "description": "Custom Next.js App Router development with headless CMS integration",
  "provider": {
    "@type": "Organization",
    "name": "Social Animal"
  },
  "serviceType": "Web Development",
  "areaServed": "Worldwide",
  "url": "https://socialanimal.dev/capabilities/nextjs-development"
}

LocalBusiness

これはNot Another Sundayの137,000会場リスティングを駆動します:

function buildLocalBusinessSchema(venue: Venue) {
  return {
    '@type': venue.type === 'restaurant' ? 'Restaurant' : 'LocalBusiness',
    name: venue.name,
    description: venue.description,
    image: venue.images[0],
    address: {
      '@type': 'PostalAddress',
      streetAddress: venue.address,
      addressLocality: venue.city,
      postalCode: venue.postcode,
      addressCountry: venue.country,
    },
    geo: {
      '@type': 'GeoCoordinates',
      latitude: venue.lat,
      longitude: venue.lng,
    },
    url: venue.website,
    telephone: venue.phone,
    priceRange: venue.priceRange,
    aggregateRating: venue.reviewCount > 0 ? {
      '@type': 'AggregateRating',
      ratingValue: venue.rating,
      reviewCount: venue.reviewCount,
    } : undefined,
  };
}

Product

{
  "@type": "Product",
  "name": "Headless CMS Development Package",
  "description": "Complete headless CMS setup with content modeling and API integration",
  "offers": {
    "@type": "Offer",
    "price": "5000",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "url": "https://socialanimal.dev/pricing"
  }
}

HowTo

{
  "@type": "HowTo",
  "name": "How to Add Schema Markup to Next.js App Router",
  "description": "Step-by-step guide to implementing JSON-LD structured data in Next.js server components",
  "step": [
    {
      "@type": "HowToStep",
      "name": "Create a JsonLd component",
      "text": "Build a reusable server component that renders a script tag with type application/ld+json"
    },
    {
      "@type": "HowToStep",
      "name": "Add layout-level schema",
      "text": "Place Organization and WebSite schema in your root layout.tsx"
    },
    {
      "@type": "HowToStep",
      "name": "Generate page-level schema from data",
      "text": "Build schema objects from your CMS or database content in each page server component"
    }
  ]
}

Person

Deluxe Astrologyのセレブリティプロフィール用に使用:

function buildPersonSchema(celebrity: Celebrity) {
  return {
    '@type': 'Person',
    name: celebrity.name,
    description: celebrity.bio,
    image: celebrity.photo,
    birthDate: celebrity.birthDate,
    birthPlace: celebrity.birthPlace ? {
      '@type': 'Place',
      name: celebrity.birthPlace,
    } : undefined,
    nationality: celebrity.nationality,
    url: `https://deluxeastrology.com/celebrities/${celebrity.slug}`,
    sameAs: celebrity.externalLinks || [],
  };
}

プログラマティックページの動的スキーマ

ここからが面白くなります。Supabaseの行によってサポートされている91,000+のページがある場合、データベースレコードを人間の介入なしに有効なJSON-LDに変える必要があります。

ここに実装してみましょう:

// app/[lang]/horoscope/[sign]/[period]/page.tsx
import { createClient } from '@/lib/supabase/server';
import { JsonLd } from '@/components/json-ld';

export async function generateStaticParams() {
  const supabase = createClient();
  const { data: pages } = await supabase
    .from('horoscope_pages')
    .select('lang, sign, period');

  return (pages || []).map((p) => ({
    lang: p.lang,
    sign: p.sign,
    period: p.period,
  }));
}

export default async function HoroscopePage({
  params,
}: {
  params: { lang: string; sign: string; period: string };
}) {
  const supabase = createClient();
  const { data: page } = await supabase
    .from('horoscope_pages')
    .select('*')
    .eq('lang', params.lang)
    .eq('sign', params.sign)
    .eq('period', params.period)
    .single();

  if (!page) return notFound();

  const articleSchema = {
    '@type': 'Article',
    headline: page.title,
    description: page.meta_description,
    datePublished: page.published_at,
    dateModified: page.updated_at,
    inLanguage: page.lang,
    author: {
      '@type': 'Organization',
      name: 'Deluxe Astrology',
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://deluxeastrology.com/${page.lang}/horoscope/${page.sign}/${page.period}`,
    },
  };

  const faqSchema = page.faqs?.length
    ? {
        '@type': 'FAQPage',
        mainEntity: page.faqs.map((faq: any) => ({
          '@type': 'Question',
          name: faq.q,
          acceptedAnswer: {
            '@type': 'Answer',
            text: faq.a,
          },
        })),
      }
    : null;

  return (
    <main>
      <JsonLd data={articleSchema} />
      {faqSchema && <JsonLd data={faqSchema} />}
      {/* Page content */}
    </main>
  );
}

ここでの重要なアーキテクチャ上の決定:

  1. スキーマはSSG経由でビルド時に生成されますgenerateStaticParamsはすべての91,000+のパスを作成し、各ページのスキーマは静的HTMLにベイクされます。
  2. Supabase行=スキーマデータ — データベースは単一の真実の源です。表示されているものとスキーマに含まれているものの間にコンテンツドリフトはありません。
  3. ページあたり複数のスキーマブロック — Googleは明示的に複数のJSON-LDスクリプトタグをサポートしています。同じページでArticle、FAQPage、およびBreadcrumbListに対して別のブロックを使用します。
  4. 鮮度のためのISRrevalidate = 3600を設定して、ページが完全な再デプロイなしで毎時間再構築されるようにします。

HostListの25,000の企業プロフィール場合、同じパターンが適用されますが、各企業のSupabaseの行から生成されたOrganizationスキーマを使用します。Not Another Sundayの137,000会場の場合、LocalBusinessです。

inLanguageを使用した多言語スキーマ

Deluxe Astrologyは30言語で実行されます。すべてのスキーマブロックにinLanguageが含まれ、hreflang認識URLを使用します:

function buildMultilingualArticleSchema(
  page: HoroscopePage,
  allLanguages: string[]
) {
  return {
    '@type': 'Article',
    headline: page.title,
    description: page.meta_description,
    inLanguage: page.lang,
    datePublished: page.published_at,
    dateModified: page.updated_at,
    author: {
      '@type': 'Organization',
      name: 'Deluxe Astrology',
    },
    // 翻訳について検索エンジンに伝える
    workTranslation: allLanguages
      .filter((lang) => lang !== page.lang)
      .map((lang) => ({
        '@type': 'Article',
        inLanguage: lang,
        url: `https://deluxeastrology.com/${lang}/horoscope/${page.sign}/${page.period}`,
      })),
  };
}

inLanguageプロパティはBCP 47言語タグ(enfrdejaなど)を使用します。これは多言語サイトに必須です。それがなければ、Googleは構造化データの言語を誤識別し、それを間違った聴衆に提供する可能性があります。

検証および監視ツール

検証なしにスキーマを出荷することは、テストなしでデプロイするようなものです。ここが当社のツールキットです:

ツール 目的 コスト 使用タイミング
Google Rich Results Test リッチ結果適格性の検証 無料 デプロイ前、スポットチェック
Schema Markup Validator 完全なschema.org仕様検証 無料 Googleのツールが無視するプロパティエラーをキャッチ
Screaming Frog Custom Extraction サイトをクロール、すべてのページからJSON-LDを抽出 £199/年(有料ライセンス) 91K+ページ全体にわたる一括検証
Google Search Console インデックス付きスキーマを監視、エラーをサーフェス 無料 継続的な本番環境監視
Rich Results Status reports 有効/無効なスキーマを持つページを表示 無料 週次レビュー

スケーション用のScreaming Frogカスタム抽出スキーマ

これは、各ページを手動でチェックせずに91,000ページを検証する方法です。Screaming Frogで:

  1. 構成 → カスタム → 抽出に進む
  2. CSSPathを使用したカスタム抽出を追加:script[type="application/ld+json"]
  3. 抽出を「内側のHTMLを抽出」に設定
  4. サイトをクロール
  5. エクスポートしてJSONをプログラムで検証

スキーマタイプごとに必須プロパティのエクスポートをNode スクリプトを通して実行し、Googleが実行する前に、空のheadlineフィールドや間違った形式の日付など、必須プロパティまたは形式が悪いデータを持つページにフラグを立てます。

豊富な検索結果をダメにする一般的な間違い

これらのほとんどを作成しました。当社の痛みから学びましょう。

1. スキーマコンテンツが表示されるコンテンツと一致していません。 Article schema が見出しが「ロンドンの最高のレストラン」と言っていても、実際の<h1>が何か異なることを言っている場合、Googleはスキーマを無視するか、罰することになります。データはページの内容を反映する必要があります。

2. ページが対応していないスキーマタイプを使用します。 ページが実際に FAQ コンテンツを表示していないページに FAQPage スキーマをスラップしないでください。Googleの手動操作チームはこれを検出し、ペナルティはすべてのリッチ結果を削除します。違反するページだけではなく、すべてのリッチ結果を削除します。

3. 必須プロパティがありません。 Articleはheadlineimageが必要です。LocalBusinessにはnameaddressが必要です。タイプごとの要件については、Google構造化データドキュメントを確認してください。

4. クライアント コンポーネントでスキーマをレンダリングします。 Next.js App Routerでは、JSON-LDを'use client'コンポーネント内にレンダリングする場合、最初のHTMLには含まれません。Googlebot通常はJSを実行しますが、他のクローラー(一部のLLMクローラーを含む)はそうしません。常にサーバーコンポーネントを使用してください。

5. ネストされたレイアウト全体でスキーマを複製します。 ルートlayout.tsxとネストされたlayout.tsxの両方がOrganizationスキーマをレンダリングする場合、重複が発生します。各スキーマタイプを最も具体的な適切なレベルに配置することでのみ重複排除します。

6. JSON内の特殊文字をエスケープしていません。 記事のタイトルまたはFAQの回答にエスケープされていない引用符または角括弧が含まれている場合、JSONはサイレントに壊れます。JSON.stringify()はほとんどのケースを処理しますが、ユーザー生成データから引き出されたコンテンツに注意してください。

7. 廃止予定またはサポートされていないスキーマタイプを使用します。 次のセクションを参照してください。

Google 2025-2026の廃止予定と変更

Googleは、リッチ結果をトリガーするスキーマタイプを厳密にしてきました:

  • FAQPage豊富な結果が削除されました(2023年8月、まだ有効): 政府および健康当局のサイトのみが現在SERPにFAQリッチ結果を取得します。しかし、これが重要です — Googleは引き続きFAQPageスキーマを読み込み、処理しています。ほとんどのサイトで検索結果に拡張FAQを表示しないだけです。LLM引用目的の場合、スキーマはまだゴールドです。
  • HowToリッチ結果がモバイルから削除されました(2023年9月、まだ有効): デスクトップはまだそれらをときどき表示しますが、Googleは大幅にHowToリッチ結果の優先順位を下げました。
  • Sitelinks Searchbox廃止予定(2024年11月): WebSiteスキーマのSearchActionはもはや sitelinks searchbox を保証しませんが、Googleは内部的にそれを使用する可能性があります。
  • AI Overviews優先度構造化データ(2025-2026): GoogleのAI Overviewsは、構造化データを持つページからますます引き出されています。スキーマは含包を保証しませんが、それなしのページは測定可能に引用される可能性が低くなります。

当社の推奨:FAQPage、HowTo、およびすべてのスキーマタイプを実装し続けてください。Googleの SERP 機能が削減されたとしてもです。データは複数のシステムによって消費されます — Google's AI、ChatGPT's browse mode、Perplexity、Bing Copilot。値は従来のリッチ結果をはるかに超えています。

ヘッドレスサイトを構築していて、この規模での実装を支援してほしい場合は、ヘッドレスCMS開発機能をチェックするか、お問い合わせください

FAQ

2026年のFAQPageスキーマはSEOに適していますか? はい。ただし、前とは異なる方法です。Googleは2023年にほとんどのサイトのFAQリッチ結果を削除しました。そのため、検索結果には拡張FAQスニペットが表示されません。ただし、Googleは内部的にスキーマを引き続き処理し、ChatGPT、Perplexity、およびGoogleのAI Overviewsなどの LLM搭載検索ツールはアクティブにFAQPageマークアップからQ&Aペアを抽出します。FAQPageスキーマなしのページと比較して、FAQPageスキーマを持つページのLLM引用で4倍の増加が測定されています。

Next.js App RouterでJSON-LDスキーママークアップを追加するにはどうすればよいですか? application/ld+json型の<script>タグをレンダリングし、JSON.stringify()をスキーマオブジェクトで使用するサーバーコンポーネントを作成します。ページのサーバーコンポーネント内に配置します。クライアントコンポーネント内には配置しないでください。サイト全体のスキーマ(Organization)の場合は、layout.tsxに配置します。ページ固有のスキーマ(ArticleやFAQPageなど)の場合は、各page.tsxでデータから生成します。

1ページに複数のJSON-LDスクリプトタグを使用できますか? 完全にそうです。Googleは明示的に1ページに複数のJSON-LDブロックをサポートしています。同じページでArticle、FAQPage、BreadcrumbList、およびOrganizationの個別ブロックを定期的にレンダリングします。各ブロックは独自の@contextを持つ独自の<script type="application/ld+json">タグを取得します。

プログラマティックページの数千のスキーママークアップを生成するにはどうすればよいですか? サーバーコンポーネントのデータベース行からスキーマオブジェクトを構築します。Next.jsでgenerateStaticParamsを使用してすべてのページのパスを作成し、各ページのサーバーコンポーネントはSupabaseからデータをフェッチしてJSON-LDを動的に構築します。スキーマはビルド時に静的HTMLにベイクされます。91,000ページの場合、これはISRがアップデートを処理するビルドプロセス中に実行されます。

ArticleとBlogPostingスキーマの違いは何ですか? BlogPostingはArticleのサブタイプです。明確な公開日と著者のあるブログポストにはBlogPostingを使用してください。より一般的な編集コンテンツ(ニュース記事やガイドなど)にはArticleを使用してください。実際には、Googleはそれらをほぼ同じに扱います。ほとんどのコンテンツに対してはArticleを使用し、明示的にブログ形式のポストに対してのみBlogPostingを使用します。

スキーママークアップはGoogle AI Overviewsに役立ちますか? はい。構造化データを含むページは、AI Overviewsで引用される可能性が大幅に高くなります。スキーマはGoogleのAIがエンティティ関係、コンテンツタイプ、およびデータ精度を理解するのに役立ちます。FAQPageスキーマは特に効果的です。AIが直接抽出できる事前構造化されたQ&Aペアを提供するためです。これは包含の保証ではありませんが、オッズを大幅に改善します。

どのツールを使用してスキーママークアップを大規模に検証する必要がありますか? 個々のページについては、GoogleのRich Results TestとSchema Markup Validator at validator.schema.orgを使用してください。数千ページ全体にわたる一括検証では、Screaming FrogのカスタムExtractionフィーチャーを使用してサイトをクロールしてすべてのJSON-LDを抽出し、検証スクリプトを通して出力を実行します。Google Search Consoleの構造化データレポートで継続的な問題を監視します。

Googleがもはやリッチ結果を表示しなくなったスキーマタイプを実装する必要がありますか? はい。Googleの SERP 機能は、構造化データの単なる1つのコンシューマーです。ChatGPT、Perplexity、Bing Copilot、およびその他のAIシステムはすべてスキーママークアップを読みます。GoogleがモバイルでのHowToリッチ結果の表示を停止した場合でも、スキーマは引き続きLLMがコンテンツを理解するのに役立ちます。構造化データを単なるGoogle機能ではなく、ユニバーサルなマシンリーダブルレイヤーと考えてください。