2024年初頭、ある大規模州立大学がよくある問題を抱えて私たちのもとに来ました。Drupal 7のインストール環境がサポート終了を迎えようとしており、学生ポータルは登録期間中に負荷で悲鳴を上げており、ウェブサイト上で最も重要なコンバージョンツールであるプログラムファインダーは検索結果を返すのに8秒以上かかっていました。40,000人のアクティブな学生、200以上の学位プログラム、そしてDrupal 7のセキュリティサポートが実質的に終了するまで残された6ヶ月間がありました。プレッシャーはかかっていました。

これは、私たちがすべてをNext.jsとヘッドレスCMSバックエンドに移行し、ページロード時間を73%削減し、予定通りに納品した話です。私たちが下した建築上の決定(そして間違えそうになった決定)、実際の移行プロセス、パフォーマンスベンチマーク、そして大規模なCMS移行に適用できる教訓を共有します。

目次

ケーススタディ:大学ポータルをDrupalからNext.jsに移行

スタート地点:扱っていたもの

状況を説明しましょう。大学のデジタルプレゼンスはDrupal 7上に構築されており、もともと2014年頃にローンチされました。この10年間で、以下が蓄積していました:

  • 約12,000のコンテンツノード(プログラム、コース、教員プロフィール、ニュース記事、イベント)
  • 200以上の学位プログラムページ(学位レベル、学部、カレッジ、提供形式、認定ステータスなどの複雑なタクソノミー関係を持つ)
  • カスタムプログラムファインダー(Drupal Viewsベースの検索であるため機能しているが遅い)
  • 学生ポータル(認証アクセス、アドバイジングツール、学位監査、登録リンク、パーソナライズされたダッシュボード)
  • 47個のカスタムDrupalモジュール(うち19個はもはやメンテナンスされていない)
  • 3つの異なるテーマレイヤー(連続する再デザインの上に重ねられたもの)

このサイトは、機関用ロードバランサーの背後にある2つの老朽化した仮想マシンでホストされていました。ピーク登録期間(8月と1月)には、プログラムファインダーが定期的にタイムアウトしていました。マーケティングチームは、プログラムのPDFリストをバックアップとして投稿することに頼るようになっていました。これがすべてを物語っています。

Core Web Vitalsは厳しい状況でした:

メトリクス Drupal 7(変更前) ターゲット
LCP 6.2秒 < 2.5秒
FID 380ms < 100ms
CLS 0.31 < 0.1
TTFB 2.8秒 < 0.8秒
プログラムファインダー読み込み 8.4秒 < 1.5秒

ステークホルダーランドスケープ

大学のウェブプロジェクトはステークホルダー数が多いため、ユニークに課題があります。私たちは以下の関係者と協力していました:

  • 中央IT — SSO統合、セキュリティコンプライアンス、ホスティングに責任
  • マーケティング&コミュニケーション — ブランド、コンテンツ戦略、分析に責任
  • 登録局 — プログラムデータと学生情報システム(SIS)に責任
  • 個々のカレッジ&学部 — 独自のコンテンツエディタ(80人以上のCMSアクセス権を持つ)
  • 学生自治会 — モバイルファーストデザインを強く支持(当然です)

これらすべてのグループから同意を得るのに、プロジェクトの最初の3週間かかりました。デザインスプリントを実施して、共有の優先事項と非交渉項目を確立しました。

Next.jsを選んだ理由(そしてDrupal 10を選ばなかった理由)

当然の疑問:Drupal 10にアップグレードしないのはなぜですか?大学のITチームは、実は私たちに連絡する6ヶ月前にその経路を始めていました。47個のカスタムモジュールのうち23個にDrupal 10の同等物がなく、完全に書き直す必要があることを発見した後で放棄しました。

実際の計算は以下のようなものでした:

