Lógica condicional de WordPress para taxonomías

Lógica condicional de WordPress para taxonomías

Última verificación: 13 de octubre de 2026
15 min de lectura
Guía
Desarrollador full-stack

La lógica condicional en WordPress no es un montón de if repartidos por functions.php. Es un contrato con la consulta principal: el núcleo decide el contexto de la petición HTTP, las etiquetas condicionales leen ese contexto y tu código reacciona. En 2026, con temas de bloques y Full Site Editing (FSE), PHP sigue gobernando assets, capabilities, caché y cualquier rama que el editor del sitio no sabe expresar.

Esta guía trata la lógica condicional como ingeniería: cuándo is_singular() te engaña, cuándo in_the_loop() falla fuera del Loop, por qué current_user_can() y la caché de página no se mezclan y cómo aislar funciones auxiliares que se puedan probar. La taxonomía (in_category frente a has_term) llega al final como caso concreto, no como único tema.

#De dónde leen el estado las etiquetas condicionales

Las etiquetas condicionales no inspeccionan la URL a mano. Después de parse_request y WP_Query, el objeto global $wp_query guarda flags (is_single, is_page, is_front_page, is_category y otras). Funciones como is_singular() e is_front_page() son envoltorios sobre esas flags.

Consecuencia práctica: si llamas a una etiqueta condicional demasiado pronto, antes de wp / template_redirect, el resultado es indefinido o falso. El manual lo dice sin rodeos: una etiqueta condicional puesta directamente en functions.php no funciona, tiene que ir dentro de una función enganchada a una acción o filtro posterior. En plugins, el sitio seguro para ramas de plantilla y encolado es wp_enqueue_scripts, template_redirect o hooks posteriores documentados en Hooks.

Otra consecuencia: una WP_Query secundaria (widget, bloque de entradas recientes, shortcode) no cambia las flags de la consulta principal. is_singular() sigue describiendo la página pedida, no la entrada que el widget está recorriendo. Para datos de una entrada concreta, pasa el ID o el objeto WP_Post a las APIs de entradas y términos.

#Errores comunes con is_singular e is_front_page

is_singular() devuelve verdadero para una entrada, página o CPT individual. No distingue el tipo. En tiendas WooCommerce is_singular() es verdadero en product, y por eso muchos temas cargan el CSS del blog en la ficha de producto. Prefiere is_singular( 'post' ), is_page() o is_singular( array( 'post', 'page' ) ) con la lista explícita.

is_front_page() e is_home() siguen siendo la confusión clásica:

  • Con «Tus últimas entradas» en Ajustes > Lectura, la portada es is_front_page() e is_home() a la vez.
  • Con una página estática como portada y otra como «página de entradas», la portada es solo is_front_page(); el listado de entradas es is_home() y no is_front_page().

El código que hace if ( is_home() ) para «mostrar el hero de la portada» se rompe en cuanto el cliente cambia la lectura a página estática. El patrón estable para la portada pública es is_front_page(). Para el archivo del blog, usa is_home() && ! is_front_page().

Un caso de producción visto en Gdynia: un tema cargaba el slider solo con is_home(). Tras migrar la portada a una página hecha con bloques, el slider desapareció y el archivo /blog/ heredó el JavaScript del carrusel. Una línea, is_front_page(), habría bastado.

is_singular() también es falso en peticiones que al usuario le parecen «una página» pero no son singulares en la consulta: búsqueda (is_search()), 404 (is_404()) y peticiones REST, que no pasan por la consulta principal del tema. Antes de suponer que «hay una entrada actual», comprueba get_queried_object() y su tipo.

#Cómo usar in_the_loop y has_term fuera del Loop

Funciones como the_title(), the_content() y varias condicionales de entrada (in_category() sin ID) dependen de la entrada global que fija el Loop. in_the_loop() indica si el código corre entre while ( have_posts() ) : the_post(); y el final de ese Loop.

Fuera del Loop, en header.php, en un filtro body_class o en wp_footer, in_category( 'noticias' ) sin segundo argumento lee la entrada global, que puede ser la última de la consulta anterior, una entrada de un widget o null. Síntoma típico: la clase CSS «categoría noticias» aparece en el archivo, o el aviso legal solo en el último artículo del listado.

