
Tutorial: Migrating a Large-Scale Legacy Drupal Site to Headless Next.js
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
- Setting Up the Drupal Backend
- Implementing the Next.js Frontend
- Creating a Hybrid Routing System
- Implementing Shared Authentication
- Gradual Content Migration
- 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
Bashcomposer 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
YAMLname: 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:
EnvironmentNEXT_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
JavaScriptimport { 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:
Bashcomposer 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 (
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

Next.js Performance Optimization: The Complete 2025 Guide
Master advanced Next.js 15+ performance techniques including React Server Components, streaming, and Core Web Vitals optimization for lightning-fast applications.

Web Performance Metrics That Actually Matter in 2025
Navigate the evolving landscape of web performance with Core Web Vitals updates, INP transition, and business-critical metrics that drive real results.

Building a Custom GraphQL API Module for Drupal 10 Article Management
Learn how to create a custom GraphQL API module for Drupal 10, focusing on article management. This guide covers the module structure, file creation, and implementation of GraphQL queries, mutations, and subscriptions.