In early 2024, a large state university came to us with a problem that's become painfully common in higher education: their Drupal 7 installation was reaching end-of-life, their student portal was buckling under load during enrollment periods, and their program finder — the single most important conversion tool on their website — took 8+ seconds to return search results. They had 40,000 active students, over 200 academic programs, and a six-month window before Drupal 7 security support effectively ended. No pressure.

This is the story of how we migrated the entire thing to Next.js with a headless CMS backend, cut page load times by 73%, and shipped it on schedule. I'll share the architecture decisions we made (and the ones we almost got wrong), the actual migration process, performance benchmarks, and the lessons that apply to any large-scale CMS migration.

Table of Contents

Case Study: Migrating a University Portal from Drupal to Next.js

The Starting Point: What We Were Working With

Let me paint the picture. The university's digital presence was built on Drupal 7, originally launched around 2014. Over the past decade, it had accumulated:

  • ~12,000 content nodes across programs, courses, faculty profiles, news articles, and events
  • 200+ academic program pages each with complex taxonomy relationships (degree level, department, college, delivery format, accreditation status)
  • A custom program finder built as a Drupal Views-based search with exposed filters — functional but slow
  • A student portal with authenticated access to advising tools, degree audits, registration links, and personalized dashboards
  • 47 custom Drupal modules, of which 19 were no longer maintained
  • 3 different theme layers stacked on top of each other from successive redesigns

The site was hosted on two aging virtual machines behind an institutional load balancer. During peak enrollment (August and January), the program finder would regularly time out. The marketing team had resorted to posting a PDF list of programs as a backup. That tells you everything.

Core Web Vitals were rough:

Metric Drupal 7 (Before) Target
LCP 6.2s < 2.5s
FID 380ms < 100ms
CLS 0.31 < 0.1
TTFB 2.8s < 0.8s
Program Finder Load 8.4s < 1.5s

The Stakeholder Landscape

University web projects are uniquely challenging because of the number of stakeholders. We were working with:

  • Central IT — responsible for SSO integration, security compliance, and hosting
  • Marketing & Communications — owned the brand, content strategy, and analytics
  • The Registrar's Office — owned program data and the student information system (SIS)
  • Individual Colleges & Departments — each with their own content editors (over 80 people with CMS access)
  • Student Government — who advocated loudly for mobile-first design (rightfully so)

Getting alignment from all of these groups took the first three weeks of the project. We ran a design sprint to establish shared priorities and non-negotiables.

Why Next.js (And Why Not Drupal 10)

The obvious question: why not just upgrade to Drupal 10? The university's IT team had actually started down that path six months before contacting us. They'd abandoned it after discovering that 23 of their 47 custom modules had no Drupal 10 equivalent and would need to be completely rewritten.

The real calculus looked like this:

Factor Drupal 10 Migration Next.js Rebuild
Estimated timeline 8-10 months 6 months
Custom module rewrites 23 modules N/A (rebuilt as APIs/components)
Content editor retraining Moderate (new admin UI) Moderate (new CMS)
Performance ceiling Moderate improvement Dramatic improvement
Hosting flexibility Traditional LAMP/similar Edge deployment, CDN-first
Developer hiring pool Shrinking (Drupal specialists) Growing (React/Next.js)
Long-term maintenance cost ~$180K/year ~$95K/year

The maintenance cost difference was the clincher for the administration. Drupal developers with institutional experience were getting harder to find and more expensive to retain. The university's own IT team had three React developers and zero Drupal specialists after their senior Drupal developer retired.

We chose Next.js specifically (over Gatsby, Remix, or Astro) for several reasons:

  1. Hybrid rendering — program pages could be statically generated, while the student portal needed server-side rendering with authentication
  2. API routes — we could build middleware for the SIS integration without a separate backend service
  3. Incremental Static Regeneration (ISR) — program data changes weekly, not hourly. ISR with a 1-hour revalidation window was perfect
  4. The university's team knew React — they'd be maintaining this after handoff

