La Jerarquía de Plantillas de WordPress (Template Hierarchy) funciona como un subsistema declarativo de enrutamiento de vistas (View Routing Subsystem) que mapea el estado interno de la petición global WP_Query con la estructura de archivos del tema activo. Tras concluir la fase de análisis de URL (WP::parse_request) y la ejecución de la consulta SQL principal en wp-blog-header.php, el núcleo transfiere el control al despachador wp-includes/template-loader.php.
En el WordPress moderno (versiones 6.7 a 7.0+ «Armstrong») conviven dos motores de resolución de plantillas en paralelo:
is_single(), is_archive(), is_page()) y en una búsqueda en cascada de archivos .php desde los más específicos (que coinciden con el slug o el ID de la entidad) hasta el fallback genérico index.php. La selección se orquesta mediante get_query_template($type, $templates) con verificaciones del sistema de archivos delegadas a locate_template().wp_is_block_theme()). La arquitectura FSE transforma de raíz el vector de búsqueda: las plantillas se almacenan como marcado HTML basado en bloques (templates/{slug}.html, parts/{slug}.html). El núcleo impone una precedencia de cuatro capas: las modificaciones de base de datos (Custom Post Types wp_template y wp_template_part creados en el Site Editor) tienen prioridad absoluta sobre los archivos del tema hijo; los archivos del tema hijo sobre el tema padre, y a nivel de plugins se utiliza la API nativa register_block_template() (introducida en WP 6.7 y ampliada en WP 7.0+). El renderizado del árbol de bloques se delega a wp-includes/template-canvas.php.Los desarrolladores pueden extender este pipeline a través de filtros dedicados: {$type}_template_hierarchy (modificación del array de candidatos antes de acceder al disco), {$type}_template (filtrado de la ruta de un tipo específico), template_include (sustitución final del archivo ejecutable), register_block_template() (registro programático desde plugins) y la acción template_redirect (punto de control previo a la generación de salida).
Conciba el enrutamiento de plantillas como un pipeline descendente de dos fases: primero se calcula el tipo semántico de la petición mediante los flags de WP_Query, y a continuación se dispara el resolvedor en cascada a través de las capas de almacenamiento (Base de datos $\rightarrow$ Tema Hijo $\rightarrow$ Tema Padre $\rightarrow$ Fallback del Núcleo).
[Petición Entrante: WP::main()]
│
▼
[WP_Query Compila Flags]
(is_single, is_page, is_category, is_404...)
│
▼
[wp-includes/template-loader.php]
│
Comprobación: wp_is_block_theme()?
┌──────────────┴──────────────┐
▼ ▼
SÍ (Block Theme) NO (Classic Theme)
│ │
locate_block_template() get_query_template()
│ │
1. ¿CPT wp_template en BD? │
(Cambios de Site Editor) │
2. Child: /templates/*.html 1. Child: child-theme/*.php
3. Parent: /templates/*.html 2. Parent: parent-theme/*.php
│ │
▼ ▼
template-canvas.php Filtro template_include
│ │
▼ ▼
Renderizado de bloques Gutenberg Inclusión directa del archivo PHPEvaluación de etiquetas condicionales booleanas (is_front_page, is_single, is_archive, is_search, is_404) a partir de query_vars.
Generación de una lista ordenada de nombres de archivo de lo específico a lo genérico. Pasada por {$type}_template_hierarchy.
Para temas FSE: consulta de registros CPT wp_template o wp_template_part publicados que coincidan con el slug.
Verificación de existencia en STYLESHEETPATH (tema hijo) y luego en TEMPLATEPATH (tema padre) mediante locate_template().
Filtrado final mediante template_include y ejecución del buffer de salida vía template-canvas.php (bloques) o include directo.
your-theme/my-plugin/single-item.php), respetando la cascada de temas hijos y con fallback al paquete del plugin.template_redirect para entregar respuestas cacheadas en el Edge, actuar como proxy SSR hacia servicios externos (Next.js, Remix) o retornar payloads JSON ultra-rápidos antes de cargar recursos pesados del tema.single_template_hierarchy basada en cookies de segmentación, geolocalización o nivel de suscripción sin alterar la URL canónica ni emitir redirecciones HTTP 302.header.php, footer.php) hacia fragmentos de bloques mediante block_template_part('header'), permitiendo a los gestores de contenido usar el editor Gutenberg sin reescribir el backend heredado.Para seleccionar adecuadamente entre arquitecturas de temas y diseñar interfaces extensibles, compare las diferencias estructurales entre los tres enfoques.
En el pipeline clásico, el núcleo evalúa las etiquetas condicionales de manera secuencial dentro de wp-includes/template-loader.php. Analicemos el orden de búsqueda para tres casos esenciales:
single-{post_type})Si se solicita una entrada del CPT project con slug alpha e ID 42:
single-project-alpha.php — Plantilla específica por slug de la entrada.single-project.php — Plantilla genérica para todas las entradas de tipo project.single.php — Plantilla genérica para cualquier entrada individual.singular.php — Plantilla universal para cualquier entidad singular (posts, páginas, CPTs, adjuntos).index.php — Fallback final del sistema.taxonomy-{taxonomy})Si se solicita el término fintech de la taxonomía industry:
taxonomy-industry-fintech.php — Plantilla para el término específico por slug.taxonomy-industry.php — Plantilla para toda la taxonomía industry.taxonomy.php — Plantilla general para taxonomías personalizadas.archive.php — Plantilla general para todo tipo de archivos (autores, fechas, categorías, CPTs).index.php — Fallback final.front-page.php vs home.phpfront-page.php. Si no existe, recurre a home.php, y si falta, a index.php.front-page.php tiene la máxima prioridad. Si no existe, se aplica la jerarquía estándar de páginas (Custom Template $\rightarrow$ page-{slug}.php $\rightarrow$ page.php $\rightarrow$ singular.php $\rightarrow$ index.php).home.php, seguido de index.php.Cuando un tema de bloques está activo (wp_is_block_theme() === true), locate_block_template() ejecuta una búsqueda en capas estrictas:
[Solicitud de plantilla: "single-project"]
│
▼
1. Buscar en BD: CPT wp_template (post_name = 'single-project', status = 'publish')
└── ¿Coincidencia en BD? ──► SÍ ──► Renderizar contenido de BD (Personalización de Site Editor)
│ NO
▼
2. Buscar en Sistema de Archivos de Tema Hijo: {STYLESHEETPATH}/templates/single-project.html
└── ¿Existe en disco? ──► SÍ ──► Renderizar archivo del tema hijo
│ NO
▼
3. Buscar en Sistema de Archivos de Tema Padre: {TEMPLATEPATH}/templates/single-project.html
└── ¿Existe en disco? ──► SÍ ──► Renderizar archivo del tema padre
│ NO
▼
4. Fallback al siguiente candidato menos específico de la jerarquía (single.html ──► index.html)<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Template Hierarchy Engine
* Description: Resolvedor de plantillas de alto rendimiento con caché en memoria y cascada de sobrescritura de temas.
* 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/';
// Integración en la cadena de resolución del núcleo (PHP 8.4+ First-Class Callable Syntax)
add_filter('template_include', self::resolveTemplateCascade(...), 99);
add_filter('single_template_hierarchy', self::injectCustomHierarchyCandidates(...), 10, 1);
}
/**
* Inyección dinámica de plantillas candidatas para tipos de contenido personalizados.
* Facilita el enrutamiento de variantes según metadatos, región o segmentos de prueba 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)) {
// Anteponemos la plantilla de variante con máxima prioridad
array_unshift($templates, sprintf('single-project-variant-%s.php', sanitize_file_name($projectLayout)));
}
return $templates;
}
/**
* Resolución final en cascada con caché en memoria para eliminar cuellos de botella de I/O en disco.
*/
public static function resolveTemplateCascade(string $defaultTemplate): string
{
// Omitir peticiones que no entregan vistas HTML (REST, AJAX, WP-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];
}
// Ejemplo: Interceptar la vista de archivo del CPT project
if (is_post_type_archive('project')) {
$candidates = [
'agentic-lab/archive-project.php', // Prioridad 1: Subcarpeta en el tema
'archive-project.php', // Prioridad 2: Raíz del tema
];
$locatedInTheme = locate_template($candidates, false, false);
if (!empty($locatedInTheme)) {
self::$resolvedCache[$cacheKey] = $locatedInTheme;
return $locatedInTheme;
}
// Prioridad 3: Fallback a la plantilla integrada en el plugin
$fallbackPluginTemplate = self::$pluginTemplateDir . 'archive-project.php';
if (file_exists($fallbackPluginTemplate)) {
self::$resolvedCache[$cacheKey] = $fallbackPluginTemplate;
return $fallbackPluginTemplate;
}
}
self::$resolvedCache[$cacheKey] = $defaultTemplate;
return $defaultTemplate;
}
/**
* Helper aislado para renderizar partes de plantilla con contexto tipado sin contaminar el ámbito global.
*/
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;
}
// Clausura aislada para impedir fugas de $context hacia el scope global
(static function(string $__path, array $__args): void {
extract($__args, EXTR_SKIP);
include $__path;
})($templateFile, $context);
}
}
// Inicialización del servicio
EnterpriseTemplateEngine::init(WP_CONTENT_DIR . '/plugins/agentic-lab-core/');En qué consiste el error: Un desarrollador asocia un callback a template_redirect e imprime HTML, JSON o ejecuta sentencias echo antes de que el cargador de plantillas complete su ejecución.
Por qué lo hacen: Con la intención de procesar rápidamente un webhook, formulario o endpoint personalizado sin cargar la plantilla del tema.
Qué sucede en producción: La emisión de bytes envía de inmediato el cuerpo de la respuesta y las cabeceras HTTP. Cuando WordPress intenta enviar cabeceras canónicas o cookies más adelante, se dispara el error fatal Warning: Cannot modify header information - headers already sent. Los plugins de caché de página completa almacenan salidas incompletas.
Cómo resolverlo: Si en la fase template_redirect se genera una respuesta personalizada, la ejecución del script DEBE finalizar inmediatamente mediante exit; o funciones del núcleo como wp_send_json() o wp_die():
// ERROR:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
echo json_encode(['status' => 'ok']); // Headers already sent!
}
});
// CORRECTO:
add_action('template_redirect', function(): void {
if (is_page('api-proxy')) {
wp_send_json(['status' => 'ok'], 200); // Envía cabeceras correctas e invoca exit()
}
});En qué consiste el error: Usar la constante TEMPLATEPATH o la función get_template_directory() para incluir fragmentos de plantilla directamente: include get_template_directory() . '/parts/header.php';.
Por qué lo hacen: Parece la forma más directa de obtener la ruta absoluta hacia los archivos del tema.
Qué sucede en producción: Se anula por completo la compatibilidad con temas hijos (Child Themes). Si un desarrollador o cliente crea un tema hijo y sobrescribe parts/header.php, WordPress continuará ejecutando el archivo del tema padre, ignorando la personalización.
Cómo resolverlo: Utilice siempre get_template_part() o locate_template(), los cuales comprueban primero el tema hijo (STYLESHEETPATH) antes de recurrir al tema padre (TEMPLATEPATH):
// ERROR: Ignora las sobrescrituras del tema hijo
require get_template_directory() . '/parts/card.php';
// CORRECTO (Mecanismo estándar):
get_template_part('parts/card', null, ['data' => $itemData]);
// CORRECTO (Resolución de ruta para control directo):
$file = locate_template(['parts/card.php']);
if (!empty($file)) {
include $file;
}En qué consiste el error: Un ingeniero crea home.php esperando que muestre la página principal cuando en Ajustes $\rightarrow$ Lectura se selecciona «Una página estática».
Por qué lo hacen: En el lenguaje común «home» se asocia intuitivamente a la página inicial o portada.
Qué sucede en producción: Según la especificación del núcleo de WordPress:
front-page.php SIEMPRE gobierna la portada del sitio (ya sea estática o listado de entradas).home.php representa la página de entradas del blog cuando se ha designado una página estática para la portada.
En consecuencia, el diseño de la portada termina mostrándose en el blog, mientras la página principal recurre a page.php.Cómo resolverlo:
front-page.php.home.php.En qué consiste el error: Un desarrollador modifica templates/single.html en el tema mediante Git/IDE y despliega en producción, pero los cambios no se reflejan en la web.
Por qué lo hacen: Inercia del desarrollo clásico donde el archivo en disco siempre define el renderizado.
Qué sucede en producción: Cuando un administrador pulsa «Guardar» en el Site Editor (Gutenberg FSE), el núcleo crea un registro en la tabla wp_posts con el tipo wp_template. En la arquitectura FSE, este registro en base de datos tiene prioridad absoluta sobre el archivo del tema. Las modificaciones en disco serán ignoradas hasta que se limpien las personalizaciones de la base de datos (Clear Customizations).
Cómo resolverlo: Restablecer las personalizaciones visuales en el Site Editor antes del despliegue o gestionar las modificaciones programáticamente con el filtro pre_get_block_template.
El despachador template-loader.php inspecciona los flags booleanos: is_404(), is_search(), is_front_page(), is_home(), is_singular(), is_archive().
Las etiquetas condicionales se derivan directamente de las propiedades $wp_query->is_*.
El núcleo ejecuta get_query_template($type), donde $type representa el contexto detectado (single, page, category, archive).
Dispara el filtro dinámico {$type}_template_hierarchy, permitiendo a plugins añadir nombres de archivo candidatos.
Si el tema de bloques está activo, el núcleo busca en wp_posts un CPT wp_template con el post_name correspondiente. Si falta, comprueba /templates/*.html.
Al localizar una plantilla de bloques, el flujo pasa a template-canvas.php.
Para temas clásicos, se examinan los candidatos en el tema hijo (STYLESHEETPATH) y luego en el tema padre (TEMPLATEPATH).
El primer archivo coincidente en disco resulta seleccionado. Si no hay candidatos específicos, se retorna index.php.
La ruta del archivo localizado pasa por el filtro template_include antes de cargarse mediante include $template.
Si el filtro retorna una cadena vacía o false, el renderizado se interrumpe sin emitir salida.