要因 Drupal 10移行 Next.js再構築
予想される時間 8~10ヶ月 6ヶ月
カスタムモジュール再実装 23モジュール なし(APIとコンポーネントとして再構築)
コンテンツエディタ再トレーニング 中程度(新しい管理UI) 中程度(新しいCMS)
パフォーマンス上限 中程度の改善 劇的な改善
ホスティング柔軟性 従来のLAMP/同等 エッジデプロイメント、CDN最優先
開発者採用プール 縮小中(Drupal専門家) 拡大中(React/Next.js)
長期メンテナンスコスト 約$180K/年 約$95K/年

メンテナンスコストの差は経営陣にとってのゲームチェンジャーでした。機関的経験を持つDrupal開発者は見つけるのが難しくなり、維持費が高くなっていました。大学のITチームは、シニアDrupal開発者の退職後、3人のReact開発者とゼロのDrupal専門家を抱えていました。

Next.jsを具体的に選びました(Gatsby、Remix、Astroではなく)いくつかの理由があります:

  1. ハイブリッドレンダリング — プログラムページは静的に生成できますが、学生ポータルは認証を備えたサーバーサイドレンダリングが必要
  2. APIルート — 別のバックエンドサービスなしでSIS統合用のミドルウェアを構築できます
  3. 増分静的再生成(ISR) — プログラムデータは週単位で変更され、時間単位ではありません。1時間の再検証ウィンドウを持つISRは完璧でした
  4. 大学チームはReactを知っていました — ハンドオフ後、彼らはこれを保守することになります

同様のオプションを検討している場合、Next.js開発機能ページで、私たちが通常構築する技術仕様をカバーしています。

アーキテクチャの決定

ヘッドレスCMS選択

大学の要件に対して5つのヘッドレスCMSオプションを評価しました:80以上のコンテンツエディタ、複雑なコンテンツ関係、ロールベースの権限、合理的なシートあたり価格モデル。

私たちはSanityを選びました。主要な要因は:

  • GROQクエリは、このユースケースではプログラム、学部、カレッジ間の複雑なタクソノミー関係をGraphQLよりもはるかに良く扱う
  • リアルタイムコラボレーション — 複数のエディタが同時に競合なしで作業できる
  • カスタム入力コンポーネント — プログラム前提条件マッパーをスタジオに直接構築しました
  • 価格設定 — Enterprise プランは約$949/月で予算内でした。ユーザーあたりコストは予測可能でした

コンテンツモデリングは約2週間かかりました。14のドキュメント型と8つのリファレンス型を定義しました。プログラムスキーマだけで34フィールドがあり、schema.org EducationalOrganizationCourseマークアップ用の構造化データを含みました。

CMSアーキテクチャに関する当社のアプローチについての詳細は、ヘッドレスCMS開発ページを参照してください。

インフラストラクチャ

Next.jsフロントエンド用にVercel上にデプロイしました(FERPAコンプライアンスとSSO要件に必要なEnterpriseプラン)。学生ポータルの認証ルートはサーバーサイドレンダリングを使用し、大学の既存CAS(Central Authentication Service) SSOを通じたセッション管理があります。

データフローは次のようになります:

[Sanity CMS] → [Vercel上のNext.js] → [CDN エッジ]
                    ↕
           [大学 SIS API]
                    ↕
           [CAS SSO / LDAP]

静的プログラムページはビルド時に事前レンダリングされ、ISR経由で1時間ごとに再検証されます。プログラムファインダーはビルド時に事前フェッチされたデータ(JSONインデックスとしてクライアントに読み込まれる)とリアルタイムフィルタリングの組み合わせを使用します — 検索操作にはサーバーの往復は不要です。

APIレイヤー

学生情報システム(Ellucian Banner、curiosityの場合 — それはいつもBannerです)はSOAP APIを公開していました。はい、2024年にです。Next.js APIルートを使用して翻訳レイヤーを構築しました。このレイヤーはSOAPエンドポイントを消費し、フロントエンドにクリーンなRESTエンドポイントを公開します:

// /app/api/programs/[programId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fetchFromBanner } from '@/lib/banner-client';
import { transformProgramData } from '@/lib/transforms';

