Интеграция CMS Headless WordPress (WP REST API + фронтенд)
Headless WordPress — архитектура, где WordPress отвечает только за управление контентом и предоставляет его через API. Фронтенд полностью отдельный: Next.js, Nuxt, React SPA или любой другой стек. Редакторы работают в привычном интерфейсе WordPress, разработчики получают свободу выбора технологий на фронте.
Когда это оправдано
Headless добавляет сложность. Оправдан, когда:
- уже есть React/Next.js-фронтенд и нужна CMS под него;
- контент нужно отдавать на несколько платформ (сайт + мобильное приложение + newsletter);
- требуется ISR/SSG на Next.js с обновлением без полного деплоя;
- команда фронтендеров не хочет работать с PHP-шаблонами.
Не нужен, если сайт строится с нуля и нет жёсткого требования к стеку фронтенда — обычный WordPress проще.
WP REST API: базовые эндпоинты
WordPress REST API включён из коробки с версии 4.7. Базовый URL: https://site.com/wp-json/wp/v2/.
# Список постов
GET /wp-json/wp/v2/posts?per_page=10&page=1&_fields=id,title,slug,date,excerpt
# Один пост по slug
GET /wp-json/wp/v2/posts?slug=my-post-slug
# Страницы
GET /wp-json/wp/v2/pages?slug=about
# Таксономии
GET /wp-json/wp/v2/categories
GET /wp-json/wp/v2/tags?post=123
# Custom Post Type (должен быть зарегистрирован с show_in_rest=true)
GET /wp-json/wp/v2/portfolio?per_page=6&acf_format=standard
Параметр _fields критически важен — по умолчанию ответ содержит десятки полей, большинство из которых не нужны:
GET /wp-json/wp/v2/posts?_fields=id,title,slug,date,featured_media,excerpt,_links
Настройка WordPress для headless
Отключить тему (фронтенд не нужен):
// functions.php
add_action('template_redirect', function () {
if (!is_admin() && !is_user_logged_in()) {
// Перенаправить все фронтенд-запросы на фронтенд-домен
if (!str_starts_with($_SERVER['REQUEST_URI'], '/wp-json')) {
wp_redirect('https://frontend.site.com' . $_SERVER['REQUEST_URI'], 301);
exit;
}
}
});
CORS для запросов с фронтенда:
add_action('rest_api_init', function () {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', function ($value) {
$allowed_origins = [
'https://frontend.site.com',
'http://localhost:3000',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins, true)) {
header("Access-Control-Allow-Origin: {$origin}");
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
header('Access-Control-Allow-Credentials: true');
}
return $value;
});
}, 15);
Расширение REST API своими данными
ACF-поля в REST API через плагин acf-to-rest-api или вручную:
// Добавить ACF-поля в ответ REST API для portfolio
add_action('rest_api_init', function () {
register_rest_field('portfolio', 'acf', [
'get_callback' => function ($post) {
return get_fields($post['id']);
},
'schema' => ['type' => 'object'],
]);
// Добавить URL изображения прямо в ответ
register_rest_field('post', 'featured_image_url', [
'get_callback' => function ($post) {
$id = $post['featured_media'];
if (!$id) return null;
$img = wp_get_attachment_image_src($id, 'large');
return $img ? $img[0] : null;
},
'schema' => ['type' => ['string', 'null']],
]);
});
Кастомный эндпоинт для нестандартных запросов:
add_action('rest_api_init', function () {
register_rest_route('app/v1', '/home', [
'methods' => 'GET',
'callback' => function (WP_REST_Request $request) {
return rest_ensure_response([
'hero' => get_fields(get_option('home_hero_page_id')),
'featured' => array_map(
fn($post) => [
'id' => $post->ID,
'title' => get_the_title($post),
'slug' => $post->post_name,
'image' => get_the_post_thumbnail_url($post, 'medium'),
],
get_posts(['post_type' => 'portfolio', 'posts_per_page' => 3])
),
]);
},
'permission_callback' => '__return_true',
]);
});
Интеграция с Next.js
// lib/wordpress.ts
const WP_API = process.env.WP_API_URL; // https://cms.site.com/wp-json/wp/v2
export interface WpPost {
id: number;
slug: string;
title: { rendered: string };
excerpt: { rendered: string };
date: string;
featured_image_url: string | null;
acf?: Record<string, unknown>;
}
export async function getPosts(params: {
perPage?: number;
page?: number;
category?: number;
} = {}): Promise<{ posts: WpPost[]; total: number; totalPages: number }> {
const url = new URL(`${WP_API}/posts`);
url.searchParams.set('per_page', String(params.perPage ?? 10));
url.searchParams.set('page', String(params.page ?? 1));
url.searchParams.set('_fields', 'id,slug,title,excerpt,date,featured_image_url,acf');
if (params.category) {
url.searchParams.set('categories', String(params.category));
}
const res = await fetch(url.toString(), {
next: { revalidate: 60 }, // ISR: обновлять не чаще раза в минуту
});
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}/posts?slug=${slug}&_fields=id,slug,title,content,date,featured_image_url,acf`,
{ next: { revalidate: 300 } }
);
const data = await res.json();
return data[0] ?? null;
}
Preview Mode
Для предпросмотра черновиков из WordPress в Next.js:
// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get('secret');
const postSlug = searchParams.get('slug');
if (secret !== process.env.WP_PREVIEW_SECRET || !postSlug) {
return new Response('Invalid token', { status: 401 });
}
draftMode().enable();
redirect(`/blog/${postSlug}`);
}
В WordPress добавляем кнопку «Preview» которая редиректит на:
https://frontend.site.com/api/preview?secret=SECRET&slug=POST_SLUG
Кэширование на стороне WordPress
REST API не кэшируется по умолчанию. Добавляем кэш через транзиент:
add_filter('rest_post_query', function ($args, $request) {
// Кэшируем только GET-запросы от неавторизованных
if ($request->get_method() === 'GET' && !is_user_logged_in()) {
$cache_key = 'rest_' . md5(serialize($args));
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached; // не совсем правильно для query args, но как иллюстрация
}
}
return $args;
}, 10, 2);
Более надёжное решение — Redis Object Cache или Nginx-кэш перед WordPress для REST-эндпоинтов только для анонимных запросов.
On-demand Revalidation (Next.js ISR)
WordPress уведомляет Next.js о публикации поста:
// В WordPress: webhook при сохранении поста
add_action('save_post', function (int $postId, WP_Post $post) {
if ($post->post_status !== 'publish') return;
wp_remote_post('https://frontend.site.com/api/revalidate', [
'method' => 'POST',
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode([
'secret' => getenv('REVALIDATE_SECRET'),
'slug' => $post->post_name,
'type' => $post->post_type,
]),
'timeout' => 5,
]);
}, 10, 2);
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(req: Request) {
const { secret, slug, type } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
const path = type === 'post' ? `/blog/${slug}` : `/${slug}`;
revalidatePath(path);
revalidatePath('/blog'); // обновляем список статей
return Response.json({ revalidated: true });
}
Сроки
Настройка WordPress под headless (CORS, ACF в API, кастомные эндпоинты) — 6–8 часов. Интеграция с Next.js (клиент API, ISR, preview mode) — 1–1,5 рабочих дня. Вебхуки on-demand revalidation — 3–4 часа.







