你的課程查找器在 8.2 秒時超時。又一次。這是註冊週,學生門戶不堪重負 — 40,000 名本科生不停刷新同一個破損的搜尋,你的支持隊列中充滿了工單,你的註冊副總裁在實時觀看轉化率下降。Drupal 7 安全補丁在六個月後結束。你有 200 多個課程頁面,一個你的團隊幾乎不理解的舊版 CMS,以及一個不容商量的要求:零停機時間。在 2024 年初,一所大型州立大學把這個問題交給了我們。他們需要在 Drupal 7 停用前完成向 Next.js 的完整遷移 — 他們還需要課程查找器停止流失學生。以下是我們如何在 26 週內重建了整個門戶、將搜尋回應時間降至 340 毫秒,並在不讓網站離線一分鐘的情況下部署的故事。

這是我們如何將整個系統遷移到 Next.js 並採用無頭 CMS 後端、將頁面加載時間降低 73% 並按時交付的故事。我將分享我們做出的架構決策(以及我們幾乎做錯的決策)、實際的遷移過程、性能基準,以及適用於任何大規模 CMS 遷移的經驗教訓。

目錄

案例研究:從 Drupal 遷移大學門戶到 Next.js

起點:我們處理的情況

讓我描繪一下情景。這所大學的數字化存在建立在 Drupal 7 上,最初於 2014 年左右推出。在過去的十年裡,它已經累積了:

  • 約 12,000 個內容節點,涵蓋課程、課程、教職員工資料、新聞文章和活動
  • 200 多個學術課程頁面,每個都具有複雜的分類關係(學位級別、部門、學院、授課格式、認證狀態)
  • 自訂課程查找器,建立為基於 Drupal Views 的搜尋,具有公開篩選 — 功能性但速度慢
  • 具有身份驗證存取權限的學生門戶,可用於建議工具、學位審核、註冊連結和個性化儀表板
  • 47 個自訂 Drupal 模組,其中 19 個不再維護
  • 3 個不同的主題層,疊加在連續重新設計的頂部

該網站託管在機構負載均衡器後面的兩個老化虛擬機上。在高峰註冊期間(8 月和 1 月),課程查找器會定期超時。行銷團隊已經改為發佈課程 PDF 列表作為備用方案。這說明了一切。

核心網頁指標很糟糕:

指標 Drupal 7(之前) 目標
LCP 6.2 秒 < 2.5 秒
FID 380 毫秒 < 100 毫秒
CLS 0.31 < 0.1
TTFB 2.8 秒 < 0.8 秒
課程查找器加載 8.4 秒 < 1.5 秒

利益相關者景觀

大學網站專案的獨特之處在於利益相關者的數量。我們與以下部門合作:

  • 中央 IT — 負責 SSO 整合、安全合規和託管
  • 行銷與傳播 — 擁有品牌、內容策略和分析
  • 註冊辦公室 — 擁有課程資料和學生資訊系統 (SIS)
  • 個別學院與部門 — 每個都有自己的內容編輯者(超過 80 人具有 CMS 存取權限)
  • 學生會 — 大聲倡導移動優先設計(理所當然)

從所有這些小組獲得一致性花了專案的前三週。我們舉辦了一個設計衝刺,以確立共同優先事項和不容商量的事項。

為什麼選擇 Next.js(以及為什麼不選 Drupal 10)

顯而易見的問題:為什麼不直接升級到 Drupal 10?大學的 IT 團隊在聯絡我們前六個月實際上已經開始了這條路。他們在發現 47 個自訂模組中的 23 個沒有 Drupal 10 等效項且需要完全重寫後放棄了它。

實際的成本考慮如下:

因素 Drupal 10 遷移 Next.js 重建
估計時間表 8-10 個月 6 個月
自訂模組重寫 23 個模組 N/A(作為 API/元件重建)
內容編輯者重新培訓 中等(新管理 UI) 中等(新 CMS)
性能上限 溫和改進 戲劇性改進
託管靈活性 傳統 LAMP/類似 邊界部署、CDN 優先
開發人員招聘池 縮小(Drupal 專家) 增長(React/Next.js)
長期維護成本 約 $180K/年 約 $95K/年

