案例研究:将大学门户从Drupal迁移到Next.js
2024年大学门户从Drupal迁移到Next.js的案例研究
2024年初,一所大型州立大学来找我们解决一个在高等教育中变得极其普遍的问题:他们的Drupal 7安装已接近生命末期,学生门户在注册期间不堪重负,他们网站上最重要的单一转化工具——专业查找器——需要8秒以上才能返回搜索结果。他们有40,000名在校学生、超过200个学位项目,以及6个月的时间窗口,之后Drupal 7的安全支持就将实际结束。可谓压力山大。
这是我们如何将整个系统迁移到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个模块 | 不适用(重建为API/组件) |
| 内容编辑者再培训 | 中等(新管理UI) | 中等(新CMS) |
| 性能上限 | 中等改进 | 显著改进 |
| 托管灵活性 | 传统LAMP/类似 | 边缘部署、CDN优先 |
| 开发者招聘库 | 萎缩(Drupal专家) | 增长(React/Next.js) |
| 长期维护成本 | 约$180K/年 | 约$95K/年 |
维护成本差异对管理部门来说是关键因素。具有机构经验的Drupal开发人员越来越难找到,保留成本也越来越高。该大学自己的IT团队有三名React开发人员和零名Drupal专家,他们的高级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%,是潜在学生的第一入口点。把这个做错了不是一个选项。
旧方法(以及它为什么很慢)
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进行Facet过滤。结果:搜索结果在50毫秒内出现。没有加载旋转器。没有服务器调用。用户可以以任何速度狂轰滥炸过滤器。
每个项目结果都链接到带有完整schema.org标记的静态生成的详细页面,大大改进了该大学在谷歌教育相关搜索功能中的外观。
学生门户迁移
学生门户是最棘手的部分。它需要身份验证、个性化和来自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响应(大多数端点的TTL为30秒,学位审计的TTL为5分钟),以避免使他们的系统超载。
内容迁移策略
将12,000个内容节点从Drupal迁移到Sanity需要系统的方法。我们构建了自定义迁移管道:
# 简化的迁移管道
1. 通过自定义Drush命令从Drupal节点导出 → JSON
2. 转换JSON → Sanity文档格式通过Node.js脚本
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成为他们的唯一选项之前在其中建立肌肉记忆。
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分 |
| 构建时间(完整) | 不适用 | 4分12秒 | — |
| 月托管成本 | 约$2,400 | 约$1,100 | 降低54% |
但对大学最重要的数字是:
- 专业查找器使用量在上线后第一个学期增加156%
- 移动跳出率从67%下降到31%
- 有机搜索流量到项目页面在4个月内增加43%(schema.org标记+核心Web指标改进)
- 与门户相关的支持票在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天内保持只读模式作为备用。