Dilip Singh logo
All posts
Web DevelopmentIntermediate2026-01-22·12 min read

Migrating Drupal 7 to Next.js: A 9-Year-Old CMS Reborn

How I migrated a 9-year-old Drupal 7 site with 80K nodes, 200K users, and 14 years of SEO into a modern Next.js + headless CMS architecture without losing rankings or breaking URLs.

The Project

A 9-year-old Drupal 7 site. 80,000 nodes (articles, pages, custom content types), 200,000 registered users, 14 years of accumulated SEO authority, and a CKEditor-based content workflow nobody wanted to throw away.

Goals: modern UX, < 1s LCP, kept editorial workflow, zero URL breakage, zero SEO drop.

Architecture Decision

After evaluating a full rewrite, headless Drupal, and headless CMS options, I picked:

  • Next.js 15 for frontend (App Router, RSC)
  • Drupal 10 as headless CMS (keep editors happy, migrate from D7)
  • JSON:API + GraphQL for data fetching
  • ISR (Incremental Static Regeneration) for content pages
  • Algolia for search

Step 1: Drupal 7 → Drupal 10 Migration

bash
# Migrate Drupal core
drush migrate:upgrade --legacy-db-url=mysql://user:pass@host/d7db --legacy-root=/old/site

# Run individual migrations drush migrate:import d7_user drush migrate:import d7_node_complete:article drush migrate:import d7_taxonomy_term drush migrate:import d7_file ```

This took 3 weeks. Custom modules required custom migrate plugins.

Step 2: Expose Content via JSON:API

yaml
# Enable JSON:API + cache headers
modules:
  - jsonapi
  - jsonapi_extras
  - cache_control_override
php
// Custom: filter PHP eval security risk fields from JSON:API output
function mymodule_jsonapi_entity_filter_access(EntityInterface $entity) {
    return AccessResult::forbiddenIf($entity->bundle() === 'internal_only');
}

Step 3: Next.js with ISR

tsx
// app/[...slug]/page.tsx
export const revalidate = 300  // 5 min ISR

export async function generateStaticParams() { const res = await fetch(${DRUPAL_URL}/jsonapi/node/article?fields[node--article]=path) const { data } = await res.json() return data.map((n: any) => ({ slug: n.attributes.path.alias.split('/').filter(Boolean) })) }

export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params const path = '/' + slug.join('/') const res = await fetch( ${DRUPAL_URL}/router/translate-path?path=${path}, { next: { revalidate: 300, tags: [node-${path}] } } ) const route = await res.json() const node = await fetchNode(route.entity.uuid, route.entity.bundle) return } ```

Step 4: Webhook-Triggered Revalidation

When an editor saves in Drupal, trigger Next.js to revalidate:

php
function mymodule_node_update(NodeInterface $node) {
    $path = $node->toUrl()->toString();
    \Drupal::httpClient()->post(
        getenv('NEXTJS_REVALIDATE_URL'),
        [
            'headers' => ['Authorization' => 'Bearer ' . getenv('REVALIDATE_TOKEN')],
            'json' => ['paths' => [$path], 'tags' => ['node-' . $path]],
        ]
    );
}
typescript
// app/api/revalidate/route.ts
export async function POST(req: Request) {
  const auth = req.headers.get('authorization')
  if (auth !== `Bearer ${process.env.REVALIDATE_TOKEN}`) return new Response('forbidden', { status: 403 })

const { paths = [], tags = [] } = await req.json() paths.forEach(p => revalidatePath(p)) tags.forEach(t => revalidateTag(t)) return Response.json({ revalidated: true }) } ```

Step 5: SEO Preservation

ConcernSolution
URL structureKept identical. Drupal aliases → Next.js dynamic routes
RedirectsImported D7 `redirect` table into a Next.js middleware-based redirect map
Meta tagsAll metatag module data exposed via JSON:API and rendered server-side
SitemapGenerated dynamically from Drupal node list
Schema.orgRe-implemented Schema.org metatag module output in Next.js

Results After 3 Months

MetricBefore (Drupal 7)After (Next.js + D10)
LCP4.2s0.9s
Lighthouse SEO88100
Organic trafficbaseline+12%
Editor satisfactionbaseline"love it"
Server cost$1200/mo$340/mo

No URL broke. No ranking dropped. The 14 years of SEO authority transferred intact because we never changed a URL — we just changed what renders at each URL.

DS
Dilip Singh
Lead Software Architect · Hureka Technologies

14+ years building enterprise software and AI systems. Architecting multi-agent AI platforms, RAG pipelines, voice AI, and high-performance SaaS for global clients.