Інтеграція Headless WordPress (WP REST API + фронтенд)
Headless WordPress — архітектура, де WordPress керує контентом і надає його через API. Фронтенд повністю окремий: Next.js, Nuxt, React SPA або будь-який інший стек. Редактори працюють у знайомому інтерфейсі WordPress, розробники отримують свободу вибору технологій на фронте.
Коли це оправдано
Headless додає складність. Оправдано, коли:
- вже є React/Next.js-фронтенд і потрібна CMS під нього;
- контент потрібно видавати на кілька платформ (сайт + мобільний додаток + розсилка);
- потрібна 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;
}
Терміни
Налаштування WordPress під headless (CORS, ACF в API, кастомні ендпоінти) — 6–8 годин. Інтеграція з Next.js (клієнт API, ISR, preview mode) — 1–1,5 робочих дня. Вебгаки on-demand revalidation — 3–4 години.







