Headless WordPress: інтеграція WP REST API з React і Next.js
Типова проблема: бізнесу потрібен керований контент у WordPress, але готові теми не забезпечують потрібну продуктивність або кастомізацію. Рішення — headless CMS, де WordPress залишається бекендом, а фронтенд будується на React, Vue, Next.js або Svelte. Це позбавляє від монолітних тем, прискорює завантаження сторінок (LCP знижується на 30–50%) і спрощує підтримку. За 5+ років ми реалізували понад 50 таких проєктів — від інтернет-магазинів до корпоративних порталів. Бюджет інтеграції порівнянний з вартістю одного місяця роботи фронтенд-розробника, але окупається за рахунок зниження витрат на хостинг і прискорення виведення нових сторінок.
Які завдання вирішує headless WordPress?
- Відділення контенту від представлення: редактори працюють у звичній адмінці WordPress, а розробники використовують сучасний стек (React, Next.js, Vue). Більше не потрібно підганяти шаблони під дизайн — API віддає чисті дані.
- Підвищення продуктивності: статичні сторінки (SSG) та ISR скорочують TTFB до 200 мс, а LCP — до 2 с. Економія на серверних ресурсах сягає 40% за рахунок кешування.
- Єдине API для всіх клієнтів: один WordPress може обслуговувати SPA, мобільний додаток і телеграм-бота — через REST або GraphQL.
Базова інтеграція: отримання даних
WordPress REST API за замовчуванням доступний за /wp-json/wp/v2/. Отримання останніх постів:
// lib/wordpress.ts const WP_API_URL = process.env.NEXT_PUBLIC_WP_URL + '/wp-json/wp/v2'; export interface WPPost { id: number; slug: string; title: { rendered: string }; content: { rendered: string }; excerpt: { rendered: string }; date: string; featured_media: number; _embedded?: { 'wp:featuredmedia'?: [{ source_url: string; alt_text: string }]; 'wp:term'?: Array<Array<{ id: number; name: string; slug: string }>>; }; } export async function getPosts(params: { perPage?: number; page?: number; category?: number; search?: string; } = {}): Promise<{ posts: WPPost[]; total: number; totalPages: number }> { const qs = new URLSearchParams({ per_page: String(params.perPage ?? 12), page: String(params.page ?? 1), _embed: 'wp:featuredmedia,wp:term', ...(params.category && { categories: String(params.category) }), ...(params.search && { search: params.search }), }); const res = await fetch(`${WP_API_URL}/posts?${qs}`, { next: { revalidate: 60 }, // ISR в Next.js 13+ }); if (!res.ok) throw new Error(`WP API error: ${res.status}`); return { posts: await res.json(), total: Number(res.headers.get('X-WP-Total')), totalPages: Number(res.headers.get('X-WP-TotalPages')), }; } export async function getPostBySlug(slug: string): Promise<WPPost | null> { const res = await fetch(`${WP_API_URL}/posts?slug=${slug}&_embed=wp:featuredmedia,wp:term`); const posts = await res.json(); return posts.length ? posts[0] : null; } Next.js App Router: динамічні маршрути
// app/blog/[slug]/page.tsx import { getPostBySlug, getPosts } from '@/lib/wordpress'; import { notFound } from 'next/navigation'; export async function generateStaticParams() { const { posts } = await getPosts({ perPage: 100 }); return posts.map(post => ({ slug: post.slug })); } export async function generateMetadata({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); if (!post) return {}; return { title: post.title.rendered, description: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160), }; } export default async function PostPage({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); if (!post) notFound(); const media = post._embedded?.['wp:featuredmedia']?.[0]; return ( <article className="post-single"> {media && ( <img src={media.source_url} alt={media.alt_text} className="post-single__cover" /> )} <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} /> <div className="post-content" dangerouslySetInnerHTML={{ __html: post.content.rendered }} /> </article> ); } dangerouslySetInnerHTML тут допустимий — контент від довіреного сервера, але якщо джерело під вашим контролем, додайте DOMPurify для безпеки.
Кастомний хук для React SPA
// hooks/usePosts.ts import useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then(r => r.json()); export function usePosts(category?: string, page = 1) { const params = new URLSearchParams({ per_page: '12', page: String(page), _embed: '1' }); if (category) params.set('categories', category); const { data, error, isLoading } = useSWR<WPPost[]>( `/wp-json/wp/v2/posts?${params}`, fetcher, { revalidateOnFocus: false } ); return { posts: data ?? [], isLoading, error }; } Використовуйте цей хук з пагінацією та скелетонами. Приклад компонента — в документації SWR.
Як налаштувати on-demand ISR для миттєвого оновлення контенту?
Next.js підтримує перебудову сторінок за тригером. Коли пост публікується в WordPress, PHP-код надсилає POST-запит на ендпоінт /api/revalidate у Next.js:
// WordPress: save_post hook і CORS filter add_action('save_post', function (int $post_id, WP_Post $post): void { if ($post->post_status !== 'publish') return; $next_url = get_option('nextjs_revalidate_url'); $secret = get_option('nextjs_revalidate_secret'); if (!$next_url || !$secret) return; wp_remote_post("{$next_url}/api/revalidate", [ 'body' => json_encode([ 'secret' => $secret, 'path' => '/' . $post->post_type . '/' . $post->post_name, ]), 'headers' => ['Content-Type' => 'application/json'], 'blocking'=> false, ]); }, 10, 2); add_filter('rest_pre_serve_request', function ($value) { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $allowed = ['https://mysite.com', 'https://www.mysite.com', 'http://localhost:3000']; if (in_array($origin, $allowed)) { header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Authorization, Content-Type'); } return $value; }); А на стороні Next.js обробляємо:
// Next.js: app/api/revalidate/route.ts import { revalidatePath } from 'next/cache'; export async function POST(req: Request) { const { secret, path } = await req.json(); if (secret !== process.env.REVALIDATE_SECRET) { return Response.json({ error: 'Invalid secret' }, { status: 401 }); } revalidatePath(path); return Response.json({ revalidated: true, path }); } Цей пайплайн забезпечує оновлення сторінок за секунди без повної перебудови сайту. Економія трафіку — до 40% порівняно з повною регенерацією.
Продуктивність: що кешувати
| Дані | Стратегія |
|---|---|
| Список постів | ISR, revalidate: 60s |
| Одиночний пост | ISR + on-demand revalidate при save_post |
| Меню навігації | Static (revalidate: false) |
| Пошукові результати | SSR (без кешу, параметри змінюються) |
| ACF-поля налаштувань сайту | Static або revalidate: 3600s |
Порівняння REST і GraphQL
| Критерій | REST | GraphQL |
|---|---|---|
| Гнучкість запитів | Фіксовані поля | Вибір лише потрібних даних |
| Кількість запитів | Часто декілька | Один |
| Складність налаштування | Мінімальна | Потрібен плагін та схема |
| Продуктивність | Висока для простих сторінок | Краще для складних вкладень |
Як проходить інтеграція headless WordPress
- Аудит — розбираємо поточну архітектуру WordPress, плагіни, обсяг контенту, навантаження.
- Проектування — визначаємо необхідні endpoint’и, поля, стратегію кешування (ISR, SSG, SSR).
- Налаштування API — розширюємо REST або встановлюємо WPGraphQL, додаємо custom fields (ACF).
- Розробка фронтенду — створюємо типізований клієнт (TypeScript), компоненти, маршрутизацію.
- Інтеграція — підключаємо CORS, on-demand revalidation, деплоїмо WordPress і фронтенд.
- Тестування — перевіряємо продуктивність (Core Web Vitals), крос-доменні запити, обробку помилок.
- Документація та передача — записуємо відео-консультацію для команди, передаємо репозиторій.
Терміни: базова інтеграція — від 5 до 7 робочих днів, складні сценарії (GraphQL, ACF, on-demand ISR) — до 15 днів. Точну оцінку даємо після аудиту.
Деталі налаштування CORS
При різних доменах WordPress і фронтенду додаємо фільтр rest_pre_serve_request, який встановлює заголовки для дозволених джерел. Це стандартна практика, описана в документації REST API. Якщо потрібна нестандартна конфігурація — ми підлаштуємо під ваш проєкт.
Скільки коштує інтеграція headless WordPress?
Вартість розраховується індивідуально на основі обсягу контенту, кількості типів записів, необхідності GraphQL та складності маршрутизації. У середньому бюджет порівнянний з місячною зарплатою молодшого розробника, а економія на хостингу та часі розробки перекриває інвестиції протягом півроку.
Хочете отримати точну оцінку? Замовте аудит вашого проєкту — ми проаналізуємо архітектуру і запропонуємо оптимальне рішення. Зв'яжіться з нами через форму на сайті або напишіть у Telegram.







