メインコンテンツへスキップ

開発

構造化データを壊さない運用

Next.js(App Router)とStrapi v5でメディアを運用するなかで、JSON-LDを破綻させない設計とCI/監査の組み立て方を解説します。

この記事の要点

  • JSON-LDは壊れてもビルドが通ります。共通シリアライザ(safeJsonLd)でscript閉じタグ混入を防ぎ、コンポーネント化して考慮を1か所に集約します。

  • Strapi v5のpublishedAtはシステム日付です。displayDate→noteマップ→publishedAtの順で解決し、構造化データの公開日と一覧の並び順をそろえます。

  • ビルドはスキーマの妥当性を検証しません。定期監査とマージ前のスキーマ検証の二層で「壊れたら落とす」仕組みを用意します。

— 01

なぜ構造化データは壊れるか

Next.js(App Router)とStrapi v5でメディアを運用すると、Article・FAQPage・BreadcrumbList・ItemListといったJSON-LDをページごとに大量に吐き出すことになります。これらは一度書けば終わりではなく、CMSのデータ構造やビルド方式が絡み合って、気づかないうちに壊れます。

JSON-LDが厄介なのは、壊れてもビルドが通ってしまう点にあります。JSON.stringifyは不正な値でも素直に文字列化しますし、npm run buildはスキーマの妥当性を一切検証しません。結果として、必須プロパティのundefined化・ISO8601非準拠の日付・解決できない@id参照・本文中のscript閉じタグ混入といった壊れ方が、ノーチェックで本番に出ます。

どれも画面表示には影響しないため、人間の目視では検知できません。だからこそ「壊れない書き方」と「壊れたら落とす仕組み」を最初から組み込む必要があります。

— 02

JSON-LDを安全に埋め込むsafeJsonLd

netsujo.jpでは、すべてのJSON-LDコンポーネントが共通のシリアライザを経由します。dangerouslySetInnerHTMLへ渡す前に、終了タグの<を含む並びをエスケープするだけのごく短い関数です。

