40,000名学生、6个月、零停机:Drupal → Next.js迁移
您的项目查找工具在8.2秒时超时。再次。这是注册周,学生门户网站正在崩溃——40,000名本科生刷新同一个损坏的搜索,您的支持队列堆满了工单,您的招生副总裁正在实时观看转化率下降。Drupal 7安全补丁将在六个月内结束。您有200多个项目页面、一个您的团队几乎不了解的遗留CMS,以及一个不可协商的要求:零停机。在2024年初,一所大型州立大学将这个问题交给了我们。他们需要在Drupal 7关闭前完成对Next.js的完整迁移——他们需要他们的项目查找工具停止流失学生。以下是我们如何在26周内重建了他们的整个门户网站、将搜索响应时间削减到340毫秒,并在不让网站离线一分钟的情况下进行部署的故事。
这是我们如何将整个项目迁移到Next.js并使用无头CMS后端、将页面加载时间减少73%、并按时发布的故事。我将分享我们做出的架构决策(以及我们几乎犯的错误)、实际迁移过程、性能基准,以及适用于任何大规模CMS迁移的经验教训。
目录

起点:我们在处理什么
让我来描绘一下这个情景。这所大学的数字存在是建立在Drupal 7上的,最初是在2014年左右推出的。在过去的十年里,它积累了:
- 约12,000个内容节点,跨越项目、课程、教师档案、新闻文章和事件
- 200多个学术项目页面,每个都有复杂的分类关系(学位级别、部门、学院、交付格式、认证状态)
- 一个自定义项目查找工具,构建为基于Drupal Views的搜索,带有公开过滤器——功能正常但速度慢
- 一个学生门户,具有对建议工具、学位审计、注册链接和个性化仪表板的身份验证访问权限
- 47个自定义Drupal模块,其中19个不再维护
- 3个不同的主题层,从连续的重新设计中堆叠在一起
该网站托管在机构负载均衡器后面的两台老化虚拟机上。在峰值注册期间(8月和1月),项目查找工具会经常超时。营销团队采取了发布项目PDF列表作为备份的方式。这说明了一切。
核心Web指标很糟糕:
| 指标 | 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)有几个原因:
- 混合渲染——项目页面可以静态生成,而学生门户需要带身份验证的服务器端渲染
- API路由——我们可以为SIS集成构建中间件,而不需要单独的后端服务
- 增量静态再生(ISR)——项目数据每周变化一次,不是每小时。ISR配合1小时重新验证窗口是完美的
- 大学的团队懂React——他们将在交付后维护此项目
如果您权衡类似的选项,我们的Next.js开发功能页面涵盖了我们通常构建的技术细节。
架构决策
无头CMS选择
我们根据大学的要求评估了五个无头CMS选项:80多个内容编辑人员、复杂内容关系、基于角色的权限,以及合理的每座定价模式。
我们在这个项目中选择了Sanity。关键因素:
- GROQ查询处理项目、部门和学院之间的复杂分类关系远比GraphQL在这个用例中更好
- 实时协作——多个编辑人员可以同时工作而不会产生冲突
- 自定义输入组件——我们直接在studio中构建了项目先决条件映射工具
- 定价——企业计划约$949/月完全在预算范围内,每用户成本是可预测的
内容建模花费了大约两周。我们定义了14个文档类型和8个引用类型。仅项目模式就有34个字段,包括用于schema.org EducationalOrganization和Course标记的结构化数据。
有关我们对CMS架构的方法,请参阅我们的无头CMS开发页面。
基础设施
我们在Vercel上部署了Next.js前端(企业计划,FERPA合规性和SSO要求需要)。学生门户的身份验证路由使用服务器端渲染,会话管理通过大学现有的CAS(中央认证服务) SSO。
数据流看起来是这样的:
[Sanity CMS] → [Vercel上的Next.js] → [CDN边缘]
↕
[大学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,他们可以用于未来的项目(移动应用已经在讨论中)。

项目查找工具:重建核心功能
项目查找工具是整个网站上最重要的页面。分析显示它占有机搜索流量的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集成:
- 未认证的用户点击
/portal→重定向到CAS登录 - CAS使用服务票证重定向回来
- 我们的API路由针对CAS服务器验证票证
- 我们创建存储在httpOnly cookie中的签名JWT
- 后续请求使用JWT进行会话管理
我们使用了next-auth(现在的Auth.js)和自定义CAS提供程序,我们从头开始编写,因为当时不存在维护的CAS提供程序。
门户功能
学生门户包括:
- 个性化仪表板,显示即将来临的注册日期、挂起项和顾问信息
- 学位审计摘要,从Banner实时提取
- 快速链接到LMS(Canvas)、电子邮件和图书馆系统
- 项目特定资源,基于学生声明的专业
所有门户页面都使用服务器端渲染。我们积极缓存Banner API响应(大多数端点为30秒TTL,学位审计为5分钟TTL),以避免他们系统过载。
内容迁移策略
从Drupal将12,000个内容节点迁移到Sanity需要系统的方法。我们构建了一个自定义迁移管道:
# 简化的迁移管道
1. 导出Drupal节点→通过自定义Drush命令生成JSON
2. 转换JSON→通过Node.js脚本转为Sanity文档格式
3. 处理媒体文件→上传到Sanity CDN
4. 导入文档→Sanity迁移API
5. 验证→自动检查损坏的引用
媒体迁移是最乏味的部分。Drupal的文件管理使用内部路径和数据库引用存储文件。我们编写了一个脚本:
- 从Drupal文件目录下载每个文件
- 将其上传到Sanity的资产管道
- 将旧的Drupal文件ID映射到新的Sanity资产引用
- 更新所有富文本内容以指向新的资产引用
这个脚本在完整数据集上运行了约14小时。我们在项目期间运行了三次:一次用于初始测试,一次在中点刷新分级,一次用于最终切换。
内容冻结策略
我们实施了两阶段内容冻结:
- 第1-20周:内容编辑在Drupal中正常工作。我们每周向分级迁移快照。
- 第21-23周:双重输入。新内容进入Drupal和Sanity。编辑人员在新CMS上接受培训。
- 第24周:完全切换。Drupal变为只读,然后脱机。
双重输入期很痛苦但很必要。我们有80多个编辑,他们需要在Sanity成为唯一选择之前与Sanity建立肌肉记忆。
6个月的时间表
| 月份 | 阶段 | 关键可交付成果 | |-------|-------|------------------|| | 第1个月 | 发现与架构 | 利益相关者对齐、CMS选择、基础设施设置、内容建模 | | 第2个月 | 核心开发 | 设计系统、页面模板、项目详细页面、导航 | | 第3个月 | 项目查找工具与搜索 | 搜索索引、过滤UI、项目数据管道、SEO标记 | | 第4个月 | 学生门户 | CAS集成、Banner API层、仪表板、学位审计显示 | | 第5个月 | 内容迁移与培训 | 迁移脚本、编辑人员培训(6场会议)、分级QA | | 第6个月 | QA、性能、启动 | 负载测试、可访问性审计、内容冻结、DNS切换 |
我们的团队是4名开发人员、1名设计师和1名项目经理。大学提供了专门的产品所有者加IT联系人进行Banner/CAS集成工作。
我们遇到了两个主要问题:
第3个月:Banner的SOAP API有一个未文档化的100请求/分钟的速率限制。我们的项目查找工具旨在在构建过程中批量获取所有项目数据。我们必须实施一个队列系统并将构建分散到多个批次。
第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%,在启动后4个月内(schema.org标记+ Core Web Vitals改进)
- 与门户相关的支持工单下降了62%——主要是因为页面实际上加载可靠
- 秋季注册期间零停机——三年来第一次
经验教训
1. 提早开始CAS/SSO集成
我们在第4个月安排了CAS集成。我们应该在第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天作为备用。