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 тижні.