If you're weighing similar options, our Next.js development capabilities page covers the technical specifics of what we typically build.

Architecture Decisions

Headless CMS Selection

We evaluated five headless CMS options against the university's requirements: 80+ content editors, complex content relationships, role-based permissions, and a reasonable per-seat pricing model.

We landed on Sanity for this project. The key factors:

  • GROQ queries handled the complex taxonomy relationships between programs, departments, and colleges far better than GraphQL for this use case
  • Real-time collaboration — multiple editors could work simultaneously without conflicts
  • Custom input components — we built a program prerequisite mapper directly in the studio
  • Pricing — the Enterprise plan at ~$949/month was well within budget, and the per-user cost was predictable

The content modeling took about two weeks. We defined 14 document types and 8 reference types. The program schema alone had 34 fields, including structured data for schema.org EducationalOrganization and Course markup.

For more on our approach to CMS architecture, see our headless CMS development page.

Infrastructure

We deployed on Vercel for the Next.js frontend (the Enterprise plan, required for FERPA compliance and SSO requirements). The student portal's authenticated routes use server-side rendering with session management through the university's existing CAS (Central Authentication Service) SSO.

The data flow looks like this:

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

Static program pages are pre-rendered at build time and revalidated every hour via ISR. The program finder uses a combination of pre-fetched data (loaded into the client at build time as a JSON index) and real-time filtering — no server round-trip needed for search operations.

The API Layer

The student information system (Ellucian Banner, if you're curious — it's always Banner) exposed a SOAP API. Yes, in 2024. We built a translation layer using Next.js API routes that consumed the SOAP endpoints and exposed clean REST endpoints to our frontend:

// /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',
    },
  });
}

This translation layer was one of the highest-value pieces of the project. It decoupled the frontend from Banner's quirks and gave the university a clean API they could use for future projects (a mobile app was already being discussed).

Case Study: Migrating a University Portal from Drupal to Next.js - architecture

The Program Finder: Rebuilding the Core Feature

The program finder was the most important page on the entire site. Analytics showed it accounted for 34% of all organic search traffic and was the #1 entry point for prospective students. Getting this wrong wasn't an option.

The Old Approach (And Why It Was Slow)

The Drupal version used Views with exposed filters. Every filter change triggered a full server round-trip, re-queried the database, and re-rendered the entire page. With 200+ programs and 6 taxonomy dimensions (degree level, college, department, delivery format, area of interest, and keyword search), the query was expensive.

The New Approach

We pre-built a search index at build time. All 200+ programs were serialized into a ~180KB JSON file (gzipped to ~22KB) that ships with the page. Filtering happens entirely client-side using a custom hook:

// 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 };
}

We used Fuse.js for fuzzy text search and plain JavaScript filtering for facets. The result: search results appear in under 50ms. No loading spinners. No server calls. Users can slam the filters as fast as they want.

Each program result links to a statically generated detail page with full schema.org markup, which dramatically improved the university's appearance in Google's education-related search features.

Student Portal Migration

The student portal was the trickiest part. It required authentication, personalization, and real-time data from Banner. We couldn't statically generate any of it.

Authentication Flow

The university uses CAS for single sign-on across all institutional systems. We integrated CAS with Next.js using a custom authentication flow:

  1. Unauthenticated user hits /portal → redirected to CAS login
  2. CAS redirects back with a service ticket
  3. Our API route validates the ticket against the CAS server
  4. We create a signed JWT stored in an httpOnly cookie
  5. Subsequent requests use the JWT for session management

We used next-auth (now Auth.js) with a custom CAS provider we wrote from scratch, since no maintained CAS provider existed at the time.

Portal Features

The student portal included:

  • Personalized dashboard with upcoming registration dates, holds, and advisor info
  • Degree audit summary pulled from Banner in real-time
  • Quick links to the LMS (Canvas), email, and library systems
  • Program-specific resources based on the student's declared major

All portal pages use server-side rendering. We cache Banner API responses aggressively (30-second TTL for most endpoints, 5-minute TTL for degree audits) to avoid overwhelming their system.

