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.







