Next.jsにおけるスキーママークアップ:91,000ページのJSON-LD
Next.jsビルドは91,000ページをコンパイルします。各ページはJSON-LDスキーマを搭載しています — セレブリティプロフィール用のPersonマークアップ、137,000のベニュー掲載用のEventデータ、25,000の企業ページ用のOrganizationグラフ。手動更新はありません。Search Consoleのスキーマ検証エラーはありません。Googleのクローラーが到着したときのデプロイ後の問題もありません。これは3つのプロダクションプロジェクトで実装されています:Deluxe Astrology(30言語、ホロスコープ、セレブリティプロフィール)、Not Another Sunday(ベニュー掲載)、HostList(企業プロフィール)。すべてのスキーマタイプはビルド時にデータベース行から取得され、自動的に検証され、本番環境で自己監視します。以下のコードは実際に実行されているもので、理論ではなく、サニタイズされた例でもありません。しかし最初に:なぜプログラマティックなスキーマがほとんどのチームで破綻するのか、そしてそれを防ぐ3つのアーキテクチャ上の選択について。
これは「スキーママークアップとは何か」という記事ではありません。それが何かはわかっています。これは、Supabase連携Next.jsアプリを30言語でサーブするときに、構造化データをワイヤリングし始めたときに存在することを望んでいた実装ガイドです。
目次
- 2026年にスキーママークアップが重要である理由
- LLM引用の観点:機械可読ゴールドとしてのFAQPage
- Next.js App Router実装パターン
- すべてのスキーマタイプと動作するJSON-LDコード
- プログラマティックページの動的スキーマ
- inLanguageを使った多言語スキーマ
- 検証とモニタリングツール
- リッチリザルトを台無しにする一般的な間違い
- Google 2026の非推奨化と変更
- FAQ

2026年にスキーママークアップが重要である理由
Googleは1日85億以上のクエリを処理します。AI Overviewsはアメリカの検索結果の約30%に表示されるようになりました。そして実装の決定に関係のあることはこちらです:構造化データは機械がページを理解する方法です。Googleだけではなく — ChatGPT、Perplexity、Claude、そしてウェブを解析するあらゆるLLM駆動検索ツール。
ROIの場合はシンプルです:
| メトリクス | スキーマなし | スキーマあり | 観測されたデルタ | |--------|---------------|-------------|-------------------|| | SERPからのCTR | ベースライン | リッチリザルトで+25-35% | Not Another Sundayベニューページで+31% | | AI Overview含有 | 低い | 大幅に高い | FAQアノテーション付きページで3.2倍高い | | LLM引用レート | 最小限 | 測定可能 | FAQPageスキーマページはPerplexityで4倍引用 | | リッチリザルト適格性 | なし | 星、FAQ、パンくずなど | インデックス付きページの87%でアクティブ |
数十万のページを持つサイトの場合、手動スキーマは不可能です。システムが必要です。このガイドがそれを構築しています。
LLM引用の観点:機械可読ゴールドとしてのFAQPage
ほとんどのスキーマガイドがカバーしていないことはこちらです:FAQPageスキーマはLLM駆動検索エンジン用の単一で最も機械可読な形式です。ChatGPTまたはPerplexityがページをクロールするとき、明確に構造化されたQ&Aペアを探しています。FAQPageスキーマはそれらに正確にそれを提供します — NLPの抽出を必要としない事前解析された、曖昧性のない質問回答ペア。
このパターンを最初に見つけたのはDeluxe Astrologyでした。FAQPageスキーマを持つページは、それなしの同等ページの約4倍の率でPerplexityの回答で引用されていました。Q&Aペアはほぼ逐語的に抜き出されていました。
これはもはやSEO対策だけではありません。これはGenerative Engine Optimization(GEO)対策です。AI生成の回答でコンテンツがサーフェスされることを望む場合 — そうしたいはずです、なぜなら検索がそこに向かっているから — FAQPageスキーマは最高レバレッジ投資です。
Next.js App Router実装パターン
実際のコードに移りましょう。Next.js開発プロジェクト全体で一貫したパターンを使用しています:サーバーコンポーネント内でレンダリングされる再利用可能なJsonLdコンポーネント。
ベースコンポーネント
// 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 オーバーヘッド。