Content Migration Strategy

Migrating 12,000 content nodes from Drupal to Sanity required a systematic approach. We built a custom migration pipeline:

# Simplified migration pipeline
1. Export Drupal nodes → JSON via custom Drush commands
2. Transform JSON → Sanity document format via Node.js scripts
3. Process media files → upload to Sanity CDN
4. Import documents → Sanity Migration API
5. Validate → automated checks for broken references

The media migration was the most tedious part. Drupal's file management stores files with internal paths and database references. We wrote a script that:

  1. Downloaded every file from the Drupal files directory
  2. Uploaded it to Sanity's asset pipeline
  3. Mapped old Drupal file IDs to new Sanity asset references
  4. Updated all rich text content to point to the new asset references

This script ran for about 14 hours on the full dataset. We ran it three times during the project: once for initial testing, once at the midpoint to refresh staging, and once for the final cutover.

Content Freeze Strategy

We implemented a two-phase content freeze:

  • Weeks 1-20: Content editors work in Drupal as normal. We migrate snapshots to staging weekly.
  • Weeks 21-23: Dual entry. New content goes into both Drupal and Sanity. Editors trained on new CMS.
  • Week 24: Full cutover. Drupal goes read-only, then offline.

The dual-entry period was painful but necessary. We had 80+ editors, and they needed to build muscle memory with Sanity before it was their only option.

The 6-Month Timeline

Month Phase Key Deliverables
Month 1 Discovery & Architecture Stakeholder alignment, CMS selection, infrastructure setup, content modeling
Month 2 Core Development Design system, page templates, program detail pages, navigation
Month 3 Program Finder & Search Search index, filtering UI, program data pipeline, SEO markup
Month 4 Student Portal CAS integration, Banner API layer, dashboard, degree audit display
Month 5 Content Migration & Training Migration scripts, editor training (6 sessions), staging QA
Month 6 QA, Performance, Launch Load testing, accessibility audit, content freeze, DNS cutover

Our team was 4 developers, 1 designer, and 1 project manager. The university provided a dedicated product owner plus an IT liaison for the Banner/CAS integration work.

We hit two major snags:

  1. Month 3: Banner's SOAP API had an undocumented rate limit of 100 requests/minute. Our program finder was designed to batch-fetch all program data during build. We had to implement a queuing system and spread the build across multiple batches.

  2. Month 5: The accessibility audit found 34 WCAG 2.1 AA violations. Most were inherited from the design (insufficient color contrast on secondary buttons, missing focus indicators on the program finder filters). We spent an unplanned 8 days on remediation.

Performance Results

Here's what the numbers looked like after launch:

Metric Drupal 7 (Before) Next.js (After) Improvement
LCP 6.2s 1.1s 82% faster
FID / INP 380ms 45ms 88% faster
CLS 0.31 0.02 94% better
TTFB 2.8s 0.12s 96% faster
Program Finder Load 8.4s 0.8s 90% faster
Lighthouse Score 34 97 +63 points
Build time (full) N/A 4m 12s
Monthly hosting cost ~$2,400 ~$1,100 54% lower

But the numbers that mattered most to the university were these:

  • Program finder usage increased 156% in the first semester after launch
  • Mobile bounce rate dropped from 67% to 31%
  • Organic search traffic to program pages increased 43% within 4 months (schema.org markup + Core Web Vitals improvements)
  • Support tickets related to the portal dropped 62% — largely because pages actually loaded reliably
  • Zero downtime during fall enrollment — the first time in three years

Lessons Learned

1. Start the CAS/SSO Integration Early

We scheduled the CAS integration for Month 4. We should have started a proof of concept in Month 1. University IT teams move deliberately (read: slowly) through security reviews. Getting the SSO architecture approved took three weeks of back-and-forth with their security office.

2. Content Modeling Is Architecture

We spent two full weeks on content modeling before writing any frontend code. This felt slow at the time. It was the single best investment we made. When you have 200+ programs with complex relationships between departments, colleges, degree levels, concentrations, and delivery formats, getting the schema right upfront saves hundreds of hours of refactoring.