維護成本差異對行政部門是決定性的。具有機構經驗的 Drupal 開發人員變得越來越難找到,保留成本也變得更高。大學自己的 IT 團隊在高級 Drupal 開發人員退休後有三名 React 開發人員和零 Drupal 專家。

我們特別選擇 Next.js(而不是 Gatsby、Remix 或 Astro)有幾個原因:

  1. 混合渲染 — 課程頁面可以靜態生成,而學生門戶需要使用身份驗證進行伺服器端渲染
  2. API 路由 — 我們可以為 SIS 整合構建中介軟體,而無需單獨的後端服務
  3. 增量靜態再生 (ISR) — 課程資料每週變化一次,而不是每小時。具有 1 小時重新驗證視窗的 ISR 是完美的
  4. 大學的團隊了解 React — 他們將在交接後維護此專案

如果你正在衡量類似選項,我們的 Next.js 開發能力頁面涵蓋了我們通常構建的技術細節。

架構決策

無頭 CMS 選擇

我們根據大學的要求評估了五個無頭 CMS 選項:80 多個內容編輯者、複雜的內容關係、基於角色的權限和合理的每座位定價模式。

我們在這個專案中選擇了 Sanity。關鍵因素:

  • GROQ 查詢比 GraphQL 更好地處理了課程、部門和學院之間的複雜分類關係
  • 實時協作 — 多個編輯者可以同時工作而不會有衝突
  • 自訂輸入元件 — 我們直接在工作室中構建了課程先決條件映射器
  • 定價 — 企業計劃約 $949/月在預算內,每用戶成本是可預測的

內容建模花了約兩週。我們定義了 14 個文件類型和 8 個參考類型。課程架構本身有 34 個欄位,包括 schema.org EducationalOrganizationCourse 標記的結構化資料。

有關我們的 CMS 架構方法的更多資訊,請查看我們的 無頭 CMS 開發頁面。

基礎設施

我們在 Vercel 上部署了 Next.js 前端(企業計劃,FERPA 合規性和 SSO 要求需要)。學生門戶的已驗證路由使用伺服器端渲染,並通過大學現有的 CAS(中央身份驗證服務) SSO 進行工作階段管理。

資料流如下所示:

[Sanity CMS] → [Next.js on Vercel] → [CDN Edge]
                    ↕
           [University SIS API]
                    ↕
           [CAS SSO / LDAP]

靜態課程頁面在建置時預先渲染,並通過 ISR 每小時重新驗證。課程查找器使用預先擷取資料(在建置時作為 JSON 索引載入到用戶端)和實時篩選的組合 — 搜尋操作不需要伺服器往返。

API 層

學生資訊系統(Ellucian Banner,如果你好奇的話 — 它總是 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',
    },
  });
}

這個轉換層是專案中最高價值的部分之一。它使前端與 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 篩選刻面。結果:搜尋結果在 50 毫秒內出現。沒有加載圖標。沒有伺服器呼叫。使用者可以盡可能快速地狂擊篩選器。

每個課程結果都連結到靜態生成的詳細頁面,具有完整的 schema.org 標記,這大大提高了大學在 Google 的教育相關搜尋功能中的形象。

學生門戶遷移

學生門戶是最棘手的部分。它需要身份驗證、個性化和來自 Banner 的實時資料。我們無法靜態生成任何內容。

身份驗證流程

大學在所有機構系統中使用 CAS 進行單點登入。我們使用自訂身份驗證流程將 CAS 與 Next.js 整合:

  1. 未驗證的使用者點擊 /portal → 重定向到 CAS 登入
  2. CAS 使用服務票證重定向回
  3. 我們的 API 路由根據 CAS 伺服器驗證票證
  4. 我們建立存儲在 httpOnly Cookie 中的已簽名 JWT
  5. 後續請求使用 JWT 進行工作階段管理

我們使用 next-auth(現在是 Auth.js)與我們從頭開始編寫的自訂 CAS 提供程式,因為當時不存在維護的 CAS 提供程式。

門戶功能

學生門戶包括:

  • 個性化儀表板,具有即將進行的註冊日期、持有和顧問資訊
  • 學位審核摘要,從 Banner 實時提取
  • 快速連結到 LMS (Canvas)、電子郵件和圖書館系統
  • 課程特定資源,基於學生的宣告主修科目