export async function GET(
  request: NextRequest,
  { params }: { params: { programId: string } }
) {
  const bannerData = await fetchFromBanner(
    'PROGRAM_DETAIL',
    { programCode: params.programId }
  );
  
  const program = transformProgramData(bannerData);
  
  return NextResponse.json(program, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
    },
  });
}

この翻訳レイヤーはプロジェクトの最高価値のピースの1つでした。フロントエンドをBannerの癖から切り離し、将来のプロジェクトに使用できるクリーンなAPIを大学に提供しました(モバイルアプリはすでに議論されていました)。

ケーススタディ:大学ポータルをDrupalからNext.jsに移行 - アーキテクチャ

プログラムファインダー:コア機能の再構築

プログラムファインダーはサイト全体で最も重要なページでした。分析では、すべてのオーガニック検索トラフィックの34%を占め、見込み学生の#1入口ポイントでした。これを誤ることはできませんでした。

古いアプローチ(そして遅い理由)

Drupalバージョンはフィルタを公開したViewsを使用していました。フィルタの変更のたびに、フルサーバーラウンドトリップがトリガーされ、データベースが再クエリされ、ページ全体が再レンダリングされました。200以上のプログラムと6つのタクソノミー次元(学位レベル、カレッジ、学部、提供形式、関心分野、キーワード検索)があるため、クエリは高コストでした。

新しいアプローチ

ビルド時に検索インデックスを事前構築しました。200以上すべてのプログラムが180KBのJSONファイルにシリアル化され(gzipで22KBに圧縮)、ページに付属しています。フィルタリングはカスタムフックを使用してクライアント側で完全に発生します:

// hooks/useProgramSearch.ts
import { useMemo, useState } from 'react';
import Fuse from 'fuse.js';
import { Program, ProgramFilters } from '@/types';

const fuseOptions = {
  keys: [
    { name: 'title', weight: 0.4 },
    { name: 'description', weight: 0.2 },
    { name: 'keywords', weight: 0.3 },
    { name: 'department', weight: 0.1 },
  ],
  threshold: 0.3,
};

export function useProgramSearch(programs: Program[]) {
  const [filters, setFilters] = useState<ProgramFilters>({});
  const fuse = useMemo(() => new Fuse(programs, fuseOptions), [programs]);

  const results = useMemo(() => {
    let filtered = programs;

    if (filters.degreeLevel) {
      filtered = filtered.filter(p => p.degreeLevel === filters.degreeLevel);
    }
    if (filters.college) {
      filtered = filtered.filter(p => p.college === filters.college);
    }
    if (filters.deliveryFormat) {
      filtered = filtered.filter(p => 
        p.deliveryFormats.includes(filters.deliveryFormat!)
      );
    }
    if (filters.searchQuery) {
      const fuseResults = fuse.search(filters.searchQuery);
      const fuseIds = new Set(fuseResults.map(r => r.item.id));
      filtered = filtered.filter(p => fuseIds.has(p.id));
    }

    return filtered;
  }, [programs, filters, fuse]);

  return { results, filters, setFilters };
}

Fuse.jsをファジー検索に使用し、ファセットには単純なJavaScriptフィルタリングを使用しました。結果:検索結果が50ms以下で表示されます。ローディングスピナーなし。サーバー呼び出しなし。ユーザーはフィルタを必要なだけ素早く変更できます。

各プログラム結果は、完全なschema.orgマークアップを持つ静的に生成された詳細ページにリンクしており、大学のGoogle教育関連検索機能での表示を劇的に改善しました。

学生ポータルの移行

学生ポータルは最も厄介な部分でした。認証、パーソナライズ、Bannerからのリアルタイムデータが必要でした。その一部を静的に生成することはできませんでした。

認証フロー

