Next.js ブログに構造化データ(JSON-LD)を実装する方法

2026.02.07
Share:

はじめに

Next.js でブログを構築して記事も公開した。メタデータも設定した。でも、Google の検索結果に表示されたときに「なんだか地味だな」と感じたことはないでしょうか。

タイトルと説明文だけのシンプルな検索結果に対して、他のサイトには公開日や著者名、パンくずリストが表示されている。この違いを生んでいるのが「構造化データ(JSON-LD)」です。

構造化データは、ページの内容を検索エンジンに機械的に伝えるためのマークアップです。「このページはブログ記事で、タイトルはこれで、著者はこの人で、公開日はいつ」という情報を、決められたフォーマットで埋め込みます。正しく実装すれば、検索結果にリッチスニペットとして追加情報が表示される可能性があります。

この記事では、ZERONOVA LAB の Focus Blog に JSON-LD を実装した経験をもとに、Next.js App Router のブログで構造化データをどう設計・実装するかを解説します。SEO の全体像については Next.js App Router で SEO 最適化する完全ガイド で網羅的に紹介していますが、本記事ではそのなかでも「構造化データ」にフォーカスして、具体的な設計判断と実装パターンを深掘りします。

構造化データを実装しようと思ったきっかけ

Focus Blog は、フリーランス・副業クリエイター向けのコンテンツブログとして構築しました。Markdown でブログを作る方法自体は Next.js で Markdown ブログを構築する方法 で詳しく書いていますが、記事を書いて公開するだけでは SEO として十分ではありません。

2026年1月29日、Focus Blog に大幅な SEO 強化を行った日の開発日記にはこう書いています。

構造化データは SEO の基本。早めに実装しておくべき

振り返ると、ブログの初期構築段階では構造化データのことをあまり意識していませんでした。メタデータ(titledescription)は設定していたものの、JSON-LD はまだ入っていなかった。記事を公開してから「あれ、検索結果の表示がシンプルすぎるな」と気づいて、後から追加する形になりました。

後付けでも実装自体はそこまで難しくありませんでしたが、ページの種類ごとにどのスキーマを使うか、どんなフィールドを含めるかという設計判断が意外と多かった。最初から計画していれば、もっとスムーズだったと思います。

「後で SEO は対策しよう」と思って後回しにしがちですが、ブログを構築する段階で構造化データも一緒に設計しておくのがベストです。記事が増えてから対応すると、全ページに手を入れる必要が出てきます。

どのスキーマを使うか — ページ種類ごとの設計

ブログには複数の種類のページがあります。記事ページ、一覧ページ、タグページ。それぞれに適した構造化データのスキーマが異なります。

同日の開発日記にはこう記録しています。

構造化データ(JSON-LD)を追加(BlogPosting、Blog、CollectionPage、BreadcrumbList)

最終的に、以下の4つのスキーマを使い分ける設計にしました。

ページ種別メインスキーマ補助スキーマ
個別記事BlogPostingBreadcrumbList
記事一覧BlogBreadcrumbList
タグ別一覧CollectionPageBreadcrumbList

注目してほしいのは、すべてのページに BreadcrumbList を入れている点です。パンくずリストの構造化データは、Google の検索結果に直接反映されやすいスキーマのひとつです。実装コストも低いため、対応して損はありません。

スキーマ選択で迷ったこと

一覧ページのスキーマ選択では少し迷いました。BlogItemList のどちらが適切か、という問題です。最終的には、記事一覧ページには Blog、タグ別一覧ページには CollectionPage を選びました。CollectionPage はコレクション(特定の条件で集めたページ群)を表すスキーマで、タグでフィルタされた記事一覧に適しています。

絶対的な正解があるわけではなく、「このページが何を表しているか」を忠実に伝えることが重要です。

Next.js App Router での実装アプローチ

Next.js App Router で JSON-LD を埋め込む方法はシンプルです。<script type="application/ld+json"> タグをページコンポーネントの中に配置します。

ポイントは、JSON-LD の生成ロジックをページコンポーネントの外に関数として切り出すことです。こうすることで、構造化データの構造が見やすくなり、後からフィールドを追加するときも楽になります。

// JSON-LD 生成関数を定義
function generateArticleJsonLd(post: {
  title: string;
  description: string;
  date: string;
  slug: string;
}) {
  return {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    description: post.description,
    datePublished: post.date,
    // ... 他のフィールド
  };
}

この関数をページコンポーネント内で呼び出し、<script> タグとして埋め込みます。

export default function BlogPostPage() {
  const jsonLd = generateArticleJsonLd(post);

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd),
        }}
      />
      {/* ページ本文 */}
    </>
  );
}