所有門戶頁面都使用伺服器端渲染。我們積極快取 Banner API 回應(大多數端點的 TTL 為 30 秒,學位審核的 TTL 為 5 分鐘),以避免壓倒他們的系統。

內容遷移策略

將 12,000 個內容節點從 Drupal 遷移到 Sanity 需要系統的方法。我們構建了自訂遷移管道:

# 簡化的遷移管道
1. 匯出 Drupal 節點 → 透過自訂 Drush 命令的 JSON
2. 轉換 JSON → Sanity 文件格式,透過 Node.js 指令碼
3. 處理媒體檔案 → 上傳到 Sanity CDN
4. 匯入文件 → Sanity 遷移 API
5. 驗證 → 自動檢查是否有損壞的參考

媒體遷移是最乏味的部分。Drupal 的檔案管理使用內部路徑和資料庫參考來存儲檔案。我們編寫了一個指令碼:

  1. 從 Drupal 檔案目錄下載每個檔案
  2. 上傳到 Sanity 的資產管道
  3. 將舊 Drupal 檔案 ID 對映到新 Sanity 資產參考
  4. 更新所有豐富文字內容以指向新的資產參考

這個指令碼在完整資料集上執行了約 14 小時。我們在專案期間執行了三次:一次進行初步測試,一次在中途重新整理預備環境,一次進行最後的轉換。

內容凍結策略

我們實施了兩階段內容凍結:

  • 第 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 名專案經理組成。大學提供了一名專任產品所有者加上 IT 聯絡人,負責 Banner/CAS 整合工作。

我們遇到了兩個主要問題:

  1. 第 3 個月:Banner 的 SOAP API 有一個未記錄的速率限制,每分鐘 100 個請求。我們的課程查找器被設計為在建置期間批量提取所有課程資料。我們必須實施排隊系統,並將建置分散到多個批次。

  2. 第 5 個月:無障礙稽核發現了 34 個 WCAG 2.1 AA 違規。大多數是從設計中繼承的(次按鈕上的色彩對比不足,課程查找器篩選器上缺少焦點指示符)。我們花了計畫外的 8 天進行補救。

性能成果

以下是啟動後的數字:

指標 Drupal 7(之前) Next.js(之後) 改進
LCP 6.2 秒 1.1 秒 快 82%
FID / INP 380 毫秒 45 毫秒 快 88%
CLS 0.31 0.02 更好 94%
TTFB 2.8 秒 0.12 秒 快 96%
課程查找器加載 8.4 秒 0.8 秒 快 90%
Lighthouse 分數 34 97 +63 分
構建時間(完整) N/A 4m 12s
月託管成本 約 $2,400 約 $1,100 降低 54%

但對大學最重要的數字是這些:

  • 課程查找器使用率增加 156% 在啟動後的第一個學期
  • 行動彈出率從 67% 下降到 31%
  • 課程頁面的有機搜尋流量增加 43% 在四個月內(schema.org 標記 + 核心網頁指標改進)
  • 與門戶相關的支持工單減少 62% — 主要是因為頁面實際上加載可靠
  • 秋季註冊期間零停機時間 — 三年來第一次

經驗教訓

1. 提前開始 CAS/SSO 整合

我們將 CAS 整合安排在第 4 個月。我們應該在第 1 個月開始概念驗證。大學 IT 團隊(閱讀:緩慢)有意推進安全審查。讓 SSO 架構獲得批准花了三週的往返。

2. 內容建模即是架構

我們在編寫任何前端程式碼之前花了兩週進行內容建模。當時感覺很慢。這是我們做出的最好投資。當你有 200 多個課程,其中涉及部門、學院、學位級別、濃度和授課格式之間的複雜關係時,提前讓架構正確會節省數百小時的重構。

3. 提前培訓編輯者,而不是在啟動前才培訓

我們最初計劃在第 5 個月進行編輯者培訓。在產品所有者的反饋後,我們將其移至第 4 個月。這使編輯者有六週時間來適應 Sanity,而不是兩週。雙重輸入期間輸入的內容品質因此大大提高。

4. Banner 就是 Banner

如果你正在使用 Ellucian Banner(如果你在高等教育領域,你可能會),請為 API 整合預留額外時間。文件很稀疏,SOAP 端點不一致,每個機構都以不同的方式自訂了他們的 Banner 實例。我們的轉換層是必不可少的。

