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

開発

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

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化しており、禁止表現を静的走査します。構造の妥当性と内容の正しさは別の検査で守る、という分担です。

— 08

仕様変更への追随と、維持の自動化

構造化データは「書いた瞬間」より「その後の維持」のほうが難しい、というのが運用してみての実感です。schema.orgやGoogleのリッチリザルト仕様は変わり、CMSのデータも記事が増えるたびに動きます。書いたときは正しかったJSON-LDが、半年後には廃止された@typeを含んでいたり、データ側の変更で必須プロパティが欠けたりします。だからnetsujo.jpでは「正しく書く」だけでなく、「変更のたびに機械が確認し、壊れていたらリリースを止める」側へ運用を寄せています。

リリース前ゲートは人手のレビューに頼りきらない

セクション07で触れた「マージ前のスキーマ検証」は、担当者の目視だけに任せると抜けます。そこで、少なくとも機械で判定できる部分は自動ゲート化しています。check:facts(禁止表現・誤日付・改名前サービス名の静的走査)は、mainへのプッシュとPRのたびに専用ワークフローで自動実行され、違反があればCIが落ちます。tsc --noEmitによる型チェックとlintも各PRのCIで回ります。「構造の妥当性(@id参照・日付形式・廃止タイプ)は人のレビューで、内容の事実整合とコードの型は機械で」という分担です。人が見るべき対象を、機械で潰せない部分だけに絞るのが狙いです。

本番に出たあとのseo:audit:netsujoは、この事前ゲートをすり抜けた構造化データの欠落や想定外のスキーマ種別を、定期的に拾い直す事後の網です。事前ゲートと事後監査を分けているのは、どちらか一方では取りこぼしが出るためです。

廃止・縮小された仕様には静かに追随する(FAQPage・Speakable)

セクション06のとおり、GoogleのFAQリッチリザルトは2026-05-07以降、検索結果に表示されなくなりました。既存ページのFaqPageJsonLdはサイト内の構造情報として残しつつ段階的に見直していますが、これから作る新規記事の方針は一段はっきりさせました。新規記事にはFAQPageのJSON-LDを付けず、FAQは可視のHTMLとして本文に残します。読者が読めるQ&Aとしての価値は変わらないので、失われたSERPトリガーのために構造化データを積み増すことはしない、という整理です。

Speakableも同じ考え方で扱いを見直しています。Speakableはもともとニュース記事の音声読み上げを想定した仕様で、日本語での対応も限定的です。日本語のBtoB記事に付ける実益は薄いと判断し、新規記事には付けない方針にしました。既存の付与分はFAQPageと同様、段階的な見直しの対象です。仕様が廃止・縮小されたときに、以前書いたものを惰性で残し続けないことも、維持の一部だと考えています。

「入れればAIに引用される」とは考えない

最後に前提の確認です。構造化データを入れればAI検索に引用される、という因果関係は確認されていません。JSON-LDはページの内容を機械が読み取りやすくするための手段であって、引用や表示を約束するものではありません。FAQPageやSpeakableを残す・付けないの判断も、「引用が増えるから」ではなく「正しく維持できる範囲か」で決めています。構造化データは、正確な情報を機械可読な形で提供するための土台として扱うのが安全です。

よくあるご質問

ページに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のビルドもスキーマの妥当性を検証しません。マージ前のスキーマ検証と定期監査を別建てで用意してください。

RESEARCH

BtoB企業のWeb営業機会損失調査 2026

国内21社の公開サイトを75項目で外部監査した調査です。判定基準と、実際に観測した状態、修正仕様まで公開しています。

調査結果と修正仕様を見る →

この記事の著者

飯田 友広

飯田 友広

代表取締役

Netsujo株式会社 代表取締役。京都発のWeb3・AI実装スタートアップを2023年6月に創業。Webサイトを営業基盤として捉え、経営・営業・検索・生成AI・コンバージョン・計測を横断して課題と改善優先順位を整理する「Netsujo SIGNAL」を設計・運営。京都ビッグデータ活用プラットフォーム参画(小規模企業会員・ベンチャー)、同プラットフォーム発のワーキンググループ「Chain Up KYOTO」参画(2026-03-10)、IVS2026サイドイベント「なぜ京都でWeb3.0ビジネスなのか」を京都府庁旧議場で開催(2026-07-02・Netsujoとして主催、企画・登壇・運営/京都府デジタル政策推進課は共催)。京都美術工芸大学での講義・龍谷大学でのセミナー実績、ITコミュニティ「みやこでIT」(connpassメンバー614名・イベント167件・2019年2月から運営)運営。NPO法人NEMTUS理事、BAR KRYPTO運営。ソーシャル企業認証「S認証」認証企業(2026年2月認証・2026年4月公表)。技術領域は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化までご相談いただけます。要件が固まっていない段階でも構いません。

Netsujoに相談する