// JSON-LDを安全にHTML内へ埋め込むためのシリアライズ関数
// </script> や </ がJSON文字列内にあるとHTMLパーサーがscriptタグを閉じてしまう問題を防ぐ
function safeJsonLd(data: Record<string, unknown>): string {
  return JSON.stringify(data).replace(/<\//g, '<\\/');
}

記事本文(excerptやdescription)にHTMLタグやコードスニペットが混ざると、エスケープなしではDOMが破壊されます。コンポーネントを必ずこの関数経由にしておけば、新しいスキーマを足すたびにこの考慮を繰り返さずに済みます。

レビューで見るべきは「共通シリアライザを通さず生のJSON.stringifydangerouslySetInnerHTMLへ渡している箇所がないか」です。

— 03

Article JSON-LDをSSGで出す

記事ページはsrc/app/blog/[slug]/page.tsxで、generateStaticParamsによるSSGを基本にしています。Strapiから記事のslugを取得し、ビルド時に静的生成します(下の例は最大100件取得。100件を超える場合はページネーションで全件取得します)。

export async function generateStaticParams() {
  try {
    const result = await getAllBlogPosts(1, 100);
    return result.data.map((post) => ({ slug: post.slug }));
  } catch {
    return []; // Strapi停止時もビルドを止めない
  }
}

catchで空配列を返している点が実運用上のポイントです。ビルド時にCMSが落ちていてもデプロイ自体は通し、個別ページはリクエスト時に解決させます。データ取得はfetchStrapinext: { revalidate }に乗っており、記事クエリはrevalidate: 3600(1時間)でISRを効かせています。ページ側にexport const revalidateは置かず、fetch単位でキャッシュ期間を制御する方式です(従来のキャッシュモデル前提。Next.js 16のCache Components有効時はrevalidateの扱いが変わるため別途確認が要ります)。

Article JSON-LDの出力では、descriptionexcerptにHTMLタグが混入するためタグ除去してプレーンテキスト化し、datePublishedは後述の理由でdisplayDateを最優先にします。authorが未指定のときはコンポーネント側で代表者のPersonノードをデフォルト出力し、画面の著者カードとJSON-LDの著者を一致させます。構造化データと表示内容の不一致は、Googleの構造化データガイドライン上も問題になり得るためです。

— 05

publishedAtマッピングの罠

ここが本記事で一番伝えたいハマりどころです。Strapi v5のpublishedAtは「CMS上でエントリを公開した日時」であり、システムが管理します。記事の本来の掲載日とは一致しません。netsujo.jpは過去にnoteへ掲載した記事をStrapiへ移行しているため、publishedAtをそのまま使うと「全記事が移行作業日に公開された」ことになってしまいます。

対策として、/tech-blogではslug単位の正しい掲載日マップを持ち、優先順位をつけて解決しています。

// note掲載日マッピング(Strapi publishedAtはシステム日付のため上書き)
const notePublishDates: Record<string, string> = {
  'ai-blockchain-trust-revolution': '2025-10-14',
  'blockchain-accounting-office': '2025-10-08',
  // ...
};

// 解決順: displayDate → noteマップ → publishedAt
const publishedAt = p.displayDate || notePublishDates[p.slug] || p.publishedAt || null;

そのうえで、この解決済みの日付で降順ソートしてから一覧を組み立てます。Article JSON-LDのdatePublishedも同じ思想でpost.displayDate || post.publishedAtの順に評価します。

publishedAtを最後のフォールバックに後退させるのが肝で、ここを逆にすると構造化データ上の公開日と一覧の並び順が食い違い、鮮度シグナルが崩れます。理想はStrapi側にdisplayDateフィールドを持たせ、ハードコードマップを段階的に消していく形です。

— 06

FAQPageは廃止後も残す理由

GoogleのFAQリッチリザルトは、2026-05-07以降Google検索結果に表示されなくなりました。ではFaqPageJsonLdを消すべきかというと、netsujo.jpでは残しています。「SERPのリッチ表示トリガー」としての役割は失われましたが、サイト内の構造化情報として、またAI検索や音声検索での参照を期待して残す判断です(後者の効果はGoogleが保証するものではありません)。

残す以上、運用方針は変えます。回答が冒頭1文で完結し、具体的な数値・条件・法律を含み、実際に検索される質問だけを残します。Yes/Noだけの空虚な回答やキーワード詰め込みのsynthetic questionは削除対象です。

— 07

CIと監査で構造化データを守る

「壊れたら落とす」側の仕組みです。netsujo.jpでは2層で守っています。

1つ目:監査スクリプト(本番への事後チェック)

npm run seo:audit:netsujoがサイトマップを辿って各ページのHTMLを取得し、script[type="application/ld+json"]を全パースします。@graphを再帰展開して@typeを収集し、構造化データの有無とスキーマ種別を一覧化します。これはビルドゲートではなく、本番に対する定期的な事後チェックの位置づけです。

2つ目:マージ前のスキーマ検証

JSON-LDコンポーネントを追加・変更するPRでは、各ブロックをschema.org仕様とGoogle Rich Results要件に照らし、必須プロパティ・日付形式・@id参照の解決・廃止タイプを点検します。とくに@idのグラフ整合性はビルドでは検知できないため、専用の検証を挟む価値があります。

なお@typeに依存しない事実整合チェック(check:facts)も別途CI化しており、禁止表現を静的走査します。構造の妥当性と内容の正しさは別の検査で守る、という分担です。

よくあるご質問

ページにexport const revalidateを書かずにISRは効きますか?

効きます。記事ページはページ単位のrevalidate指定を持たず、データ取得層のfetchStrapiがnext: { revalidate }を渡しています。記事クエリはrevalidate: 3600で、これがISRの再生成間隔になります。

なぜStrapiのpublishedAtをそのまま掲載日に使えないのですか?

Strapi v5のpublishedAtはCMS上でエントリを公開した日時で、システムが管理します。記事の本来の掲載日とは別物です。displayDateや明示的なマッピングで上書きしないと、構造化データの公開日と一覧の並び順がそろいません。

FAQPageスキーマはもう不要ですか?

リッチリザルト目的なら不要です。AI検索や音声検索での参照を期待して残す選択肢はありますが、その効果は保証されません。残す場合は、回答が冒頭で完結する質の高いQ&Aだけに絞ります。

JSON-LDをコンポーネント化する利点は何ですか?

エスケープ・@idの付け方・publisher/authorのデフォルトを1か所に集約でき、ページごとに再実装しなくて済みます。新しいスキーマを足すときも共通の考慮を繰り返さずに済みます。

ビルドが通ればJSON-LDは正しいと考えてよいですか?

いいえ。JSON.stringifyもNext.jsのビルドもスキーマの妥当性を検証しません。マージ前のスキーマ検証と定期監査を別建てで用意してください。

この記事の著者

飯田 友広

飯田 友広

代表取締役

Netsujo株式会社 代表取締役。京都発のWeb3・AI実装スタートアップを2023年6月に創業。京都府ワーキンググループ「Chain UP KYOTO」参画(2026-03-10)、京都美術工芸大学での講義・龍谷大学でのセミナー実績、ITコミュニティ「みやこでIT」(connpassメンバー592名・イベント158回以上・2019年2月から運営)運営。NPO法人NEMTUS理事、BAR KRYPTO運営。ソーシャル企業認証「S認証」取得企業(2026-02)。技術領域はWeb3/ブロックチェーン/DID/NFT/生成AI/コミュニティ運営。

プロフィールを見る

この記事が向いている方

  • Next.js App RouterとStrapi v5でメディアを運用しているWeb担当者・開発者

  • JSON-LDが壊れていないか不安で、CIや監査の組み立て方を知りたい方

  • Strapiのpublishedで掲載日がずれる問題に直面している方

— 壁打ち相談

読者のよくある相談

記事を読んだ後に「自分の状況だとどう判断すべきか」を整理するための壁打ち相談を受け付けています。下記のような相談例が当てはまる方は、お気軽にご連絡ください。

Q. 構造化データが壊れていないか、まとめて点検したいです。

サイトマップ起点の監査とマージ前のスキーマ検証で、@id参照や日付形式の壊れを洗い出す進め方を壁打ちできます。

Q. Strapiの公開日と一覧の並び順がずれています。

displayDate→マッピング→publishedAtの解決順とソート設計を、既存実装に合わせて整理します。

上記いずれかが該当する場合、初回30分の壁打ち相談で論点整理に対応します。記事に書ききれない個別事情を踏まえた判断材料が必要な段階こそ、壁打ちが活きやすいフェーズです。

関連するサービス

Web/メディア開発|Netsujo

構造化データを壊さない実装をつくる

Next.js App RouterとStrapiでのJSON-LD実装、SSG/ISR設計、スキーマ検証のCI化までご相談いただけます。要件が固まっていない段階でも構いません。

無料でWeb3導入を相談する初回相談無料・原則1営業日以内に一次返信