すべてのスキーマタイプと動作するJSON-LDコード
ここはプロダクションで使用しているすべてのスキーマタイプで、プロジェクトからの実際のパターンです。
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,
},
})),
};
}
BreadcrumbList
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のベニューページのUsage:
<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 || [],
};
}
プログラマティックページの動的スキーマ
ここが興味深いところです。91,000以上のページがSupabaseの行でサポートされている場合、人的介入なしにデータベースレコードを有効な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>
);
}
こちらの主要なアーキテクチャの決定:
- スキーマはSSG経由でビルド時に生成される —
generateStaticParamsはすべての91,000以上のパスを作成し、各ページのスキーマは静的HTMLに焼き込まれます。 - Supabase行 = スキーマデータ — データベースが単一の真実のソースです。見える内容とスキーマの内容の間にコンテンツドリフトはありません。
- ページごとの複数スキーマブロック — Googleは複数のJSON-LD script タグを明確にサポートしています。同じページ上でArticle、FAQPage、BreadcrumbListに個別ブロックを使用します。
- フレッシュさのためのISR —
revalidate = 3600を設定して、ページは完全な再デプロイなしで1時間ごとに再構築されます。
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言語タグ(en、fr、de、jaなど)を使用しています。これは多言語サイトに重要です — それなしでは、Googleはスキーマ化されたデータの言語を誤認識し、間違った視聴者に提供する可能性があります。
検証とモニタリングツール
検証なしでスキーマをシップすることは、テストなしでデプロイするようなものです。ここはツールキットです:
| ツール | 目的 | コスト | いつ使用するか |
|---|---|---|---|
| Google リッチリザルトテスト | リッチリザルトの適格性を検証する | 無料 | デプロイ前、スポットチェック |
| Schema Markup Validator | 完全なschema.org仕様検証 | 無料 | Googleのツールが無視するプロパティエラーをキャッチ |
| Screaming Frog カスタム抽出 | サイトをクロール、すべてのページからJSON-LDを抽出 | £199/年(有料ライセンス) | 91K以上のページ全体で一括検証 |
| Google Search Console | インデックス付きスキーマを監視、エラーをサーフェス | 無料 | 継続的なプロダクション監視 |
| リッチリザルト ステータスレポート | 有効/無効なスキーマを持つページを表示 | 無料 | 週一回のレビュー |
規模でのスキーマ検証のためのScreaming Frogカスタム抽出
これは各ページを手動でチェックすることなく、91,000ページを検証する方法です。Screaming Frogで:
- Configuration → Custom → Extractionに移動します
- CSSPath付きカスタム抽出を追加します:
script[type="application/ld+json"] - 抽出を「Extract Inner HTML」に設定
- サイトをクロール
- JSONを検証するためにエクスポートを解析
スキーマタイプごとに必要なプロパティをチェックし、Googleが検出する前に不正な形式データを持つページにフラグを立てるNode スクリプトを使用してエクスポートをパイプします。空のheadlineフィールドや間違った形式の日付などの問題をキャッチします。
リッチリザルトを台無しにする一般的な間違い
これらのほとんどを作りました。私たちの痛みから学んでください。
1. スキーマコンテンツが見える内容と一致しません。 Articleスキーマが見出しが「London Best Restaurants」と言っているが、実際の<h1>が違うことを言っている場合、Googleはスキーマを無視するか罰します。データは見えるものを反映する必要があります。
2. 適格でないページのスキーマタイプを使用。 FAQページがFAQコンテンツを実際に表示していないページに無視しないでください。Googleの手動アクション チームがこれをキャッチし、不適切なページだけでなく、すべてのリッチリザルトを削除するペナルティです。
3. 必要なプロパティが不足しています。 Articleにはheadlineとimageが必要です。LocalBusinessにはnameとaddressが必要です。タイプごとに要件のGoogle構造化データドキュメントをチェック します。
4. クライアントコンポーネントでスキーマをレンダリング。 Next.js App Routerでは、'use client'コンポーネント内でJSON-LDをレンダリングする場合、初期HTMLには含まれません。Googlebotは通常JavaScriptを実行しますが、他のクローラー(一部のLLMクローラーを含む)はそうしません。常にサーバーコンポーネントを使用します。
5. ネストされたレイアウト全体で重複スキーマ。 ルートlayout.tsxとネストされたlayout.tsxの両方がOrganizationスキーマをレンダリングする場合、重複が発生します。最も特定の適切なレベルでのみ各スキーマタイプを配置することにより、重複排除します。
6. JSONで特殊文字をエスケープしていない。 記事のタイトルまたはFAQ回答にエスケープされていない引用符またはサンコ括弧が含まれている場合、JSONは暗黙的に破綻します。JSON.stringify()はほとんどの場合を処理しますが、ユーザーが生成したデータから取得したコンテンツを注視します。
7. 非推奨またはサポートされていないスキーマタイプの使用。 次のセクションを参照してください。
Google 2026の非推奨化と変更
Googleはどのスキーマタイプがリッチリザルトをトリガーするかを厳しくしてきました:
- FAQPageリッチリザルトはほとんどのサイトで削除されました(2023年8月、まだ有効): 政府と保健当局サイトのみがほとんどのサイトのSERPsでFAQリッチリザルトを取得するようになりました。しかし — そしてこれは重要です — Googleは引き続きFAQPageスキーマを読み込み、処理します。単にほとんどのサイトの検索結果で展開可能なFAQを示していません。LLM引用目的のためには、スキーマはまだ金です。
- HowToリッチリザルトはモバイルから削除されました(2023年9月、まだ有効): デスクトップはそれらをしばしば示していますが、Googleは大幅にHowToリッチリザルトを優先度を下げました。
- サイトリンク検索ボックスの非推奨化(2024年11月): WebSiteスキーマの
SearchActionはもはやサイトリンク検索ボックスを保証しませんが、Googleは内部的にそれを使用する可能性があります。 - AI Overviewsが構造化データを優先します(2026): Googleの AI Overviewsはますます構造化データを持つページから引き出します。スキーマは含有を保証しませんが、それなしのページは引用される可能性が大幅に低いです。
当社の推奨:FAQPage、HowTo、およびすべてのスキーマタイプを実装し続けます。Googleの SERPの機能は減少していますが、データは複数のシステムで使用されています — Googleの AI、ChatGPTのブラウズモード、Perplexity、Bing Copilot。価値は従来のリッチリザルトをはるかに超えています。
ヘッドレス サイトを構築していて、このスケールでの実装を支援したい場合は、当社のヘッドレス CMS開発機能を確認するか、お問い合わせください。
FAQ
FAQPageスキーマは2026年のSEOに対してまだ機能しますか?
はい、ただし2023年よりも異なります。Googleはほとんどのサイトについて2023年にFAQリッチリザルトを削除したため、検索結果に展開可能なFAQスニペットは表示されません。ただし、Googleは引き続きスキーマを内部で処理し、ChatGPT、Perplexity、Googleの AI Overviewsなどの LLM駆動検索ツールはFAQPageマークアップからQ&Aペアを積極的に抽出しています。FAQPageスキーマなしのページと比較して、FAQPageスキーマのあるページで4倍のLLM引用の増加を測定しました。
Next.js App RouterでJSON-LDスキーママークアップをどのように追加しますか?
<script type="application/ld+json"> タグをレンダリングするサーバーコンポーネントを作成して、スキーマオブジェクトにJSON.stringify()を使用してdangerouslySetInnerHTMLを使用します。ページのサーバーコンポーネント内に配置します — クライアントコンポーネント内には決してありません。サイト全体のスキーマ(Organization)の場合は、layout.tsxに配置します。ページ固有のスキーマ(ArticleまたはFAQPage)の場合は、各page.tsxのデータから生成します。
1ページに複数のJSON-LD scriptタグを持つことができますか?
絶対にそうです。Googleは明確に単一ページ上の複数のJSON-LD ブロックをサポートしています。同じページで Article、FAQPage、BreadcrumbList、Organizationの個別ブロックをルーティンでレンダリングします。それぞれが独自の<script type="application/ld+json"> タグを独自の@contextで取得しています。
数千のプログラマティックページのスキーママークアップをどのように生成しますか?
サーバーコンポーネント内のデータベース行からスキーマオブジェクトを構築します。generateStaticParamsを Next.jsで使用してすべてのパスを作成してから、各ページのサーバーコンポーネントは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のリッチリザルトテストおよびValidator.schema.orgのSchema Markup Validatorを使用します。数千のページ全体での一括検証では、Screaming Frogのカスタム抽出機能を使用してサイトをクロール、すべてのページからJSON-LDを抽出し、検証スクリプトを通じてエクスポートを実行します。Google Search Consoleの構造化データレポートで進行中の問題を監視します。
Googleがリッチリザルトを表示しなくなったスキーマタイプを実装する必要がありますか?
はい。Googleの SERPの機能はスキーマ化されたデータの1つの消費者にすぎません。ChatGPT、Perplexity、Bing Copilot、その他のAI システムはすべてスキーママークアップを読みます。GoogleがモバイルでのHowToリッチリザルトの表示を停止した場合でも、スキーマはまだLLMがコンテンツを理解するのに役立ちます。構造化データを Google 機能だけでなく、普遍的な機械可読レイヤーと見なします。