3. Train Editors Early, Not Just Before Launch

We initially planned editor training for Month 5. We moved it to Month 4 after feedback from the product owner. This gave editors six weeks to get comfortable with Sanity instead of two. The quality of content entered during the dual-entry period was dramatically better because of this.

4. Banner Is Banner

If you're working with Ellucian Banner (and if you're in higher ed, you probably are), budget extra time for the API integration. The documentation is sparse, the SOAP endpoints are inconsistent, and every institution has customized their Banner instance differently. Our translation layer was essential.

5. Budget for Accessibility From Day One

Our 34 WCAG violations at Month 5 were almost entirely preventable. We now run axe-core checks in our CI pipeline on every pull request. If you're building for a public university, WCAG 2.1 AA compliance isn't optional — it's a legal requirement under Section 508.

If you're facing a similar migration challenge, we're happy to talk through the specifics. You can reach out to us directly or check our pricing page for how we typically scope these projects.

FAQ

How long does it take to migrate a university website from Drupal to Next.js? For a site of this scale — 12,000 content nodes, 200+ programs, authenticated student portal — six months is realistic with a dedicated team of 4-6 people. Smaller institutional sites (under 2,000 pages, no portal) can often be done in 3-4 months. The timeline is driven less by the frontend build and more by content migration, stakeholder alignment, and integration with institutional systems like Banner or PeopleSoft.

What headless CMS is best for higher education websites? It depends on your editorial team's size and technical comfort. We chose Sanity for this project because of its real-time collaboration, flexible content modeling, and GROQ query language. Contentful and Storyblok are also strong options. For universities with very large content teams (100+ editors), Contentful's workflow and permissions model can be advantageous. For smaller teams that want more customization, Sanity tends to win.

Can Next.js handle authenticated student portals? Absolutely. Next.js supports server-side rendering for authenticated pages, and the App Router's server components make it straightforward to fetch user-specific data without exposing it to the client bundle. We integrated with CAS (Central Authentication Service) using Auth.js with a custom provider. The portal handled 40,000 students without performance issues.

How much does a Drupal to Next.js migration cost for a university? A project of this scope — program finder, student portal, 200+ programs, full content migration, CMS setup, and training — typically ranges from $250,000 to $450,000 depending on complexity. However, the long-term savings are significant. This university reduced their annual maintenance costs from approximately $180K to $95K, meaning the project pays for itself within 3-4 years even at the higher end of the budget range.

What happens to SEO during a large-scale CMS migration? This is a legitimate concern. We implemented a comprehensive redirect map (over 2,400 301 redirects), preserved all existing URL structures where possible, and added schema.org structured data that the Drupal site lacked. Organic traffic dipped about 8% in the first two weeks post-launch (normal for any major migration), then recovered and exceeded the baseline by 43% within four months.

Is Drupal 10 a better choice than going headless for universities? It can be, depending on the situation. If your team has strong Drupal expertise, your custom modules have Drupal 10 compatibility, and you don't need the performance characteristics of a static/hybrid site, Drupal 10 is a perfectly valid path. In our case, the university had lost their Drupal expertise, had 23 incompatible modules, and needed dramatic performance improvements. The headless approach was clearly the better fit.

How do you handle content migration from Drupal to a headless CMS? We use custom Node.js scripts that export Drupal content via Drush commands, transform the data to match the new CMS schema, handle media file migration, and import everything via the CMS's migration API. The process typically runs 3 times: once for initial testing, once for staging refresh, and once for final cutover. Rich text content with embedded media is the hardest part — you need to remap every internal file reference.

Can you run Drupal and Next.js simultaneously during migration? Yes, and we recommend it. During our migration, Drupal continued serving the production site while we built and tested the Next.js version on a staging domain. We used a three-week dual-entry period where content went into both systems. The final cutover was a DNS switch that took about 15 minutes, with Drupal kept in read-only mode for 30 days as a fallback.