В современной веб-разработке экосистема готовых тем и визуальных конструкторов (Elementor, Divi, WPBakery) часто воспринимается новичками как стандарт индустрии. Однако на реальных коммерческих проектах этот подход приводит к катастрофическим последствиям для бизнеса:
!important.Разработка кастомной темы с нуля (Custom Theme from Scratch) — это профессиональный инженерный подход, при котором разработчик проектирует исключительно Presentation Layer приложения.
В результате вы получаете абсолютный контроль над каждым тегом HTML5, идеальные показатели Google PageSpeed (100/100 для Mobile и Desktop), нулевую зависимость от сторонних плагинов и максимальную безопасность.
Тема оформления отвечает исключительно за визуальное представление данных (UI, CSS, клиентский JavaScript, семантический HTML-скелет).
Бизнес-логика, пользовательские типы записей (Custom Post Types), таксономии и обработка форм обязаны находиться в специализированных плагинах (Data & Business Logic Layer). Если заказчик переключает тему сайта, визуальное оформление меняется, но ни одна единица данных не должна исчезнуть.
Классическая тема WordPress — это не монолитный исполняемый сервис, а набор шаблонов-обработчиков, которые ядро WordPress вызывает в ответ на входящий HTTP-запрос.
После того как ядро инициализировало окружение, подключилось к базе данных MySQL и выполнило разбор URL в глобальный объект запроса $wp_query, управление передается в скрипт wp-includes/template-loader.php. Этот скрипт реализует алгоритм Template Hierarchy (Иерархия шаблонов):
[ Входящий HTTP-запрос к WordPress ]
│
▼
[ Парсинг URL и построение глобального WP_Query ]
│
▼
[ Файл wp-includes/template-loader.php ]
│
┌──────────────────┴──────────────────┐
▼ ▼
(Запрос одиночной статьи) (Запрос архива рубрики)
│ │
1. single-post-{slug}.php 1. category-{slug}.php
2. single-post.php 2. category-{id}.php
3. single.php 3. category.php
4. singular.php 4. archive.php
│ │
└──────────────────┬──────────────────┘
▼
[ Финальный универсальный фолбэк: ]
index.phpЯдро WordPress требует наличия всего двух обязательных файлов в директории wp-content/themes/your-theme-name/:
style.css: Главный стилевой манифест темы. Он выполняет двойную роль: во-первых, объявляет метаданные темы (название, автор, версия, текстовый домен) в специальном комментарии заголовка, без которого админ-панель не распознает директорию как тему. Во-вторых, может содержать глобальные CSS-стили.index.php: Абсолютный резервный обработчик каскада иерархии. Если для отображаемой страницы не найдено более специфичного шаблона (например, single.php или category.php), ядро всегда загрузит index.php.Третьим важнейшим (но формально опциональным) файлом является functions.php. Он автоматически загружается ядром при каждом веб-запросе и в CLI-окружении, выступая точкой входа для регистрации сервисов темы, подписки на хуки жизненного цикла и подключения клиентских ассетов.
Создание изолированной директории в wp-content/themes/. Заполнение обязательного мета-заголовка в style.css и создание базового index.php с резервным циклом The Loop.
Без заголовка 'Theme Name:' в style.css тема не появится в списке доступных в консоли 'Внешний вид -> Темы'.
Подключение декларативных возможностей ядра через add_theme_support: генерация тега <title>, поддержка post-thumbnails, семантическая разметка HTML5 и регистрация областей навигации (register_nav_menus).
Инициализация возможностей строго изолируется на событии after_setup_theme.
Инициализация package.json, установка Vite 6 и Tailwind CSS v4. Настройка локального сервера с Hot Module Replacement (HMR) и компиляцией в /dist с генерацией manifest.json.
Позволяет писать современный CSS с токенами и TypeScript с мгновенным обновлением стилей без перезагрузки страницы.
Регистрация стилей и скриптов через функции wp_enqueue_style() и wp_enqueue_script() на хуке wp_enqueue_scripts. Автоматическое чтение manifest.json для хеширования файлов в продакшене.
Прямая вставка тегов <link> и <script> в header.php категорически запрещена стандартами разработки WordPress.
Разделение разметки на переиспользуемые фрагменты: шапка с хуком wp_head(), подвал с хуком wp_footer() и семантический цикл The Loop с обязательным экранированием данных.
Хуки wp_head() и wp_footer() критически необходимы для инъекции стилей, скриптов и метатегов плагинов.
Активация строгого режима типов declare(strict_types=1), аудит доступности (a11y) и проверка метрик Core Web Vitals в Google Lighthouse.
Качественная тема отдает первую отрисовку (FCP) менее чем за 0.5 секунды при минимальном размере CSS менее 30 КБ.
Что произойдет, если в файле header.php кастомной темы случайно удалить вызов функции wp_head()?
Разработка чистых тем с нуля востребована в следующих коммерческих сценариях:
Для создания темы уровня Enterprise организуем модульную структуру файлов:
wp-content/themes/agenticlab-theme/
├── assets/ # Исходные файлы фронтенда
│ ├── css/
│ │ └── main.css # Стили с директивами Tailwind v4
│ └── js/
│ └── app.ts # Клиентская логика на TypeScript
├── dist/ # Скомпилированные ассеты (артефакты Vite)
│ ├── .vite/manifest.json # Карта хешированных файлов
│ └── assets/
├── inc/ # Модульная PHP-логика (сервисы темы)
│ ├── setup.php # Поддержка темы, меню, миниатюры
│ ├── assets.php # Enqueue стилей и скриптов с Vite HMR
│ └── template-tags.php # Вспомогательные функции рендеринга
├── template-parts/ # Переиспользуемые фрагменты разметки
│ ├── header/
│ ├── footer/
│ └── post/card.php
├── functions.php # Главный бутстрап темы (declare strict)
├── header.php # Открывающие теги, DOCTYPE, wp_head()
├── footer.php # Подвал, wp_footer(), закрывающие теги
├── index.php # Главный фолбэк-шаблон с The Loop
├── package.json # Зависимости Node.js (Vite, Tailwind)
├── vite.config.ts # Конфигурация сборщика
└── style.css # Декларативный заголовок метаданныхVite компилирует TypeScript и стили Tailwind в директорию /dist. В режиме production генерируется манифест .vite/manifest.json с уникальными хешами файлов для сброса кэша браузера.
Ядро WordPress загружает functions.php. Подключаются изолированные модули из папки /inc/. На хуке after_setup_theme регистрируются title-tag, post-thumbnails и меню.
Функция wp_enqueue_scripts считывает манифест Vite. В dev-режиме подключается клиентский скрипт Vite для HMR, а в production — скомпилированные минифицированные бандлы.
Ядро выбирает соответствующий шаблон (например, single.php или index.php). Шаблон открывает get_header(), прокручивает цикл have_posts() -> the_post() и закрывает get_footer().
Браузер получает чистый HTML5 со 100% семантикой. Стили и скрипты загружаются асинхронно, обеспечивая время отрисовки контента (FCP) менее 0.5 с.
В чем заключается принципиальная разница между функциями get_template_directory_uri() и get_stylesheet_directory_uri()?
Ниже представлены ключевые производственные файлы эталонной темы с поддержкой строгого типизирования PHP 8.2+ и современного конвейера сборки на Vite 6.
style.css#/*
Theme Name: Fullstack Agentic Lab Production Engine
Theme URI: https://agenticlab.dev/workflows/wordpress/wf-theme-from-scratch
Author: Artur Yusupov (Fullstack Agentic Lab)
Author URI: https://agenticlab.dev
Description: Высокопроизводительная модульная тема WordPress, созданная с нуля по стандарту Fullstack Agentic Lab 2.0 (Vite 6, Tailwind v4, PHP 8.2+).
Version: 1.0.0
Requires at least: 6.4
Requires PHP: 8.2
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: agenticlab-engine
Tags: custom, fast, tailwind, vite, clean-code
*/functions.php#<?php
/**
* Fullstack Agentic Lab Production Engine — Main Theme Bootstrap.
*
* @package AgenticLabEngine
*/
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit; // Защита от прямого вызова файла вне контекста WordPress
}
// Константы темы для оптимизации резолвинга путей
define('AGENTIC_THEME_VERSION', '1.0.0');
define('AGENTIC_THEME_DIR', get_template_directory());
define('AGENTIC_THEME_URI', get_template_directory_uri());
// Модульное подключение сервисов
require_once AGENTIC_THEME_DIR . '/inc/setup.php';
require_once AGENTIC_THEME_DIR . '/inc/assets.php';
require_once AGENTIC_THEME_DIR . '/inc/template-tags.php';inc/setup.php#<?php
/**
* Theme Setup and Core Supports.
*
* @package AgenticLabEngine
*/
declare(strict_types=1);
add_action('after_setup_theme', 'agenticlab_setup_theme_features');
function agenticlab_setup_theme_features(): void {
// Автоматическое управление мета-тегом <title> ядром WordPress
add_theme_support('title-tag');
// Поддержка миниатюр постов (Featured Images)
add_theme_support('post-thumbnails');
// Семантическая разметка HTML5 для стандартных компонентов
add_theme_support('html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script',
]);
// Активация кастомного логотипа
add_theme_support('custom-logo', [
'height' => 80,
'width' => 240,
'flex-height' => true,
'flex-width' => true,
]);
// Регистрация меню навигации
register_nav_menus([
'primary_menu' => esc_html__('Основная навигация', 'agenticlab-engine'),
'footer_menu' => esc_html__('Навигация в подвале', 'agenticlab-engine'),
]);
}inc/assets.php с поддержкой Vite#<?php
/**
* Asset Enqueue & Vite 6 Integration Resolver.
*
* @package AgenticLabEngine
*/
declare(strict_types=1);
add_action('wp_enqueue_scripts', 'agenticlab_enqueue_theme_assets');
function agenticlab_enqueue_theme_assets(): void {
// Базовый style.css для соблюдения стандарта ядра
wp_enqueue_style(
'agenticlab-root-style',
get_stylesheet_uri(),
[],
AGENTIC_THEME_VERSION
);
$manifest_path = AGENTIC_THEME_DIR . '/dist/.vite/manifest.json';
$is_development = defined('WP_ENVIRONMENT_TYPE') && WP_ENVIRONMENT_TYPE === 'development';
if ($is_development && agenticlab_is_vite_dev_server_running()) {
// РЕЖИМ РАЗРАБОТКИ: Инъекция HMR клиента Vite и исходников TypeScript
wp_enqueue_script_module(
'vite-client',
'http://localhost:5173/@vite/client',
[],
null
);
wp_enqueue_script_module(
'agenticlab-app-dev',
'http://localhost:5173/assets/js/app.ts',
['vite-client'],
null
);
return;
}
// ПРОДАКШЕН: Чтение скомпилированного .vite/manifest.json
if (!file_exists($manifest_path)) {
return;
}
$manifest = json_decode((string) file_get_contents($manifest_path), true);
if (!is_array($manifest)) {
return;
}
// Подключение скомпилированного JS
if (isset($manifest['assets/js/app.ts']['file'])) {
wp_enqueue_script(
'agenticlab-app',
AGENTIC_THEME_URI . '/dist/' . $manifest['assets/js/app.ts']['file'],
[],
AGENTIC_THEME_VERSION,
['strategy' => 'defer', 'in_footer' => true]
);
}
// Подключение скомпилированного CSS из манифеста
if (isset($manifest['assets/js/app.ts']['css'][0])) {
wp_enqueue_style(
'agenticlab-main-css',
AGENTIC_THEME_URI . '/dist/' . $manifest['assets/js/app.ts']['css'][0],
[],
AGENTIC_THEME_VERSION
);
}
}
/**
* Проверка доступности локального dev-сервера Vite.
*/
function agenticlab_is_vite_dev_server_running(): bool {
static $running = null;
if ($running !== null) {
return $running;
}
$handle = @fsockopen('localhost', 5173, $errno, $errstr, 0.05);
if (is_resource($handle)) {
fclose($handle);
$running = true;
return true;
}
$running = false;
return false;
}header.php, index.php, footer.php#<?php
/**
* The Main Template File.
*
* @package AgenticLabEngine
*/
declare(strict_types=1);
get_header(); ?>
<main id="primary" class="site-main max-w-5xl mx-auto px-4 py-8">
<?php if (have_posts()) : ?>
<header class="page-header mb-8">
<h1 class="text-3xl font-extrabold text-slate-900 dark:text-white">
<?php single_post_title(); ?>
</h1>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<?php while (have_posts()) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class('bg-white dark:bg-slate-800 rounded-xl p-6 shadow-sm border border-slate-200 dark:border-slate-700'); ?>>
<?php if (has_post_thumbnail()) : ?>
<div class="post-thumbnail mb-4 overflow-hidden rounded-lg">
<a href="<?php echo esc_url(get_permalink()); ?>" aria-hidden="true" tabindex="-1">
<?php the_post_thumbnail('medium_large', ['class' => 'w-full h-48 object-cover hover:scale-105 transition-transform duration-300']); ?>
</a>
</div>
<?php endif; ?>
<h2 class="text-xl font-bold mb-2">
<a href="<?php echo esc_url(get_permalink()); ?>" class="text-slate-900 dark:text-white hover:text-cyan-500 transition-colors">
<?php the_title(); ?>
</a>
</h2>
<div class="entry-summary text-slate-600 dark:text-slate-300 text-sm mb-4">
<?php the_excerpt(); ?>
</div>
<footer class="entry-footer text-xs text-slate-400">
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
</footer>
</article>
<?php endwhile; ?>
</div>
<div class="pagination-wrapper mt-8">
<?php the_posts_pagination([
'prev_text' => esc_html__('← Назад', 'agenticlab-engine'),
'next_text' => esc_html__('Вперед →', 'agenticlab-engine'),
]); ?>
</div>
<?php else : ?>
<section class="no-results not-found py-12 text-center">
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100">
<?php esc_html_e('Ничего не найдено', 'agenticlab-engine'); ?>
</h2>
<p class="text-slate-500 mt-2">
<?php esc_html_e('По вашему запросу записи отсутствуют.', 'agenticlab-engine'); ?>
</p>
</section>
<?php endif; ?>
</main>
<?php get_footer();Почему разработчики так делают: Новички копируют статическую HTML-верстку и вставляют <link rel="stylesheet" href="..."> прямо внутрь тега <head> в файле header.php.
Что происходит в продакшене:
defer/async) через плагины оптимизации кэширования.Как правильно: Использовать исключительно системный хук wp_enqueue_scripts и функции wp_enqueue_style() / wp_enqueue_script():
add_action('wp_enqueue_scripts', function(): void {
wp_enqueue_style('custom-font', 'https://fonts.googleapis.com/...', [], null);
wp_enqueue_script('theme-logic', get_template_directory_uri() . '/js/main.js', [], '1.0.0', [
'strategy' => 'defer',
'in_footer' => true,
]);
});Почему разработчики так делают: functions.php кажется самым простым местом для вызова register_post_type('portfolio', ...) или регистрации шорткодов.
Что происходит в продакшене: Когда через 2 года клиент решает обновить визуальное оформление сайта и активирует новую тему (или дочернюю тему), весь контент раздела "Портфолио" мгновенно исчезает из админ-панели, а все страницы выдают ошибку 404 Not Found. Это грубейшее нарушение принципа разделения ответственности (Separation of Concerns).
Как правильно: Регистрировать CPT, таксономии и метабоксы исключительно в виде независимого Must-Use плагина (директория wp-content/mu-plugins/site-core-cpt.php) или стандартного плагина.
Почему разработчики так делают: Привычка писать echo get_post_meta($id, 'author_bio', true); или echo $_GET['search'];.
Что происходит в продакшене:
Открывается критическая уязвимость XSS (Stored & Reflected Cross-Site Scripting). Злоумышленник может внедрить вредоносный тег <script> в поле профиля или URL, который перехватит cookie администратора и передаст злоумышленнику полный контроль над сервером.
Как правильно: Всегда применять контекстные функции экранирования:
esc_html() — для обычного текста внутри HTML-тегов.esc_attr() — для значений внутри HTML-атрибутов (<input value="<?php echo esc_attr($val); ?>">).esc_url() — для ссылок и путей (<a href="<?php echo esc_url($link); ?>">).wp_kses_post() — если разрешен безопасный форматированный HTML (жирный шрифт, ссылки).// ПЛОХО (XSS Vulnerability):
echo get_post_meta(get_the_ID(), 'custom_subtitle', true);
// ПРАВИЛЬНО:
echo esc_html(get_post_meta(get_the_ID(), 'custom_subtitle', true));Почему разработчики так делают: Разработчик делает кастомную выборку new WP_Query(['post_type' => 'event']) внутри шаблона или сайдбара и забывает сбросить глобальное состояние.
Что происходит в продакшене:
Метод $query->the_post() перезаписывает глобальную переменную $post. Если после этого в шаблоне вызываются стандартные функции вроде the_title(), comments_template() или фоновые фильтры SEO-плагинов, они начинают работать с последней записью из кастомного запроса, а не с оригинальной страницей. Ломаются канонические URL, хлебные крошки и комментарии.
Как правильно: Всегда закрывать кастомные циклы вызовом wp_reset_postdata():
$featured_query = new WP_Query(['posts_per_page' => 3]);
if ($featured_query->have_posts()) {
while ($featured_query->have_posts()) {
$featured_query->the_post();
the_title('<h3>', '</h3>');
}
wp_reset_postdata(); // КРИТИЧНО: восстановление глобального $post
}style.css с метаданными заголовка и index.php как фолбэка каскада template-loader.php.wp_enqueue_scripts, предотвращая дублирование ассетов.esc_html, esc_attr, esc_url, wp_kses_post), исключая риски XSS-уязвимостей.