dangerouslySetInnerHTML という名前が少し物々しいと感じるかもしれません。しかし、JSON-LD の場合はサーバー側で生成した構造化データをそのまま埋め込むだけなので、XSS のリスクは基本的にありません。ユーザー入力をそのまま埋め込む場合は注意が必要ですが、記事のメタデータを使う範囲であれば問題ないでしょう。

Server Component との相性

Next.js App Router では Server Component がデフォルトです。JSON-LD の生成は Server Component と非常に相性が良い。なぜなら、構造化データはクライアント側で動的に変更する必要がないからです。

ビルド時に記事データから JSON-LD を生成し、HTML に埋め込む。これだけで完了します。Client Component にする必要はまったくありません。

BlogPosting — 記事ページの構造化データ

ブログで最も重要な構造化データが BlogPosting です。Google が「リッチスニペット」として検索結果に追加情報を表示するために参照するスキーマです。

Claude Code で実装した BlogPosting の構造は以下のようになっています。

const articleJsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  description: post.description,
  datePublished: post.date,
  dateModified: post.updatedAt || post.date,
  author: { "@type": "Person", name: "著者名" },
  publisher: { "@type": "Organization", name: "サイト名" },
  mainEntityOfPage: {
    "@type": "WebPage",
    "@id": `https://your-site.com/blog/${post.slug}`,
  },
  image: `https://your-site.com/blog/${post.slug}/opengraph-image`,
  keywords: post.tags.join(", "),
};

フィールド選択のポイント

すべてのフィールドを入れればいいわけではありません。Google が実際にリッチスニペットに使うフィールドは限られています。

必須レベルで入れたいフィールド:

  • headline — 記事タイトル
  • datePublished — 公開日(検索結果に表示される)
  • dateModified — 更新日(Google に「この記事は最新です」と伝える)
  • author — 著者情報
  • image — OGP 画像の URL

あると望ましいフィールド:

  • publisher — サイト情報
  • mainEntityOfPage — 正規 URL
  • keywords — タグ情報

dateModified は見落としがちですが重要です。記事を更新したときに dateModified を更新すると、Google に記事の鮮度を伝えることができます。実装としては、frontmatter に updatedAt フィールドを用意しておき、なければ date(公開日)にフォールバックさせる形にしました。

image フィールドの工夫

OGP 画像を動的に生成している場合、image フィールドにはその生成 URL を指定します。静的な画像なら public/ に配置した絶対 URL で十分です。

BreadcrumbList は、検索結果でのページの階層表示に直接影響する構造化データです。実装もシンプルなので、最も費用対効果の高いスキーマと言えるかもしれません。

function generateBreadcrumbJsonLd(post: { title: string; slug: string }) {
  return {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: [
      { "@type": "ListItem", position: 1, name: "Home", item: "https://your-site.com" },
      { "@type": "ListItem", position: 2, name: "Blog", item: "https://your-site.com/blog" },
      { "@type": "ListItem", position: 3, name: post.title,
        item: `https://your-site.com/blog/${post.slug}` },
    ],
  };
}

設計のポイントは position の番号付けです。1 から始まる連番で、サイト階層のルートから現在のページまでの経路を表現します。

ページごとにパンくずが変わる

記事一覧ページと記事ページでは、パンくずの深さが異なります。

  • 記事一覧: Home > Blog(2階層)
  • 個別記事: Home > Blog > 記事タイトル(3階層)
  • タグページ: Home > Blog > タグ名(3階層)

これを各ページの generateBreadcrumbJsonLd 関数でそれぞれ定義しました。共通化しようとも思いましたが、階層構造がページごとに微妙に異なるため、あえて各ページに個別の関数を持たせる設計にしています。

ブログのページ種類はせいぜい3〜4種類なので、個別に定義しても管理負荷はほとんどありません。

Blog と CollectionPage — 一覧ページの構造化データ

個別記事だけでなく、一覧ページにも構造化データを入れています。

記事一覧ページには Blog スキーマを使いました。

const blogJsonLd = {
  "@context": "https://schema.org",
  "@type": "Blog",
  name: "ブログ名",
  description: "ブログの説明",
  url: "https://your-site.com/blog",
  publisher: {
    "@type": "Organization",
    name: "サイト名",
  },
  blogPost: {
    "@type": "ItemList",
    numberOfItems: postCount,
  },
};

タグ別一覧ページには CollectionPage を使いました。

const collectionJsonLd = {
  "@context": "https://schema.org",
  "@type": "CollectionPage",
  name: `${tag}の記事一覧`,
  description: `「${tag}」に関する記事一覧です。`,
  url: `https://your-site.com/blog/tags/${tag}`,
  numberOfItems: postCount,
  isPartOf: {
    "@type": "Blog",
    name: "ブログ名",
    url: "https://your-site.com/blog",
  },
};

