関連企業 Testlify

Testlify - マーケティングサイトのNext.js移行

11,000ページ以上のマーケティングサイト向けに、WordPressをキャッシュファーストのNext.js 16アーキテクチャに置き換えるアーキテクチャ判断を主導し、既存のSEO評価を守るためページ単位で本番と照合検証した。

Testlifyのマーケティングサイトは11,000ページ以上ある。旧来のWordPress構成はリクエストのたびにサーバーサイドレンダリングのオーバーヘッドとプラグインのボトルネックが発生し、ページ表示速度と検索エンジンの順位に影響していた。私はサイトをNext.js 16へ移行するアーキテクチャ判断を主導し、現在も展開作業が進行中だ。

アーキテクチャの判断

  • WordPressではなくキャッシュファースト: ページは初回リクエスト時に一度だけレンダリングされ、無期限にキャッシュされる。11,000ページ以上に対して毎リクエストごとにデータベースを叩いたり、数時間かかるバッチ再構築を必要としたりしない。Payload CMSのwebhookが、実際にコンテンツ変更の影響を受けるページだけを無効化する。
  • ヘッドレスCMS統合: コンテンツはPostgresを基盤とするセルフホスト型ヘッドレスCMSであるPayload CMSで管理され、マーケティングチームがページを編集・公開できる一方、キャッシュ無効化のフックが各変更を自動的に検知する。
  • 大規模な移行: 11,000ページ以上の段階的な移行を計画・実行し、本番の実際のサイトマップに対してページ単位で検証し、全カテゴリにわたってURL構造、canonicalタグ、既存のSEO評価を維持した。

なぜ重要だったか

11,000ページ以上のサイトをWordPressからキャッシュファーストアーキテクチャに移行することで、ページ表示時間を短縮し、技術的なSEOパフォーマンスを改善し、マーケティングチームがエンジニアリングのデプロイに頼らずに更新を公開できるようになる。

進行中のオーナーシップ

私はWordPressから移行するアーキテクチャ判断を主導した。一人で作ったものではない: 2026年7月以降、およそ15〜20人のコントリビューターによる2,500件以上のコミットがあり、今月も活動があり、最後のステータス確認時点で残るステップは本番へのDNS切り替えのみだった。

なぜ古典的なJamstackではないのか

11,000ページ以上に対する分かりやすいアプローチはビルド時の静的生成だ。しかしここには適合しない: すべてのロケールとコンテンツタイプにわたるフルリビルドには何時間もかかり、コンテンツを一件編集するたびにサイト全体を再構築して公開する必要があってはならない。代わりに、動的ルート(category、author、hr-tools、hr-guide、blog、test-libraryなど)はすべて同じ規約に従う: Cache Componentsはビルド時に少なくとも一つのパラメータを必要とし、ビルド自体はデータベースクエリをゼロにしなければならないため、ビルドは各ルートタイプにつきサンプルページを一件だけ事前レンダリングする。そのルート配下の他のすべてのページは、最初の実リクエスト時にNext.js 16のCache Components上でレンダリングされ、その後は無期限にキャッシュされ続ける(cacheLifeはNext自身のINFINITE_CACHE定数に設定)。Payload CMSのafterChangeフックがその特定のページに対して署名付きの再検証エンドポイントを呼び出すまで、その状態が続く。コンテンツの変更は数秒で反映され、フルリビルドのサイクルを経ない。それでいてキャッシュは訪問者から見ればCDNのエッジキャッシュのように振る舞う。再検証エンドポイント自体はHMAC署名されており、一定時間比較でチェックされ、意図的に一度に一つのタグしか無効化できないよう制限されている。コレクション全体のタグをパージできるのは別の手動トリガー式の管理者専用パスのみで、webhookのシークレットが漏洩しても、せいぜい一ページを無効化できるだけで、サイト全体のキャッシュを一斉に無効化されることはない。

本番で見つかり、根本から修正された二つのフレームワークのバグ

ビルド中に二つの異なるNext.js 16の問題が発生し、両方とも次の担当者が同じ苦労をして再発見しないようnext.config.tsに直接記録されている。一つ目は、cacheComponentsがデフォルトで暗黙的にenablePrerenderSourceMapsを有効にしてしまい、静的ページ生成中に本番ビルドがOOMで強制終了していた問題で、Next自身のソースまで追跡し、明示的なオーバーライドで修正した。二つ目は、キャッシュの有効期限にInfinityを使うとNextのビルドワーカーに渡る際にJSONシリアライズされてnullになってしまい、エラーを出す代わりにそのルートを黙ってキャッシュなし状態に落としてしまい、発見されるまで全ロケールでプリレンダリングが壊れていた問題で、生のInfinityではなくNext自身のINFINITE_CACHE定数を使うことで修正した。

ストリーミングのエッジケースに対するSEO対策

Cache Componentsのストリーミングでは、Nextの自動notFound()はページシェルがクライアントへのストリーミングを開始した後には404のHTTPステータスをコミットできないため、そのシェルしか読まないクローラーは404を目にすることがない。この修正では、最初のフラッシュに手動でnoindexディレクティブを注入することで、ページの残りが解決する前にシェルで止まったクローラーにも正しいシグナルが伝わるようにしている。

データモデルとスケール

  • 11,000ページ以上は、単一のフラットなコンテンツソースではなく、複数のコンテンツタイプ(category、author、hr-tools、hr-guide、leadership、test-library)がnext-intlによりサポートされる全ロケールと掛け合わされて生まれている。
  • すべての本番WordPress URLは専用の監査(docs/audits/url-parity-with-production.md)でこのプロジェクトの出力と照合される: WordPressが今日提供しているURLをこのプロジェクトが404にしてしまうなら、それは失われたトラフィックであり、許容できるギャップではない。
  • ユニットテスト(Vitest)がサイトマップ生成、カタログのシードフォールバック、関連コンテンツのローテーションをカバーし、Playwrightがエンドツーエンドのフローをカバーする。

Build vs. Buy

SaaSのヘッドレスCMSではなくPostgres上のセルフホスト型Payload CMS: マーケティングサイトのためにシート課金のSaaS費用を払うのではなく、コンテンツの所有権とデータベースを自社内に保持する。自前のCDN/ISRレイヤーではなくNext.js自身のCache Componentsとタグベースの無効化: キャッシュの挙動は、このチームが自前で構築・運用しなければならないインフラではなく、フレームワークから得られる。

Stack

Next.js 16、React、TypeScript、Postgres上のPayload CMS、ローカライゼーション用のnext-intl、ユニットテスト用のVitest、エンドツーエンドテスト用のPlaywright、Vercelにデプロイ。