Hero image for Tutorial: Migrating a Large-Scale Legacy Drupal Site to Headless Next.js

Tutorial: Migrating a Large-Scale Legacy Drupal Site to Headless Next.js

2025-04-15By Arttus Team6 min read
nextjsperformanceoptimization

This tutorial focuses on implementing a hybrid approach to migrate a large-scale Drupal site to a headless architecture using Next.js. We'll cover the specific steps and code examples needed to integrate your existing Drupal setup with a new Next.js frontend.

Table of Contents

  1. Setting Up the Drupal Backend
  2. Implementing the Next.js Frontend
  3. Creating a Hybrid Routing System
  4. Implementing Shared Authentication
  5. Gradual Content Migration
  6. Performance Optimization for Hybrid Setup

1. Setting Up the Drupal Backend

First, we'll modify our Drupal backend to serve as both a traditional and headless CMS.

1.1 Install and Configure Necessary Modules

Bash
composer require drupal/next drupal/subrequests drupal/decoupled_router drush en next subrequests decoupled_router

1.2 Create a Custom Module for Next.js Integration

Create a new module called custom_nextjs_integration. Add the following files:

custom_nextjs_integration.info.yml

YAML
name: Custom Next.js Integration type: module description: 'Provides custom integration with Next.js' package: Custom core_version_requirement: ^9 || ^10 dependencies: - next:next

custom_nextjs_integration.module

PHP
<?php /** * Implements hook_next_site_preview_alter(). */ function custom_nextjs_integration_next_site_preview_alter(array &$preview) { $preview['url'] = 'https://your-nextjs-preview-url.com/api/preview'; }

1.3 Extend GraphQL Schema

Create a schema extension file:

src/Plugin/GraphQL/SchemaExtension/CustomNextjsSchemaExtension.php

PHP
<?php namespace Drupal\custom_nextjs_integration\Plugin\GraphQL\SchemaExtension; use Drupal\graphql\GraphQL\ResolverBuilder; use Drupal\graphql\GraphQL\ResolverRegistryInterface; use Drupal\next\Plugin\GraphQL\Schema\NextSchemaExtensionPluginBase; /** * @SchemaExtension( * id = "custom_nextjs_schema_extension", * name = "Custom Next.js schema extension", * description = "Schema extension for custom Next.js integration", * schema = "next" * ) */ class CustomNextjsSchemaExtension extends NextSchemaExtensionPluginBase { public function registerResolvers(ResolverRegistryInterface $registry) { $builder = new ResolverBuilder(); // Add custom resolvers for your specific content types $registry->addFieldResolver('Query', 'customContentType', $builder->produce('entity_load') ->map('type', $builder->fromValue('node')) ->map('bundles', $builder->fromValue(['custom_content_type'])) ->map('id', $builder->fromArgument('id')) ); // Add more field resolvers as needed } } ## 2. Implementing the Next.js Frontend Now, let's set up the Next.js frontend to work alongside the existing Drupal site. ### 2.1 Initialize Next.js Project ```bash npx create-next-app@latest nextjs-drupal-frontend cd nextjs-drupal-frontend npm install next-drupal

2.2 Configure Environment Variables

Create a .env.local file:

Environment
NEXT_PUBLIC_DRUPAL_BASE_URL=https://your-drupal-site.com NEXT_IMAGE_DOMAIN=your-drupal-site.com DRUPAL_PREVIEW_SECRET=your_secret_here

2.3 Create API Routes for Drupal Integration

pages/api/preview.js

JavaScript
import { DrupalClient } from 'next-drupal' const drupal = new DrupalClient(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL) export default async function handler(req, res) { const { slug, secret } = req.query if (secret !== process.env.DRUPAL_PREVIEW_SECRET) { return res.status(401).json({ message: 'Invalid token' }) } const path = await drupal.translatePath(slug) if (!path) { return res.status(404).json({ message: 'Slug not found' }) } res.setPreviewData({}) res.writeHead(307, { Location: path }) res.end() }

3. Creating a Hybrid Routing System

Implement a routing system that serves content from both Drupal and Next.js.

3.1 Next.js Dynamic Routing

Create a catch-all route in Next.js: javascript // pages/[…slug].js import { DrupalClient } from 'next-drupal' import Error from 'next/error' const drupal = new DrupalClient(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL) export async function getServerSideProps(context) { const path = await drupal.translatePath(context.params.slug.join('/')) if (!path) { // If path not found in Drupal, let Next.js handle 404 return { notFound: true } } const data = await drupal.getResourceFromContext(path) return { props: { data, }, } } export default function DynamicPage({ data }) { if (!data) return <Error statusCode={404} /> // Render your page based on the data return (

<div> <h1>{data.title}</h1> {/* Render other fields */} </div> ) } ### 3.2 Drupal Fallback Modify your Drupal `.htaccess` file to fallback to Drupal for unhandled routes:

