Архітектура Headless WordPress у зв'язці з Next.js 16 (React 19) є сучасним інженерним стандартом розділення відповідальності (Decoupled Architecture), де WordPress функціонує виключно як контентний бекенд та панель адміністрування (Headless CMS), а Next.js виступає високопродуктивним шаром представлення (Edge Presentation Layer).
Взаємодія систем здійснюється через суворо типізовані API-контракти:
/wp-json/wp/v2/): нативний протокол із підтримкою фільтрації полів (_fields), вбудовування пов'язаних ресурсів (_embed) та кастомних просторів імен.Ключовою перевагою Next.js 16 є архітектура React Server Components (RSC). Серверні компоненти виконуються строго в середовищі Node.js/Edge Runtime, здійснюють вибірку даних з WordPress безпосередньо в момент генерації сторінки та віддають браузеру оптимізований легковагий віртуальний DOM без надсилання важких клієнтських бібліотек у JS-бандл.
Критичне інженерне завдання інтеграції — безпечний парсинг і гідратація контенту. WordPress зберігає тіло публікацій у вигляді сирих HTML-рядків (post_content). Безпосереднє використання dangerouslySetInnerHTML неприпустиме через ризики Stored XSS та втрату оптимізацій Next.js. Сучасний стек реалізує компіляцію AST (Abstract Syntax Tree), за якої теги розмітки рекурсивно трансформуються в нативні оптимізовані компоненти:
<img> перетворюється на <Image sizes="..." priority={...} /> з адаптивною нарізкою та форматами WebP/AVIF.<a href="..."> перетворюються на <Link prefetch={false}>, забезпечуючи миттєву клієнтську навігацію (SPA) без повного перезавантаження документа.Додатковий контур безпеки реалізується через Origin Cloaking (приховування джерела): адреса CMS WordPress повністю ізолюється всередині приватної віртуальної мережі або приховується за зворотним проксі (rewrites у next.config.ts), захищаючи панель керування від автоматизованого сканування ботами та DDoS-атак.
Архітектурна схема взаємодії клієнтського браузера, шлюзу Next.js 16 та ізольованого бекенду WordPress 7.0+:
+-----------------------------------------------------------------------------------------+
| ПУБЛІЧНИЙ ІНТЕРНЕТ |
+-----------------------------------------------------------------------------------------+
| [Користувач / Браузер] |
| | |
| | 1. HTTP/3 Запит (HTML + Minimal JS Bundle) |
| v |
+-----------------------------------------------------------------------------------------+
| NEXT.JS 16 APPSERVER / EDGE RUNTIME |
+-----------------------------------------------------------------------------------------+
| [Reverse Proxy: /api/*] ---> [Edge Route Handlers: Cache / ISR] |
| | |
| v |
| [React Server Components Layer (RSC)] |
| | |
| +---> [AST Parser & Sanitizer] |
| | | (img -> NextImage, a -> NextLink) |
| | v |
| | [Optimized Virtual DOM Stream] |
+-----------------------------------------------------------------------------------------+
|
| 2. Private M2M Network (TLS 1.3 / JWT / AppPass)
v
+-----------------------------------------------------------------------------------------+
| ІЗОЛЬОВАНИЙ БЕКЕНД WORDPRESS 7.0+ (CMS) |
+-----------------------------------------------------------------------------------------+
| [Nginx Ingress (Origin Cloaked)] -> [WP REST API / WPGraphQL] -> [Redis Object Cache] |
| | |
| v |
| [MySQL Database] |
+-----------------------------------------------------------------------------------------+Архітектурний шаблон розділення контентної CMS і клієнтського інтерфейсу, за якого бекенд надає виключно структуровані дані через API, а фронтенд реалізує всю логіку рендерингу та маршрутизації.
Компоненти React, що виконуються виключно на сервері. Вони мають прямий доступ до API, не збільшують розмір клієнтського JavaScript-бандла та підтримують потокову передачу розмітки через Suspense.
Синтаксичний аналіз сирої розмітки WordPress у деревоподібну структуру (Abstract Syntax Tree) з подальшою заміною стандартних HTML-тегів на екосистему Next.js (Image, Link).
Метод приховування реальної IP-адреси та домену WordPress за пограничним шлюзом Next.js, що виключає прямий публічний доступ зловмисників до панелі керування CMS.
Дворівнева система типізації: генерація статичних типів TypeScript зі схеми CMS на етапі збірки та рантайм-валідація відповідей API за допомогою бібліотеки Zod для захисту від змін контрактів.
Серверний компонент Next.js app/blog/[slug]/page.tsx приймає запит та ініціює вибірку даних публікації з WordPress.
Через Promise.allSettled паралельно запитуються: тіло публікації, SEO-метадані, меню навігації та список свіжих публікацій із тегами кешу next: { tags: [...] }.
Сирий JSON-відповідь перевіряється схемою Zod. У разі невідповідності контракту викидається типізована помилка, що запобігає падінню сторінки.
Сирий HTML контенту парситься бібліотекою html-react-parser. Рекурсивний обхід замінює теги img на next/image та очищує небезпечні inline-атрибути за допомогою DOMPurify.
Next.js повертає перший байт (TTFB) за 30 мс, віддаючи критичний HTML першого екрана, а важкі блоки (коментарі) досилаються асинхронно в міру готовності.
Коли розробник виводить контент публікації через <div dangerouslySetInnerHTML={{ __html: post.content }} />, виникають дві фундаментальні проблеми:
unfiltered_html скомпрометовано або впроваджено шкідливий вектор через кастомні поля, шкідливий скрипт виконується в браузерах відвідувачів у контексті домену Next.js.<img> без адаптивних srcset, без fetchpriority і без оптимізації форматів, а посилання <a href="..."> викликають повне перезавантаження сторінки, руйнуючи переваги SPA.Рішення: використання AST-парсера, який перехоплює вузли дерева DOM і замінює їх на типобезпечні оптимізовані компоненти.
Нижче наведено модульний комплекс на TypeScript 5.8 для Next.js 16 (App Router), що включає відмовостійкий API-клієнт із валідацією Zod та рекурсивний AST-парсер контенту з нативними компонентами next/image та next/link.
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import parse, { DOMNode, Element, domToReact } from 'html-react-parser';
import { z } from 'zod';
/**
* Схема валідації поста WordPress через Zod.
* Захищає рантайм застосунку від пошкоджених даних API.
*/
export const WpPostSchema = z.object({
id: z.number(),
slug: z.string(),
date: z.string(),
title: z.object({
rendered: z.string(),
}),
content: z.object({
rendered: z.string(),
}),
excerpt: z.object({
rendered: z.string(),
}),
featured_media: z.number().default(0),
_embedded: z.record(z.string(), z.unknown()).optional(),
});
export type WpPost = z.infer<typeof WpPostSchema>;
interface FetchOptions extends RequestInit {
revalidateTags?: string[];
revalidateTime?: number | false;
}
/**
* Відмовостійкий типізований клієнт WP REST API.
* Реалізує експоненційний бекофф і валідацію Zod.
*/
export class WpApiClient {
private baseUrl: string;
constructor(baseUrl?: string) {
this.baseUrl = baseUrl || process.env.WORDPRESS_INTERNAL_API_URL || 'https://cms.agenticlab.com/wp-json';
}
public async getPostBySlug(slug: string): Promise<WpPost | null> {
const endpoint = `/wp/v2/posts?slug=${encodeURIComponent(slug)}&_embed=true`;
const data = await this.fetchWithRetry<unknown[]>(endpoint, {
revalidateTags: ['posts', `post:${slug}`],
});
if (!Array.isArray(data) || data.length === 0) {
return null;
}
const validated = WpPostSchema.safeParse(data[0]);
if (!validated.success) {
console.error('[WpApiClient] Schema validation error:', validated.error.format());
throw new Error(`Invalid API response format for post ${slug}`);
}
return validated.data;
}
private async fetchWithRetry<T>(endpoint: string, options: FetchOptions = {}, retries = 2): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers = new Headers(options.headers);
headers.set('Accept', 'application/json');
// M2M автентифікація через закритий токен
if (process.env.WORDPRESS_M2M_SECRET) {
headers.set('X-Agentic-M2M-Secret', process.env.WORDPRESS_M2M_SECRET);
}
const nextConfig: { tags?: string[]; revalidate?: number | false } = {};
if (options.revalidateTags) {
nextConfig.tags = options.revalidateTags;
}
if (options.revalidateTime !== undefined) {
nextConfig.revalidate = options.revalidateTime;
}
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
...options,
headers,
next: nextConfig,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return (await response.json()) as T;
} catch (error) {
if (attempt === retries) {
throw error;
}
// Затримка з експоненційним бекоффом: 200ms, 400ms...
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 200));
}
}
throw new Error('Unreachable fetch failure');
}
}
/**
* Пропси компонента безпечного рендерингу контенту.
*/
interface WpContentRendererProps {
html: string;
allowedOrigins?: string[];
}
/**
* Високопродуктивний AST-парсер контенту WordPress.
* Автоматично підміняє <img> на next/image та <a> на next/link.
*/
export const WpContentRenderer: React.FC<WpContentRendererProps> = ({
html,
allowedOrigins = ['https://cms.agenticlab.com'],
}) => {
const options = {
replace: (domNode: DOMNode) => {
if (!(domNode instanceof Element)) {
return;
}
// 1. Оптимізація тегів <img> через next/image
if (domNode.name === 'img') {
const { src, alt, width, height, class: className } = domNode.attribs;
if (!src) {
return null;
}
const isAllowedOrigin = allowedOrigins.some((origin) => src.startsWith(origin));
const numWidth = width ? parseInt(width, 10) : 800;
const numHeight = height ? parseInt(height, 10) : 500;
return (
<Image
src={src}
alt={alt || 'WordPress Post Asset'}
width={isNaN(numWidth) ? 800 : numWidth}
height={isNaN(numHeight) ? 500 : numHeight}
className={className || 'rounded-lg object-cover my-6'}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
unoptimized={!isAllowedOrigin}
/>
);
}
// 2. Трансформація внутрішніх посилань <a> у next/link
if (domNode.name === 'a') {
const { href, class: className, target, rel } = domNode.attribs;
if (!href) {
return;
}
const isInternal = href.startsWith('/') || allowedOrigins.some((origin) => href.startsWith(origin));
if (isInternal) {
// Нормалізація абсолютного посилання на відносне
let internalHref = href;
for (const origin of allowedOrigins) {
internalHref = internalHref.replace(origin, '');
}
if (internalHref === '') {
internalHref = '/';
}
return (
<Link
href={internalHref}
className={className || 'text-accent-cyan hover:underline transition-colors'}
prefetch={false}
>
{domToReact(domNode.children as DOMNode[], options)}
</Link>
);
}
// Зовнішні посилання: безпечні атрибути rel
return (
<a
href={href}
target={target || '_blank'}
rel={rel || 'noopener noreferrer'}
className={className || 'text-accent-violet hover:underline'}
>
{domToReact(domNode.children as DOMNode[], options)}
</a>
);
}
},
};
return <div className="prose prose-invert max-w-none">{parse(html, options)}</div>;
};
Розгортання WordPress в ізольованій підмережі. Налаштування CORS для дозволу звернень виключно з IP-адрес серверів Next.js.
Унеможливлює прямий доступ публічного інтернету до ядра CMS.
Написання відмовостійкої обгортки fetch із передачею ключів автентифікації M2M та тегів кешу next: { tags: [...] }.
Забезпечує гранулярне інвалідування кешу на рівні окремих сутностей.
Створення Zod-схем для публікацій, сторінок, метаполів ACF та таксономій. Захист рантайму від пошкоджених відповідей API.
Гарантує 100% стабільність застосунку при оновленні плагінів у CMS.
Заміна стандартного рендерингу на WpContentRenderer. Автоматична оптимізація зображень через next/image та клієнтська навігація через next/link.
Виводить показники LCP та CLS у зелену зону Core Web Vitals.
Глибока інтеграція Next.js 16 та WordPress 7.0+ ґрунтується на трьох системних принципах:
dangerouslySetInnerHTML на користь рекурсивної трансформації розмітки в нативні компоненти next/image та next/link.