Настройка Drupal как Headless CMS (Decoupled Drupal)
Headless Drupal — архитектура, где Drupal выступает только как бэкенд и API для контента, а фронтенд (Next.js, Nuxt, мобильное приложение) подключается через JSON:API или GraphQL. Это даёт полную свободу в выборе фронтенда, но усложняет архитектуру.
Архитектурные варианты
Полностью decoupled — Drupal только API, фронтенд полностью отдельный проект. Drupal и фронтенд деплоятся независимо. Нет Drupal-тем, нет Twig.
Progressively decoupled — часть страниц рендерится традиционно через Drupal, интерактивные блоки (корзина, формы, real-time данные) — отдельные React/Vue компоненты. Проще в переходе от монолита.
Необходимые модули
composer require drupal/jsonapi_extras drupal/simple_oauth \
drupal/decoupled_router drupal/subrequests drupal/consumers \
drupal/next drupal/preview_url_generator
drush en jsonapi jsonapi_extras simple_oauth decoupled_router \
subrequests consumers next -y
decoupled_router — разрешает URL алиасы (/about) в UUID + bundle через API, нужно для роутинга на фронтенде.
consumers — управление фронтенд-клиентами с назначением ролей.
next — официальный модуль для интеграции с Next.js.
Настройка JSON:API для headless
# Убрать лишние поля из ответа (JSON:API Extras)
drush config:set jsonapi_extras.settings default_disabled_fields \
"revision_log,revision_uid,revision_timestamp,menu_link"
Конфигурация JSON:API Extras для типа контента:
# config/install/jsonapi_extras.jsonapi_resource_config.node--article.yml
id: node--article
resourceType: node--article
resourceFields:
title:
fieldName: title
publicName: title
disabled: false
body:
fieldName: body
publicName: content
disabled: false
field_hero_image:
fieldName: field_hero_image
publicName: hero_image
disabled: false
revision_timestamp:
fieldName: revision_timestamp
disabled: true # скрыть
Decoupled Router: разрешение путей
# Разрешить URL-алиас в тип контента и UUID
curl "https://drupal.site.com/router/translate-path?path=/about-us&_format=json"
# Ответ:
{
"resolved": "/node/42",
"isHomePath": false,
"entity": {
"canonical": "https://drupal.site.com/about-us",
"type": "node",
"bundle": "page",
"id": "42",
"uuid": "a1b2c3d4-..."
}
}
Next.js интеграция
// lib/drupal.ts
import { DrupalClient } from "next-drupal";
export const drupal = new DrupalClient(
process.env.NEXT_PUBLIC_DRUPAL_BASE_URL!,
{
auth: {
clientId: process.env.DRUPAL_CLIENT_ID!,
clientSecret: process.env.DRUPAL_CLIENT_SECRET!,
},
}
);
// app/[...slug]/page.tsx
import { drupal } from "@/lib/drupal";
import { DrupalNode } from "next-drupal";
export async function generateStaticParams() {
return await drupal.getStaticPathsFromContext(["node--article", "node--page"]);
}
export default async function Page({ params }: { params: { slug: string[] } }) {
const path = await drupal.translatePathFromContext({ params });
if (!path) notFound();
const node = await drupal.getResourceFromContext<DrupalNode>(path, {
params: {
include: "field_hero_image,field_tags",
fields: {
"node--article": "title,body,field_hero_image,field_tags,created",
},
},
});
return <Article node={node} />;
}
Preview / Draft режим
// app/api/preview/route.ts
import { drupal } from "@/lib/drupal";
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const path = await drupal.getResourceCollectionPathSegments(
["node--article"],
{
params: { "filter[status][value]": "0" }, // черновики
}
);
draftMode().enable();
redirect(searchParams.get("slug") || "/");
}
On-demand ISR (Incremental Static Regeneration)
При публикации контента в Drupal — автоматически обновить кэш Next.js:
// Drupal: хук на сохранение ноды
function mymodule_node_update(NodeInterface $node): void {
$next_base_url = \Drupal::config('next.settings')->get('site_base_url');
$revalidate_secret = \Drupal::config('next.settings')->get('revalidate_secret');
\Drupal::httpClient()->post(
"$next_base_url/api/revalidate",
['json' => ['path' => $node->toUrl()->toString(), 'secret' => $revalidate_secret]]
);
}
GraphQL альтернатива
composer require drupal/graphql
drush en graphql -y
GraphQL 4 для Drupal использует schema-first подход с кастомными резолверами. Более гибкий, чем JSON:API, но требует больше разработки.
CORS настройка
# services.yml
parameters:
cors.config:
enabled: true
allowedHeaders: ['*']
allowedMethods: ['*']
allowedOrigins:
- 'https://frontend.yourdomain.com'
- 'http://localhost:3000'
exposedHeaders: false
maxAge: false
supportsCredentials: true
Сроки
Базовая headless настройка с JSON:API + Next.js — 5–7 дней. Полный проект с Preview режимом, On-demand ISR, мультиязычностью — 2–3 недели.