大学はすべての機関システム間でシングルサインオンにCASを使用しています。CASをNext.jsと統合しました:

  1. 認証されていないユーザーが/portalにアクセス → CASログインにリダイレクト
  2. CASはサービスチケット付きでリダイレクトバック
  3. APIルートはCASサーバーに対するチケットを検証
  4. httpOnlyクッキーに保存された署名済みJWTを作成
  5. 後続のリクエストはセッション管理にJWTを使用

当時メンテナンスされたCASプロバイダが存在しなかったため、next-auth(現在のAuth.js)をカスタムCASプロバイダで使用しました。

ポータル機能

学生ポータルには以下が含まれていました:

  • パーソナライズされたダッシュボード(今後の登録日、ホールド、アドバイザー情報付き)
  • 学位監査概要(Bannerからリアルタイムで取得)
  • クイックリンク(LMS(Canvas)、メール、図書館システムへのリンク)
  • プログラム固有のリソース(学生の専攻に基づく)

すべてのポータルページはサーバーサイドレンダリングを使用します。Bannerシステムに過負荷をかけないため、Bannerはじっくり出すのではなく早めにAPI応答をキャッシュします(ほとんどのエンドポイントは30秒TTL、学位監査は5分TTL)。

コンテンツ移行戦略

12,000コンテンツノードをDrupalからSanityに移行するには、体系的なアプローチが必要でした。カスタム移行パイプラインを構築しました:

# 簡略化された移行パイプライン
1. Drupalノード → JSON(カスタムDrushコマンド経由でエクスポート)
2. JSON → Sanityドキュメント形式(Node.jsスクリプト経由で変換)
3. メディアファイル処理 → Sanity CDNにアップロード
4. ドキュメントのインポート → Sanity Migration API
5. 検証 → 破損した参照のための自動チェック

メディア移行は最も退屈な部分でした。Drupalのファイル管理は内部パスとデータベース参照でファイルを保存します。スクリプトを記述しました:

  1. Drupalファイルディレクトリからすべてのファイルをダウンロード
  2. Sanityのアセットパイプラインにアップロード
  3. 古いDrupalファイルIDを新しいSanityアセット参照にマッピング
  4. 新しいアセット参照を指すようにすべてのリッチテキストコンテンツを更新

このスクリプトはフルデータセットで約14時間実行されました。プロジェクト中に3回実行しました:初期テスト用、中間ポイントでステージングをリフレッシュするため、最終カットオーバー用です。

コンテンツフリーズ戦略

2段階のコンテンツフリーズを実装しました:

  • 週1~20:コンテンツエディタはDrupal内で通常通り作業。週単位でスナップショットをステージングに移行。
  • 週21~23:デュアルエントリ。新しいコンテンツはDrupalとSanityの両方に入力。新しいCMSでエディタのトレーニング。
  • 週24:フルカットオーバー。Drupalは読み取り専用になり、その後オフラインになります。

デュアルエントリ期間は苦痛でしたが、必要でした。80以上のエディタがいて、唯一のオプションになる前にSanityで筋肉記憶を構築する必要がありました。

6ヶ月のタイムライン

フェーズ 主なデリバラブル
1ヶ月目 発見とアーキテクチャ ステークホルダー調整、CMS選択、インフラストラクチャセットアップ、コンテンツモデリング
2ヶ月目 コア開発 デザインシステム、ページテンプレート、プログラム詳細ページ、ナビゲーション
3ヶ月目 プログラムファインダー&検索 検索インデックス、フィルタリングUI、プログラムデータパイプライン、SEOマークアップ
4ヶ月目 学生ポータル CAS統合、Banner APIレイヤー、ダッシュボード、学位監査表示
5ヶ月目 コンテンツ移行&トレーニング 移行スクリプト、エディタトレーニング(6セッション)、ステージングQA
6ヶ月目 QA、パフォーマンス、ローンチ ロードテスト、アクセシビリティ監査、コンテンツフリーズ、DNSカットオーバー

私たちのチームは4人の開発者、1人のデザイナー、1人のプロジェクトマネージャーでした。大学は専任のプロダクトオーナーとBanner/CAS統合作業用のITリエゾンを提供しました。