Patrón seguro:

function wppoland_is_news_singular(): bool {
	if ( ! is_singular( 'post' ) ) {
		return false;
	}

	$post_id = (int) get_queried_object_id();
	if ( $post_id <= 0 ) {
		return false;
	}

	return has_term( 'noticias', 'category', $post_id );
}

Usa get_queried_object_id() para el documento de la petición; usa el ID de la entrada dentro de un Loop secundario cuando recorres resultados. No mezcles los dos.

in_the_loop() ayuda a decidir si puedes fiarte de la entrada global. Si estás fuera del Loop y necesitas datos del documento actual, no «entres» en el Loop de forma artificial solo para leer meta: léelo por el ID de la consulta principal.

Para código que corre sin contexto de petición, como un trabajo en segundo plano o un endpoint REST, is_object_in_term( $object_id, $taxonomy, $terms ) recibe el ID del objeto como primer argumento y no necesita entrada global.

#current_user_can y ramas para usuarios conectados

current_user_can( 'edit_posts' ) y similares leen la sesión y las capabilities del usuario actual. Son la API correcta para interfaz de administración, atajos de edición y contenido solo para editores.

Tres reglas que evitan incidentes:

  1. Nunca mezcles HTML distinto para anónimos y conectados en el mismo documento si ese documento sale de una caché de página compartida. El edge o el plugin de caché guarda la variante del primer visitante. Un editor ve el botón «Editar»; el siguiente anónimo recibe el mismo HTML.
  2. Para ramas de usuarios conectados, usa cookies de bypass de caché (lo habitual en la mayoría de plugins de caché de página), ESI, o sirve la rama solo mediante JavaScript/REST autenticado después del HTML estático.
  3. Las capabilities no son roles. Comprueba current_user_can( 'manage_options' ), no current_user_can( 'administrator' ): lo segundo no es el contrato estable de la API.

En temas clásicos un patrón frecuente es ocultar la barra de administración a los suscriptores. Hazlo con un filtro en show_admin_bar, no con CSS condicional en el HTML cacheado:

add_filter(
	'show_admin_bar',
	static function ( $show ) {
		if ( is_user_logged_in() && ! current_user_can( 'edit_posts' ) ) {
			return false;
		}
		return $show;
	}
);

El filtro corre en la petición PHP; la caché de página anónima nunca lo ve porque el usuario conectado ya está fuera de esa caché.

#Cuándo usar la jerarquía de plantillas y cuándo condicionales PHP

La jerarquía de plantillas resuelve archivos (single-product.php, category-noticias.php, front-page.php). Las etiquetas condicionales resuelven ramas dentro de un archivo compartido (header.php, functions.php, un parcial).

Prefiere la jerarquía cuando el marcado diverge de forma estructural: un CPT con maquetación propia merece single-{post_type}.php (o el equivalente en un tema de bloques: una plantilla HTML dedicada). Prefiere condicionales PHP cuando la diferencia es pequeña: un aviso, un encolado, una clase en body.

Antipatrón: un único page.php genérico con veinte if ( is_page( 'contacto' ) ) encadenados. Eso duplica la jerarquía en PHP, complica la revisión de código e impide que FSE (o un tema hijo) sustituya solo una plantilla. Saca las páginas con marcado distinto a plantillas propias; deja las condicionales para la variación fina.

Regla práctica: si la rama cabe en un nombre de archivo de la jerarquía, usa el archivo. Si la rama depende de una capability, una cookie, un test A/B o una feature flag, usa PHP (o un bloque con lógica de servidor desde un plugin).

#Qué límites tiene FSE en los temas de bloques

FSE lleva plantillas y partes de plantilla a HTML dentro del editor del sitio. No elimina las etiquetas condicionales.