CollectionPage で注目してほしいのは isPartOf フィールドです。このタグページが親ブログの一部であることを検索エンジンに伝えることで、サイト構造の理解を助けます。

タグページの SEO 効果

開発日記にはタグの粒度について悩んだ記録が残っています。

タグの粒度

  • 細かすぎると管理が大変。大きなカテゴリのみ

タグを増やしすぎると、1つのタグに紐づく記事が少なくなり、SEO 的に薄いページが量産されてしまいます。かといって大きすぎるカテゴリだと、絞り込みの意味がない。

最終的に、大きなカテゴリに絞る判断をしました。タグページに CollectionPage の構造化データを入れたうえで、記事数が十分にあるタグだけを運用する方針です。

Google Rich Results Test で検証する

構造化データを実装したら、必ず検証しましょう。Google の Rich Results Test に URL を入力すると、そのページの構造化データが正しく解析できるかチェックできます。

確認すべきポイントは3つです。エラーがないこと(必須フィールドの欠落や型の不一致)、警告への対応(推奨フィールドの追加)、プレビューの確認(リッチスニペットの表示イメージ)です。

ただし、Rich Results Test で問題がなくても、Google が必ずリッチスニペットを表示するとは限りません。「正しく実装すれば表示される可能性が高まる」程度に捉えておくのが適切です。

実装してわかったこと

構造化データは「後付けより最初から」

冒頭で述べたとおり、Focus Blog では後から Claude Code で構造化データを追加しました。結果的にうまくいきましたが、記事数が多い状態で追加すると確認作業が増えます。

ブログを構築する段階で、BlogPostingBreadcrumbList の生成関数をあらかじめ用意しておくことをおすすめします。記事ページのテンプレートに最初から組み込んでおけば、記事を追加するたびに自動的に構造化データが生成されます。

1ページに複数の JSON-LD を入れてよい

記事ページでは BlogPostingBreadcrumbList の2つの JSON-LD を埋め込んでいます。「1ページに複数入れていいのか」と不安に思うかもしれませんが、Google は公式に「1ページに複数の構造化データを含めることは問題ない」と明言しています。

それぞれ別の <script type="application/ld+json"> タグとして配置すれば、検索エンジンは正しく解析してくれます。

過剰な構造化データは逆効果になりうる

だからといって、ありとあらゆるスキーマを詰め込めばいいわけではありません。ページの実際の内容と一致しない構造化データを入れると、Google のガイドライン違反になる可能性があります。

PMとして大事にしているのは、「ページが実際に何を表しているか」を忠実に伝えるという原則です。記事ページは BlogPosting、一覧ページは BlogCollectionPage。それぞれのページの役割に合ったスキーマを選ぶこと。これが構造化データ設計の基本だと考えています。

タグの設計は構造化データにも影響する

タグ設計で迷ったことが、結果的に構造化データの設計にも影響しました。タグの粒度が細かすぎると、タグページの数が増え、それぞれに CollectionPage の構造化データを生成することになります。

記事数が少ないタグページに構造化データを入れても、検索エンジンにとっては「薄いページ」として評価されかねません。タグ設計と構造化データは、切り離して考えられない関係にあります。

まとめ

Next.js App Router のブログに構造化データ(JSON-LD)を実装する方法を、Focus Blog での実体験をもとに解説しました。

実装のポイントをおさらいします。

  1. ページ種別ごとにスキーマを選ぶ — 記事は BlogPosting、一覧は Blog、タグは CollectionPage
  2. BreadcrumbList は全ページに入れる — 費用対効果が最も高い
  3. 生成関数を切り出す — 保守性が上がり、フィールド追加も簡単
  4. Server Component で完結させる — クライアント側の処理は不要
  5. Rich Results Test で検証する — エラーと警告をゼロにする
  6. ブログ構築時に最初から入れる — 後付けより圧倒的に楽

構造化データは、実装コストの割に SEO への効果が期待できる施策です。「リッチスニペットが表示されるかどうかは Google 次第」という不確実性はありますが、正しくマークアップしておくことで、検索エンジンにページの内容を正確に伝えることができます。

Next.js App Router の SEO 対策全般については Next.js App Router で SEO 最適化する完全ガイド も合わせて参照してください。構造化データだけでなく、メタデータ、sitemap、robots.txt まで含めた包括的な解説を行っています。

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。

関連プロダクト

ZERONOVA LAB

AIネイティブ実験開発スタジオ