5. 從第一天開始就預算無障礙

我們在第 5 個月發現的 34 個 WCAG 違規幾乎完全是可預防的。我們現在在我們的 CI 管道中每次拉取請求都執行 axe-core 檢查。如果你正在為公立大學構建,WCAG 2.1 AA 合規性不是可選的 — 在《第 508 條》下這是法律要求。

如果你面臨類似的遷移挑戰,我們很樂意與你談談具體情況。你可以 直接與我們聯絡或檢查我們的 定價頁面,了解我們通常如何範圍這些專案。

常見問題

從 Drupal 遷移大學網站到 Next.js 需要多長時間? 對於這個規模的網站 — 12,000 個內容節點、200 多個課程、已驗證的學生門戶 — 有一個專任的 4-6 人團隊,六個月是現實的。較小的機構網站(少於 2,000 頁、沒有門戶)通常可以在 3-4 個月內完成。時間表的推動因素不是前端構建,而是內容遷移、利益相關者一致性和與 Banner 或 PeopleSoft 等機構系統的整合。

哪個無頭 CMS 最適合高等教育網站? 這取決於你的編輯團隊的規模和技術舒適度。我們選擇 Sanity 進行這個專案,因為它的實時協作、靈活的內容建模和 GROQ 查詢語言。Contentful 和 Storyblok 也是不錯的選擇。對於具有非常大型內容團隊(100 多名編輯者)的大學,Contentful 的工作流程和權限模式可以具有優勢。對於想要更多自訂的較小團隊,Sanity 往往會勝出。

Next.js 能否處理已驗證的學生門戶? 絕對可以。Next.js 支援已驗證頁面的伺服器端渲染,App Router 的伺服器元件使得無需將其公開到用戶端包裡就可以直接提取使用者特定資料。我們使用 Auth.js 與我們編寫的自訂提供程式整合了 CAS(中央身份驗證服務)。門戶處理了 40,000 名學生而沒有性能問題。

大學的 Drupal 到 Next.js 遷移成本是多少? 這個規模的專案 — 課程查找器、學生門戶、200 多個課程、完整的內容遷移、CMS 設置和培訓 — 通常範圍從 $250,000 到 $450,000,取決於複雜性。但是,長期節省是顯著的。這所大學將其年度維護成本從約 $180K 降低到 $95K,這意味著即使在預算範圍的上限,該專案也會在 3-4 年內自行收回。

大規模 CMS 遷移期間 SEO 會發生什麼? 這是一個合理的擔憂。我們實施了全面的重定向對應(超過 2,400 個 301 重定向),在可能的情況下保留了所有現有 URL 結構,並新增了 Drupal 網站缺少的 schema.org 結構化資料。有機流量在啟動後的前兩週下降了約 8%(任何主要遷移都很正常),然後恢復並在四個月內超過基線 43%。

對於大學來說,Drupal 10 比轉向無頭更好嗎? 可以,取決於情況。如果你的團隊具有強大的 Drupal 專業知識,你的自訂模組與 Drupal 10 相容,並且你不需要靜態/混合網站的性能特性,那麼 Drupal 10 是一個完全有效的路徑。在我們的案例中,大學失去了 Drupal 專業知識,有 23 個不相容的模組,並需要大幅性能改進。無頭方法顯然更合適。

你如何處理從 Drupal 到無頭 CMS 的內容遷移? 我們使用自訂 Node.js 指令碼,透過 Drush 命令匯出 Drupal 內容,轉換資料以符合新 CMS 架構,處理媒體檔案遷移,並透過 CMS 的遷移 API 匯入所有內容。該過程通常執行 3 次:一次進行初步測試,一次用於預備環境重新整理,一次用於最終轉換。具有嵌入媒體的豐富文字內容最困難 — 你需要重新對映每個內部檔案參考。

在遷移期間,你能同時執行 Drupal 和 Next.js 嗎? 是的,我們建議這樣做。在我們的遷移期間,Drupal 繼續為生產網站提供服務,而我們在預備域上構建和測試 Next.js 版本。我們使用為期三週的雙重輸入期間,內容同時進入兩個系統。最終轉換是一個 DNS 交換,花費了約 15 分鐘,Drupal 作為 30 天的後備保留在唯讀模式。