El editor del sitio cubre la variación visual por tipo de plantilla (singular, archivo, 404). No cubre de forma fiable:

  • Encolado condicional de scripts pesados (mapas, calculadoras, reproductores).
  • Ramas por capability (current_user_can).
  • Lógica de taxonomía recursiva (el padre incluye a los hijos).
  • Integraciones ERP o WooCommerce que dependen de meta del producto en la consulta actual.
  • Cualquier cosa que necesite una WP_Query secundaria con argumentos dinámicos.

Los temas de bloques siguen cargando functions.php. Ahí viven wp_enqueue_scripts, los filtros de body_class y las funciones auxiliares de términos. Los bloques con «variaciones» en el editor no sustituyen a has_term() cuando la regla de negocio es «si el producto está en cualquier descendiente de ropa».

Límite operativo: el marcado, solo en el editor; las decisiones de ejecución, en PHP. Mezclar las dos capas sin un contrato claro (por ejemplo, un patrón de bloque que da por hecho is_front_page() y un plugin que altera la consulta principal) produce regresiones difíciles de reproducir en staging.

#Taxonomías: in_category frente a has_term

in_category() es un atajo histórico para la taxonomía category. No cubre product_cat, post_tag ni taxonomías registradas con register_taxonomy(). Su página de referencia recoge dos parámetros: la categoría (ID, nombre, slug o un array de ellos) y una entrada opcional.

has_term( $term, $taxonomy, $post = null ) es la API general. Para código nuevo, prefiere has_term() incluso con categorías nativas: la firma es explícita y la misma función auxiliar sirve para WooCommerce y para CPTs.

Las comprobaciones nativas son coincidencia exacta. Una entrada que solo está en manzana no cumple in_category( 'fruta' ) por el hecho de que manzana sea hija de fruta. El núcleo no ofrece «está en esta rama del árbol» en una sola llamada. Usa get_term_children() (y object cache) en una función auxiliar:

function wppoland_post_in_term_branch( int $post_id, string $term_slug, string $taxonomy ): bool {
	$term = get_term_by( 'slug', $term_slug, $taxonomy );
	if ( ! $term instanceof WP_Term ) {
		return false;
	}

	if ( has_term( (int) $term->term_id, $taxonomy, $post_id ) ) {
		return true;
	}

	$children = get_term_children( (int) $term->term_id, $taxonomy );
	if ( is_wp_error( $children ) || $children === [] ) {
		return false;
	}

	return has_term( $children, $taxonomy, $post_id );
}

Trampa de WooCommerce: is_product_category() describe el archivo de categoría de producto, no el producto dentro del Loop. WooCommerce tiene sus propias condicionales de página (is_product(), is_shop(), is_product_category()), y todas describen la petición. Dentro de un loop de productos usa has_term( $slug, 'product_cat', $post_id ). Más detalle en comprobar términos de taxonomías personalizadas y en mostrar la categoría actual y la categoría padre.

#Caché de página con usuarios conectados en WordPress

La caché de página (nginx fastcgi_cache, Cloudflare, plugins de caché de página completa) sirve HTML ya hecho a los anónimos. Cualquier if ( is_user_logged_in() ) o current_user_can() incrustado en ese HTML es una fuga de interfaz o, en sentido contrario, contenido que falta.

Patrones que funcionan:

  • Separar: HTML público idéntico para todos los anónimos; la interfaz de usuarios conectados mediante un endpoint autenticado o un fragmento sin caché.
  • Bypass: las cookies de sesión hacen que el proxy salte la caché para los usuarios conectados (coste: menor tasa de aciertos).
  • No condicionar assets críticos de la portada con is_user_logged_in() en el PHP del documento cacheado.

Condicionales «seguras» bajo caché de página anónima: is_front_page(), is_singular( 'post' ), has_term( $slug, 'category', $queried_id ), todo lo que depende solo de la URL y la consulta públicas. Condicionales «peligrosas» en el mismo documento: capabilities, preferencias del usuario, geolocalización por IP sin vary, tests A/B sin cookie de vary.

En proyectos con caché en el edge dentro de la UE, documenta la matriz de vary (cookie, cabecera) junto a la función auxiliar. Sin eso, el siguiente plugin de «barra de promoción para clientes» vuelve a compartir HTML.

#Cómo probar etiquetas condicionales con PHPUnit