2つの大きな課題に直面しました:

  1. 3ヶ月目:Bannerのソープアピはドキュメント化されていない1分あたり100リクエストのレート制限がありました。プログラムファインダーはビルド中にすべてのプログラムデータをバッチフェッチするように設計されました。キューイングシステムを実装し、複数のバッチにビルドを分散する必要がありました。

  2. 5ヶ月目:アクセシビリティ監査で34のWCAG 2.1 AA違反が見つかりました。ほとんどはデザインから継承されました(セカンダリボタンの色コントラスト不足、プログラムファインダーフィルタのフォーカスインジケータなし)。予定外に8日間を修復に費やしました。

パフォーマンス結果

ローンチ後の数字は以下のようになりました:

メトリクス Drupal 7(変更前) Next.js(変更後) 改善
LCP 6.2秒 1.1秒 82%高速化
FID / INP 380ms 45ms 88%高速化
CLS 0.31 0.02 94%改善
TTFB 2.8秒 0.12秒 96%高速化
プログラムファインダー読み込み 8.4秒 0.8秒 90%高速化
Lighthouse スコア 34 97 +63ポイント
ビルド時間(フル) N/A 4分12秒
月間ホスティングコスト ~$2,400 ~$1,100 54%削減

しかし、大学にとって最も重要な数字は以下でした:

  • プログラムファインダーの使用が156%増加(ローンチ後の最初の学期)
  • モバイルバウンス率が67%から31%に低下
  • プログラムページへのオーガニック検索トラフィックが43%増加(4ヶ月以内、schema.orgマークアップとCore Web Vitals改善により)
  • ポータルに関連するサポートチケットが62%削減 — 主にページが実際に確実に読み込まれるようになったため
  • 秋の登録期間中のダウンタイムがゼロ — 3年ぶりの初めて

学んだ教訓

1. CAS/SSO統合を早期に開始する

CAS統合を4ヶ月目にスケジュールしました。1ヶ月目から概念実証を開始するべきでした。大学のITチームはセキュリティレビューの段階的実行(遅い)を進めます。SSO建築を承認させるには、セキュリティオフィスとの3週間の往復が必要でした。

2. コンテンツモデリングはアーキテクチャである

フロントエンドコードを書く前に、2週間全体をコンテンツモデリングに費やしました。当時は遅く感じられました。これは私たちが下した単一の最高投資でした。200以上のプログラムがあり、学部、カレッジ、学位レベル、濃度、提供形式間の複雑な関係がある場合、スキーマを事前に正しく取得することで、数百時間のリファクタリングを節約できます。

3. ローンチ直前ではなく早期にエディタをトレーニングする

当初、5ヶ月目のエディタトレーニングを計画していました。プロダクトオーナーのフィードバック後、4ヶ月目に移動しました。これにより、エディタはローンチ前の2週間ではなく、6週間Sanityに慣れる時間がありました。デュアルエントリ期間中に入力されたコンテンツの品質は、このため劇的に改善されました。

4. Bannerはバナーである

Ellucian Banner(高等教育にいれば、おそらくあなたもそうです)と協力しており、API統合に追加時間を予算計上してください。ドキュメントは希薄です。SOAPエンドポイントは一貫性がなく、すべての機関が異なるようにBannerインスタンスをカスタマイズしています。当社の翻訳レイヤーは不可欠でした。

5. 初日からアクセシビリティ用に予算を確保する

5ヶ月目の34のWCAG違反はほぼ完全に予防可能でした。現在、すべてのプルリクエストでaxe-coreチェックをCIパイプラインで実行しています。公立大学向けに構築している場合、WCAG 2.1 AA準拠はオプションではありません — それは508条の下での法的要件です。

同様の移行課題に直面している場合、詳細について話し合いましょう。直接私たちに連絡するか、価格設定ページを確認してください。これらのプロジェクトをどのようにスコープしているかについて。

よくある質問

