Ієрархія шаблонів WordPress (Template Hierarchy) є декларативною підсистемою маршрутизації представлень (View Routing Subsystem), яка зіставляє внутрішній стан глобального запиту WP_Query з файловою структурою активної теми. Після завершення фази розбору URL (WP::parse_request) та виконання SQL-запиту у wp-blog-header.php, ядро передає керування диспетчеру wp-includes/template-loader.php.
У сучасному WordPress (версії 6.7–7.0+ «Armstrong») паралельно функціонують два рушії резолвінгу шаблонів:
is_single(), is_archive(), is_page()) та каскадному пошуку файлів .php від найбільш специфічних (що містять слаг або ID сутності) до універсального запасного варіанта index.php. Вибір здійснюється через функцію get_query_template($type, $templates) з перевіркою файлової системи через locate_template().wp_is_block_theme()). Архітектура FSE докорінно змінює вектор пошуку: шаблони зберігаються у форматі блокової HTML-розмітки (templates/{slug}.html, parts/{slug}.html). При цьому ядро реалізує чотирирівневий пріоритет: користувацькі модифікації з бази даних (Custom Post Types wp_template та wp_template_part, створені в Site Editor) мають абсолютний пріоритет над файлами дочірньої теми; файли дочірньої теми — над батьківською, а на рівні плагінів використовується нативний API register_block_template() (введений у WP 6.7 та розширений у WP 7.0+). Рендеринг блокового дерева делегується обробнику wp-includes/template-canvas.php.Точкою розширення для розробників виступає конвеєр фільтрів: {$type}_template_hierarchy (модифікація списку кандидатів до звернення до диска), {$type}_template (фільтрація шляху конкретного типу), template_include (фінальна підміна виконуваного файлу), register_block_template() (програмна реєстрація блокового шаблону з плагіна) та екшен template_redirect (контрольна точка перед формуванням виводу).
Маршрутизацію шаблонів слід розглядати як дворівневий низхідний конвеєр: спочатку обчислюється семантичний тип запиту за прапорцями WP_Query, після чого запускається каскадний резолвер з перевіркою шарів зберігання (База даних $\rightarrow$ Дочірня тема $\rightarrow$ Батьківська тема $\rightarrow$ Фолбек ядра).
[Вхідний запит: WP::main()]
│
▼
[WP_Query компілює прапорці]
(is_single, is_page, is_category, is_404...)
│
▼
[wp-includes/template-loader.php]
│
Перевірка: wp_is_block_theme()?
┌──────────────┴──────────────┐
▼ ▼
ТАК (Block Theme) НІ (Classic Theme)
│ │
locate_block_template() get_query_template()
│ │
1. CPT wp_template в БД? │
(Правки Site Editor) │
2. Child: /templates/*.html 1. Child: child-theme/*.php
3. Parent: /templates/*.html 2. Parent: parent-theme/*.php
│ │
▼ ▼
template-canvas.php template_include фільтр
│ │
▼ ▼
Рендеринг блоків Gutenberg Підключення знайденого PHP-файлуОбчислення булевих умовних тегів (is_front_page, is_single, is_archive, is_search, is_404) на основі розібраних query_vars.
Генерація впорядкованого масиву імен файлів від вузькоспецифічних до загальних. Прогін через фільтр {$type}_template_hierarchy.
Для FSE-тем: перевірка наявності опублікованого поста CPT wp_template або wp_template_part із відповідним слагом.
Перевірка наявності файлу в STYLESHEETPATH (дочірня тема), потім у TEMPLATEPATH (батьківська тема) через locate_template().
Фінальна фільтрація через template_include та запуск буфера виводу через template-canvas.php (для блоків) або прямий include.
your-theme/my-plugin/single-item.php), збереженням каскаду дочірніх тем та фолбеком на дистрибутив плагіна.template_redirect для генерації відповідей Edge-кешу, SSR-проксіювання на зовнішні сервіси (Next.js, Remix) або видачі високошвидкісних JSON API відповідей до завантаження важких ресурсів теми.single_template_hierarchy на основі cookie сегмента аудиторії, геолокації або підписки користувача без зміни канонічного URL та без зовнішніх HTTP 302 редиректів.header.php, footer.php) на блокові фрагменти за допомогою виклику block_template_part('header'), що дозволяє контент-менеджерам використовувати редактор Gutenberg без переписування всього бекенду теми.Для усвідомленого вибору між типами тем та проєктування розширюваних інтерфейсів необхідно зіставити архітектурні відмінності трьох підходів.
У класичному конвеєрі ядро послідовно перебирає умовні теги у wp-includes/template-loader.php. Розглянемо детальний порядок пошуку для трьох ключових сценаріїв:
single-{post_type})Якщо запитано пост CPT project зі слагом alpha та ID 42:
single-project-alpha.php — шаблон для конкретного запису за слагом.single-project.php — шаблон для всіх записів типу project.single.php — загальний шаблон для будь-яких одиночних записів.singular.php — універсальний шаблон для всіх одиничних сутностей (пости, сторінки, CPT, медіафайли).index.php — фінальний системний фолбек.taxonomy-{taxonomy})Якщо запитано терм fintech таксономії industry:
taxonomy-industry-fintech.php — шаблон для конкретного терма за слагом.taxonomy-industry.php — шаблон для всієї таксономії industry.taxonomy.php — загальний шаблон для будь-яких кастомних таксономій.archive.php — загальний шаблон для всіх типів архівів (автори, дати, категорії, CPT).index.php — фінальний фолбек.front-page.php vs home.phpfront-page.php. Якщо його немає, завантажується home.php, а за його відсутності — index.php.front-page.php (найвищий пріоритет). Якщо його немає — застосовується стандартна ієрархія сторінок (Custom Template $\rightarrow$ page-{slug}.php $\rightarrow$ page.php $\rightarrow$ singular.php $\rightarrow$ index.php).home.php, потім index.php.Коли активна блокова тема (wp_is_block_theme() === true), функція locate_block_template() виконує пошук за таким суворим алгоритмом:
[Запит шаблону: "single-project"]
│
▼
1. Пошук у БД: CPT wp_template (post_name = 'single-project', status = 'publish')
└── Знайдено в БД? ──► ТАК ──► Рендеринг із БД (користувацький шаблон із Site Editor)
│ НІ
▼
2. Пошук у ФС Дочірньої теми: {STYLESHEETPATH}/templates/single-project.html
└── Знайдено на диску? ──► ТАК ──► Рендеринг файлу дочірньої теми
│ НІ
▼
3. Пошук у ФС Батьківської теми: {TEMPLATEPATH}/templates/single-project.html
└── Знайдено на диску? ──► ТАК ──► Рендеринг файлу батьківської теми
│ НІ
▼
4. Перехід до наступного менш специфічного кандидата ієрархії (single.html ──► index.html)<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Template Hierarchy Engine
* Description: Високошвидкісний резолвер шаблонів з in-memory кешуванням та підтримкою theme overrides.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Templates;
if (!defined('ABSPATH')) {
exit;
}
final class EnterpriseTemplateEngine
{
private static array $resolvedCache = [];
private static string $pluginTemplateDir = '';
public static function init(string $pluginPath): void
{
self::$pluginTemplateDir = trailingslashit($pluginPath) . 'templates/';
// Вбудовування в ланцюжок резолвінгу ієрархії ядра (First-Class Callable PHP 8.4+)
add_filter('template_include', self::resolveTemplateCascade(...), 99);
add_filter('single_template_hierarchy', self::injectCustomHierarchyCandidates(...), 10, 1);
}
/**
* Динамічне розширення списку кандидатів ієрархії для кастомних типів записів.
* Дозволяє додавати кандидатів на основі метаданих, регіону або A/B сегмента.
*/
public static function injectCustomHierarchyCandidates(array $templates): array
{
if (!is_singular('project')) {
return $templates;
}
$projectId = get_queried_object_id();
$projectLayout = get_post_meta($projectId, '_project_layout_variant', true);
if (!empty($projectLayout) && is_string($projectLayout)) {
// Впроваджуємо специфічний шаблон варіанта з найвищим пріоритетом
array_unshift($templates, sprintf('single-project-variant-%s.php', sanitize_file_name($projectLayout)));
}
return $templates;
}
/**
* Фінальний каскадний резолвінг шаблону із захистом від зайвих звернень до диска (I/O Bottlenecks).
*/
public static function resolveTemplateCascade(string $defaultTemplate): string
{
// Пропускаємо нестандартні запити (REST, AJAX, CLI)
if (wp_is_json_request() || (defined('DOING_AJAX') && DOING_AJAX)) {
return $defaultTemplate;
}
$cacheKey = md5((string)($_SERVER['REQUEST_URI'] ?? '') . ':' . (int)is_user_logged_in());
if (isset(self::$resolvedCache[$cacheKey])) {
return self::$resolvedCache[$cacheKey];
}
// Приклад: перехоплення виводу архіву проєктів
if (is_post_type_archive('project')) {
$candidates = [
'agentic-lab/archive-project.php', // Пріоритет 1: Папка в темі оформлення
'archive-project.php', // Пріоритет 2: Корінь теми
];
$locatedInTheme = locate_template($candidates, false, false);
if (!empty($locatedInTheme)) {
self::$resolvedCache[$cacheKey] = $locatedInTheme;
return $locatedInTheme;
}
// Пріоритет 3: Дефолтний шаблон плагіна
$fallbackPluginTemplate = self::$pluginTemplateDir . 'archive-project.php';
if (file_exists($fallbackPluginTemplate)) {
self::$resolvedCache[$cacheKey] = $fallbackPluginTemplate;
return $fallbackPluginTemplate;
}
}
self::$resolvedCache[$cacheKey] = $defaultTemplate;
return $defaultTemplate;
}
/**
* Публічний хелпер для підключення ізольованих частин шаблону (Template Parts)
* з передачею типізованого контексту без засмічення глобальної області видимості.
*/
public static function renderPart(string $slug, array $context = []): void
{
$templateFile = locate_template([
sprintf('agentic-lab/parts/%s.php', $slug),
sprintf('parts/%s.php', $slug),
], false, false);
if (empty($templateFile)) {
$templateFile = self::$pluginTemplateDir . sprintf('parts/%s.php', $slug);
}
if (!file_exists($templateFile)) {
error_log(sprintf('[AGENTIC-LAB] Template part "%s" not found at path: %s', $slug, $templateFile));
return;
}
// Ізольоване замикання для виключення витоку $context у глобальний scope
(static function(string $__path, array $__args): void {
extract($__args, EXTR_SKIP);
include $__path;
})($templateFile, $context);
}
}
// Ініціалізація сервісу
EnterpriseTemplateEngine::init(WP_CONTENT_DIR . '/plugins/agentic-lab-core/');У чому суть помилки: Розробник вішає колбек на екшен template_redirect та виводить HTML, JSON або виконує echo до завершення роботи завантажувача шаблонів.
Чому розробники так роблять: Спроба швидко обробити вебхук, форму або віддати кастомну відповідь до завантаження важкого шаблону теми.
Що відбувається в продакшені: Спрацьовування echo негайно надсилає тіло відповіді та HTTP-заголовки. WordPress надалі намагається надіслати канонічні заголовки та кукі, що генерує фатальну помилку Warning: Cannot modify header information - headers already sent. Плагіни повносторінкового кешування кешують неповний вивід.
Як правильно: Якщо на стадії template_redirect формується кастомна відповідь, виконання скрипта ЗОБОВ'ЯЗАНЕ завершуватися негайно через exit; або системні функції wp_send_json() / wp_die():
// ПОМИЛКА:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
echo json_encode(['status' => 'ok']); // Headers already sent!
}
});
// ПРАВИЛЬНО:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
wp_send_json(['status' => 'ok'], 200); // Надсилає правильні заголовки та викликає exit
}
});У чому суть помилки: Використання константи TEMPLATEPATH або функції get_template_directory() для підключення підшаблонів через include: include get_template_directory() . '/parts/header.php';.
Чому розробники так роблять: Здається очевидним способом отримати абсолютний шлях до файлів теми.
Що відбувається в продакшені: Повна втрата підтримки дочірніх тем (Child Themes). Якщо клієнт або розробник створює дочірню тему та перевизначає файл parts/header.php, WordPress продовжить завантажувати файл із батьківської теми, ігноруючи кастомізацію.
Як правильно: Завжди використовувати функцію get_template_part() або locate_template(), які перевіряють спочатку дочірню тему (STYLESHEETPATH), а потім батьківську (TEMPLATEPATH):
// ПОМИЛКА: Ігнорує дочірню тему
require get_template_directory() . '/parts/card.php';
// ПРАВИЛЬНО (стандартний механізм):
get_template_part('parts/card', null, ['data' => $itemData]);
// ПРАВИЛЬНО (отримання шляху для прямого контролю):
$file = locate_template(['parts/card.php']);
if (!empty($file)) {
include $file;
}У чому суть помилки: Розробник створює файл home.php, очікуючи, що він відображатиме головну сторінку сайту, коли в налаштуваннях читання (Settings -> Reading) увімкнено статичну головну сторінку.
Чому розробники так роблять: У побутовому розумінні «home» асоціюється з головною сторінкою сайту.
Що відбувається в продакшені: За специфікацією ядра WordPress:
front-page.php ЗАВЖДИ відповідає за головну сторінку (статичну або стрічку записів).home.php відповідає за сторінку блогу (стрічку останніх записів), коли статична головна сторінка увімкнена.
У результаті верстка головної сторінки відображається на сторінці новин, а головна сторінка виводить стандартний page.php.Як правильно:
front-page.php.home.php.У чому суть помилки: Розробник вносить зміни у файл теми templates/single.html через Git/IDE, деплоїть на сервер, але зміни не відображаються на сайті.
Чому розробники так роблять: Звичка до класичної розробки, де файл на диску завжди визначає вивід.
Що відбувається в продакшені: Якщо адміністратор сайту хоча б один раз натиснув кнопку «Зберегти» у візуальному Site Editor (Gutenberg FSE), ядро створює запис у таблиці wp_posts з типом wp_template. Згідно з архітектурою FSE, запис із БД має абсолютний пріоритет над файлом теми. Зміни у файлі на диску ігноруватимуться доти, доки шаблон у БД не буде скинуто (Clear Customizations).
Як правильно: Під час розробки блокових тем скидати користувацькі кастомізації через Site Editor перед деплоєм або керувати шаблонами через фільтр pre_get_block_template.
Диспетчер template-loader.php опитує прапорці запиту: is_404(), is_search(), is_front_page(), is_home(), is_singular(), is_archive().
Умовні теги обчислюються суворо на основі властивостей $wp_query->is_*.
Ядро викликає get_query_template($type), де $type — визначений тип сторінки (наприклад, single, page, category).
Запускається динамічний фільтр {$type}_template_hierarchy, куди плагіни можуть впровадити додаткові імена файлів.
Якщо активна блокова тема, ядро перевіряє таблицю wp_posts на наявність CPT wp_template із зазначеним post_name. За відсутності запису перевіряються файли /templates/*.html теми.
При виявленні блокового шаблону керування передається в template-canvas.php.
Для класичних тем перебираються кандидати в дочірній темі (STYLESHEETPATH), потім у батьківській темі (TEMPLATEPATH).
Перший знайдений на диску файл вважається переможцем. Якщо нічого не знайдено, береться index.php теми.
Шлях до знайденого файлу пропускається через фільтр template_include. Повернутий файл підключається через include $template.
Якщо фільтр повернув порожній рядок або false, рендеринг припиняється без виводу.