Las condicionales en línea dentro de plantillas son difíciles de probar. Extrae predicados puros (o casi puros) que reciban el estado de forma explícita:

function wppoland_should_load_map_assets( bool $is_contact_page, bool $user_can_edit ): bool {
	return $is_contact_page || $user_can_edit;
}

En el tema:

add_action(
	'wp_enqueue_scripts',
	static function () {
		$should = wppoland_should_load_map_assets(
			is_page( 'contacto' ),
			current_user_can( 'edit_pages' )
		);
		if ( ! $should ) {
			return;
		}
		wp_enqueue_script( 'wppoland-maps', get_theme_file_uri( 'assets/maps.js' ), array(), '1.0.0', true );
	}
);

En PHPUnit (con la suite de tests de WordPress o con dobles) pruebas wppoland_should_load_map_assets( true, false ) sin arrancar $wp_query. El mismo patrón vale para las ramas de taxonomía: pasa $post_id y slugs, no leas globales dentro del predicado.

Convenciones útiles:

  • Prefijo de proyecto en el nombre (wppoland_) para evitar colisiones.
  • Retorno bool explícito; nada de imprimir HTML dentro de la función auxiliar.
  • Una función auxiliar responde a una pregunta de negocio («¿es un single de noticias?»), no es un switch con efectos secundarios.

#Rendimiento de has_term y get_term_children

has_term() y las etiquetas condicionales sobre la consulta principal son baratas cuando el object cache está caliente. get_term_children() en taxonomías profundas puede salir más caro; memoriza el resultado por petición (un static local o wp_cache_get / wp_cache_set con grupo propio).

Evita llamar al mismo predicado decenas de veces dentro de un Loop: calcúlalo una vez antes del Loop o guárdalo por $post_id. En archivos con 30 productos y una comprobación recursiva de product_cat, la diferencia se nota en el TTFB en hosting compartido.

No uses SQL crudo como «atajo» de categoría si has_term() basta: pierdes el object cache y la preparación de $wpdb que el núcleo ya hace.

#is_main_query y pre_get_posts: cambiar solo la consulta principal

Para cambiar lo que muestra un archivo (número de entradas, excluir una categoría, orden), el hook correcto es pre_get_posts, no una WP_Query nueva dentro de la plantilla. El hook recibe el objeto WP_Query por referencia y se ejecuta para todas las consultas: la principal, las secundarias de widgets y bloques, y las del panel de administración.

Dos reglas de la referencia de pre_get_posts evitan la mayoría de los errores:

  • Usa los métodos del objeto recibido ($query->is_main_query(), $query->is_category()), no las funciones globales. La documentación explica que is_main_query() comprueba la instancia global $wp_query, no la que ha pasado el hook. La página de is_main_query() llama «circular» a ese uso.
  • Protege el panel con is_admin(). Sin esa guarda, la misma regla cambia también el listado de entradas en wp-admin.
add_action(
	'pre_get_posts',
	static function ( WP_Query $query ): void {
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		if ( $query->is_category( 'noticias' ) ) {
			$query->set( 'posts_per_page', 20 );
		}
	}
);

La misma referencia advierte de que, dentro de pre_get_posts, algunas etiquetas condicionales todavía no funcionan: is_front_page() falla, is_home() funciona. Si la regla es «solo en la portada estática», decide con $query->get( 'page_id' ) comparado con get_option( 'page_on_front' ), o lleva la lógica a template_redirect, donde la consulta ya está resuelta.

#Qué páginas cubre is_archive y cuándo usar is_tax

is_archive() es un agregado. En el código del núcleo (class-wp-query.php), la flag is_archive pasa a verdadero cuando la consulta es un archivo de tipo de contenido, de fecha, de autor, de categoría, de etiqueta o de taxonomía personalizada. El manual de etiquetas condicionales lo resume: las páginas de categoría, etiqueta, autor y fecha son todas archivos.