DrupalからNext.jsへの大学ウェブサイト移行にはどのくらいの時間がかかりますか? このスケールのサイト — 12,000コンテンツノード、200以上のプログラム、認証学生ポータル — 4~6人の専任チームであれば6ヶ月は現実的です。より小さい機関サイト(2,000ページ未満、ポータルなし)は通常3~4ヶ月で完了できます。タイムラインはフロントエンド構築よりも、コンテンツ移行、ステークホルダー調整、BannerやPeopleSoftなどの機関システムとの統合によって推進されます。

高等教育ウェブサイト向けの最適なヘッドレスCMSは? それはあなたの編集チームのサイズと技術的快適さによって異なります。このプロジェクトではSanityを選びました。リアルタイムコラボレーション、柔軟なコンテンツモデリング、GROQ照会言語のため。ContentfulとStoryblokも強いオプションです。非常に大きなコンテンツチーム(100以上のエディタ)を持つ大学の場合、Contentfulのワークフローと権限モデルが有利になる場合があります。より小さなチームがより多くのカスタマイズを望む場合、Sanityが勝つ傾向があります。

Next.jsは認証された学生ポータルを処理できますか? 絶対に。Next.jsはサーバーサイドレンダリング認証ページをサポートしており、App Routerのサーバーコンポーネントは、クライアントバンドルに露出することなくユーザー固有データをフェッチするのを簡単にします。Auth.jsを備えたカスタムプロバイダを使用してCAS(Central Authentication Service)と統合しました。ポータルは40,000学生をパフォーマンスの問題なく処理しました。

大学向けのDrupalからNext.jsへの移行にはいくらかかりますか? このスコープのプロジェクト — プログラムファインダー、学生ポータル、200以上のプログラム、フルコンテンツ移行、CMSセットアップ、トレーニング — 通常は$250,000から$450,000の範囲です。複雑さによって異なります。しかし、長期的な節約は大きいです。この大学は年間メンテナンスコストを約$180Kから$95Kに削減しました。予算の上限での最高価格でも、プロジェクトは3~4年以内に支払われます。

大規模なCMS移行中のSEOはどうなりますか? 正当な懸念です。包括的なリダイレクトマップ(2,400以上の301リダイレクト)を実装し、可能な限り既存のURLを保持し、Drupalサイトにはなかったschema.org構造化データを追加しました。オーガニックトラフィックはローンチ後の最初の2週間で約8%低下しました(大規模な移行では通常)。その後、4ヶ月以内にベースラインを超えて43%増加しました。

DrupalはDrupal 10にアップグレードするのに大学向けの選択肢ですか? 状況によります。強力なDrupal専門知識があり、カスタムモジュールにDrupal 10互換性があり、静的/ハイブリッドサイトのパフォーマンス特性が不要な場合、Drupal 10は完全に有効なパスです。私たちの場合、大学はDrupal専門知識を失い、23の互換性のないモジュールがあり、劇的なパフォーマンス改善を必要としていました。ヘッドレスアプローチは明らかに適切なフィットでした。

DrupalからヘッドレスCMSへのコンテンツ移行はどうやるのですか? Drushコマンド経由のDrupalコンテンツをエクスポートしたカスタムNode.jsスクリプトを使用して、新しいCMSスキーマに一致するようにデータを変換し、メディアファイル移行を処理し、CMSの移行APIを通じてすべてをインポートします。プロセスは通常3回実行されます:初期テスト用、ステージングリフレッシュ用、最終カットオーバー用。組み込みメディアを備えたリッチテキストコンテンツが最難関 — すべての内部ファイル参照を再マップする必要があります。

移行中にDrupalとNext.jsを同時に実行できますか? はい。移行中、DrupalはステージングドメインでNext.jsバージョンを構築、テストしながら、本番サイトを継続して提供していました。私たちの移行中に、コンテンツが両方のシステムに入力されていた3週間のデュアルエントリ期間を使用しました。最終カットオーバーは約15分かかったDNSスイッチで、Drupalは30日間、フォールバックとして読み取り専用モードで保つのを使用しました。