Existing Drupal rewrite rules…

Next.js integration

RewriteCond %{REQUEST_URI} !^/admin RewriteCond %{REQUEST_URI} !^/user RewriteCond %{REQUEST_URI} !^/sites RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ https://your-nextjs-site.com/$1 [P,L]

4. Implementing Shared Authentication

Use JSON Web Tokens (JWT) for shared authentication between Drupal and Next.js.

4.1 Drupal JWT Configuration

Install and configure the JWT module in Drupal:

Bash
composer require drupal/jwt drush en jwt jwt_auth_consumer

Configure the JWT settings in Drupal's admin interface.

4.2 Next.js JWT Integration

Create an API route for authentication:

// pages/api/auth.js import { DrupalClient } from 'next-drupal' import jwt from 'jsonwebtoken' const drupal = new DrupalClient(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL) export default async function handler(req, res) { const { username, password } = req.body try { const auth = await drupal.authenticate(username, password) const token = jwt.sign( { id: auth.user.id, mail: auth.user.mail }, process.env.JWT_SECRET, { expiresIn: '1h' } ) res.status(200).json({ token }) } catch (error) { res.status(401).json({ error: 'Authentication failed' }) } }

5. Gradual Content Migration

Implement a system for gradually migrating content from Drupal to Next.js.

5.1 Drupal Migration Flag

Add a boolean field to your content types to indicate if they've been migrated:

// In your custom module's install file function custom_nextjs_integration_update_8001() { $field_storage = FieldStorageConfig::create([ 'field_name' => 'field_nextjs_migrated', 'entity_type' => 'node', 'type' => 'boolean', 'cardinality' => 1, 'translatable' => FALSE, ]); $field_storage->save(); $field_instance = FieldConfig::create([ 'field_storage' => $field_storage, 'bundle' => 'article', // Repeat for other content types 'label' => 'Migrated to Next.js', ]); $field_instance->save(); }

5.2 Next.js Content Check

In your Next.js pages, check if the content has been migrated:

// pages/[…slug].js // … existing imports and getServerSideProps … export default function DynamicPage({ data }) { if (!data) return <Error statusCode={404} /> if (!data.field_nextjs_migrated) { // Redirect to Drupal for non-migrated content if (typeof window !== 'undefined') { window.location.href = ${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}/${data.path.alias} } return null } // Render migrated content return (

<div> <h1>{data.title}</h1> {/* Render other fields */} </div> ) }

6. Performance Optimization for Hybrid Setup

Implement caching strategies to ensure optimal performance in the hybrid setup.

6.1 Drupal Cache Tags

Expose Drupal cache tags in the API response:

// In your custom module function custom_nextjs_integration_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { if ($view_mode == 'full') { $build['#cache']['tags'][] = 'node:' . $entity->id(); } }

6.2 Next.js Incremental Static Regeneration

Use Incremental Static Regeneration (ISR) in Next.js for dynamic content:

// pages/[…slug].js export async function getStaticProps(context) { const path = await drupal.translatePath(context.params.slug.join('/')) if (!path) { return { notFound: true } } const data = await drupal.getResourceFromContext(path) return { props: { data, }, revalidate: 60, // Revalidate every 60 seconds } } export async function getStaticPaths() { return { paths: [], fallback: 'blocking' } }

This setup allows you to serve fresh content while maintaining high performance.

Conclusion

This hybrid approach allows for a gradual migration from a traditional Drupal site to a headless architecture with Next.js. By implementing these steps, you can maintain your existing Drupal functionality while progressively enhancing your site with Next.js capabilities. Remember to thoroughly test each component and continuously monitor performance as you migrate more content and functionality to the new architecture.

Related Posts