Esto tiene consecuencias directas:

  • is_archive() a secas carga el mismo CSS en el archivo de autor y en el archivo de product_cat. Si solo quieres archivos de categoría del blog, usa is_category().
  • is_tax() devuelve falso en archivos de categoría y de etiqueta. La referencia de is_tax() dice que es verdadero en archivos de taxonomías personalizadas y falso en las taxonomías integradas. Para cubrir las tres, escribe is_category() || is_tag() || is_tax().
  • is_post_type_archive( 'producto' ) solo es verdadero si el tipo de contenido se registró con has_archive => true, como recuerda el mismo manual.

Ninguna de estas funciones dice nada de una entrada individual. En un single, is_tax() es falso aunque la entrada tenga el término; ahí la pregunta correcta es has_term().

#is_page_template en temas de bloques

is_page_template() también ha cambiado de significado con los temas de bloques, sin cambiar de firma. En el núcleo (post-template.php), la función devuelve falso fuera de is_singular() y después compara el argumento con get_page_template_slug() del objeto pedido, es decir, con el valor guardado en la entrada.

En un tema clásico ese valor es el nombre del archivo PHP, así que is_page_template( 'landing.php' ) funciona. En un tema de bloques, las plantillas personalizadas registradas en customTemplates dentro de theme.json usan como name el nombre del archivo HTML sin extensión, según el manual de plantillas personalizadas. Una plantilla /templates/landing.html se comprueba con is_page_template( 'landing' ). Copiar condiciones de un tema clásico a uno de bloques sin ajustar este argumento produce un if que nunca es verdadero, sin aviso ni error.

#Lista de comprobación para la lógica condicional en WordPress

  1. ¿La etiqueta condicional se ejecuta cuando la consulta principal ya está lista?
  2. ¿is_front_page() frente a is_home() encaja con Ajustes > Lectura?
  3. ¿Las comprobaciones de términos y categorías usan un ID de entrada explícito fuera del Loop?
  4. ¿Alguna rama depende de current_user_can() en HTML servido desde caché de página?
  5. ¿La diferencia de marcado cabe en la jerarquía de plantillas en lugar de en un if gigante?
  6. ¿La función auxiliar tiene tests o, como mínimo, casos manuales documentados (anónimo, editor, archivo, singular)?
  7. ¿Los callbacks de pre_get_posts usan $query->is_main_query() y excluyen is_admin()?

#Resumen

Las etiquetas condicionales leen la consulta principal; trátalas como una API de contexto, no como un parser de URL. Separa la jerarquía de plantillas (estructura) del PHP condicional (variación fina y ejecución). En FSE el editor cubre las plantillas; PHP sigue cubriendo assets, capabilities y reglas de taxonomía. has_term() sustituye a in_category() en código nuevo; las ramas de usuarios conectados se quedan fuera del HTML anónimo cacheado. Las funciones auxiliares con argumentos explícitos mantienen la lógica revisable y comprobable.

Una lógica condicional mal colocada en una plantilla es el tipo de error que solo aparece en la entrada que nadie probó. Si quieres que revise esas plantillas alguien que ya ha cazado ese error antes, mira nuestra práctica de desarrollo WordPress.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

¿Quieres implementar esto en tu sitio?

Si quieres transformar el artículo en mejoras concretas, rediseño o un plan de implementación, puedo cerrar el alcance y ejecutar.

Cluster relacionado

Explora otros servicios WordPress y base de conocimiento

Refuerza tu negocio con soporte técnico profesional en áreas clave del ecosistema WordPress.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

¿Cuándo debo usar has_term() en lugar de in_category()?#
Usa has_term() siempre que trabajes con taxonomías personalizadas o quieras un enfoque más flexible. in_category() está limitado a la taxonomía de categorías integrada.
¿Cómo compruebo correctamente las categorías hijas en WordPress?#
Para lógica padre-hijo generalmente necesitas un helper que expanda los descendientes con get_term_children() y luego compruebe la pertenencia contra esa lista.
¿Pueden los condicionales de taxonomía afectar el rendimiento?#
Las comprobaciones simples de términos suelen ser baratas, pero las búsquedas recursivas y las llamadas repetidas dentro de bucles grandes pueden añadir sobrecarga. Los proyectos con mucho cache deben mantener esa lógica ligera.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados