A lógica condicional no WordPress não é um conjunto de if espalhados pelo functions.php. É um contrato com a query principal: o núcleo decide o contexto do pedido HTTP, as conditional tags leem esse contexto, e o seu código reage. Em 2026, com temas de blocos e Full Site Editing (FSE), o PHP continua a governar assets, capabilities, cache e qualquer ramo que o editor de sítio não consegue expressar.
Este guia trata a lógica condicional como engenharia: quando is_singular() mente, quando in_the_loop() falha fora do Loop, porque current_user_can() e page cache não se misturam, e como isolar helpers testáveis. A taxonomia (in_category vs has_term) entra no fim como caso concreto, não como único tema.
De onde as conditional tags leem o estado
As tags condicionais não inspeccionam o URL à mão. Depois de parse_request e WP_Query, o objeto global $wp_query guarda flags (is_single, is_page, is_front_page, is_category e outras). Funções como is_singular() e is_front_page() são wrappers sobre essas flags.
Consequência prática: se chamar uma conditional tag demasiado cedo - antes de wp / template_redirect - o resultado é indefinido ou falso. O manual diz isto sem rodeios: uma conditional tag colocada diretamente no functions.php não funciona, tem de ficar dentro de uma função ligada a uma ação ou filtro posterior. Em plugins, o sítio seguro para ramos de template e enqueue é wp_enqueue_scripts, template_redirect ou hooks posteriores documentados em Hooks.
Outra consequência: uma WP_Query secundária (widget, bloco de posts recentes, shortcode) não altera as flags da query principal. is_singular() continua a descrever a página pedida, não o post que o widget está a iterar. Para dados de um post específico, passe o ID ou o objeto WP_Post às APIs de post/termo.
Erros comuns com is_singular e is_front_page
is_singular() devolve verdadeiro para um post, página ou CPT singular. Não distingue tipo. Em lojas WooCommerce, is_singular() é verdadeiro em product, o que leva muita gente a carregar CSS de blog no PDP. Prefira is_singular( 'post' ), is_page(), ou is_singular( array( 'post', 'page' ) ) com a lista explícita.
is_front_page() e is_home() continuam a ser a confusão clássica:
- Com «As suas últimas publicações» em Definições > Leitura, a homepage é
is_front_page()eis_home(). - Com uma página estática como página inicial e outra como «página de publicações», a página inicial é só
is_front_page(); o feed de posts éis_home()e nãois_front_page().
Código que faz if ( is_home() ) para «mostrar o hero da homepage» parte quando o cliente muda a leitura para página estática. O padrão estável para a homepage pública é is_front_page(). Para o arquivo do blog, use is_home() && ! is_front_page().
Caso de produção visto em Gdynia: um tema carregava o slider só com is_home(). Depois de migrar a homepage para uma página com blocos, o slider desapareceu e o arquivo /blog/ herdou o JavaScript do carrossel. Uma linha - is_front_page() - teria bastado.
is_singular() também falha em endpoints que parecem «uma página» ao utilizador mas não são singular no query: pesquisa (is_search()), 404 (is_404()), e pedidos REST, que não passam pela query principal do tema. Antes de assumir «há um post atual», confirme get_queried_object() e o tipo.
Como usar in_the_loop e has_term fora do Loop
Funções como the_title(), the_content() e várias condicionais de post (in_category() sem ID) dependem do post global definido pelo Loop. in_the_loop() diz se o código corre entre while ( have_posts() ) : the_post(); e o fim desse Loop na query em causa.
Fora do Loop - num header.php, num filtro body_class, num wp_footer - in_category( 'noticias' ) sem segundo argumento lê o post global, que pode ser o último da query anterior, um post de um widget, ou null. Sintoma típico: a classe CSS «categoria notícias» aparece no arquivo, ou o aviso legal só no último artigo da listagem.
Padrão 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 );
}Use get_queried_object_id() para o documento do pedido; use o ID do post dentro de um Loop secundário quando estiver a iterar resultados. Não misture os dois.
in_the_loop() ajuda a decidir se pode confiar no post global. Se estiver fora do Loop e precisar de dados do documento atual, não «entre» no Loop artificialmente só para ler meta: leia pelo ID da query principal.
Para código que corre sem contexto de pedido, como uma tarefa em segundo plano ou um endpoint REST, is_object_in_term( $object_id, $taxonomy, $terms ) recebe o ID do objeto como primeiro argumento e não precisa de post global.
current_user_can e ramos de autenticação
current_user_can( 'edit_posts' ) e afins leem a sessão e as capabilities do utilizador atual. São a API correta para UI administrativa, atalhos de edição e conteúdo só para editores.
Três regras que evitam incidentes:
- Nunca misture HTML diferente para anónimos e autenticados no mesmo documento se esse documento for servido por page cache partilhado. O edge ou o plugin de cache guarda a variante do primeiro visitante. Um editor vê o botão «Editar»; o próximo anónimo recebe o mesmo HTML.
- Para ramos autenticados, use cookies de bypass de cache (padrão da maioria dos plugins de page cache), ESI, ou sirva o ramo só via JavaScript/REST autenticado depois do HTML estático.
- Capabilities não são roles. Teste
current_user_can( 'manage_options' ), nãocurrent_user_can( 'administrator' )- o segundo não é o contrato estável da API.
Em temas clássicos, um padrão frequente é esconder a admin bar para subscritores. Faça-o com um filtro em show_admin_bar, não com CSS condicional no HTML em cache:
add_filter(
'show_admin_bar',
static function ( $show ) {
if ( is_user_logged_in() && ! current_user_can( 'edit_posts' ) ) {
return false;
}
return $show;
}
);O filtro corre no pedido PHP; o page cache anónimo nunca o vê porque o utilizador autenticado já está fora desse cache.
Quando usar a hierarquia de templates e quando condicionais PHP
A hierarquia de templates resolve ficheiros (single-product.php, category-noticias.php, front-page.php). As conditional tags resolvem ramos dentro de um ficheiro partilhado (header.php, functions.php, um partial).
Prefira a hierarquia quando o markup diverge de forma estrutural: um CPT com layout próprio merece single-{post_type}.php (ou o equivalente em block theme: template HTML dedicado). Prefira condicionais PHP quando a diferença é pequena: um aviso, um enqueue, uma classe no body.
Anti-padrão: um único page.php genérico com vinte if ( is_page( 'contacto' ) ) encadeados. Isso duplica a hierarquia em PHP, dificulta code review e impede que o FSE (ou um child theme) substitua só um template. Extraia páginas com markup distinto para templates próprios; deixe condicionais para variação fina.
Regra de ouro: se o ramo cabe num nome de ficheiro da hierarquia, use o ficheiro. Se o ramo depende de capability, cookie, A/B ou feature flag, use PHP (ou um bloco com lógica no servidor via plugin).
Quais são os limites do FSE em temas de blocos?
O FSE move templates e partes para HTML no editor de sítio. Não elimina conditional tags.
O editor de sítio cobre variação visual por tipo de template (singular, arquivo, 404). Não cobre, de forma fiável:
- Enqueue condicional de scripts pesados (mapas, calculadoras, players).
- Ramos por capability (
current_user_can). - Lógica de taxonomia recursiva (pai inclui filhos).
- Integrações ERP/WooCommerce que dependem de meta do produto na query atual.
- Qualquer coisa que precise de
WP_Querysecundária com argumentos dinâmicos.
Block themes ainda carregam functions.php. É aí que vivem wp_enqueue_scripts, filtros de body_class, e helpers de termo. Os blocos com «variantes» no editor não substituem has_term() quando a regra de negócio é «se o produto está em qualquer descendente de roupa».
Limite operacional: markup só no editor, decisões de runtime no PHP. Misturar as duas camadas sem contrato claro (por exemplo, um padrão de bloco que assume is_front_page() e um plugin que altera a query principal) produz regressões difíceis de reproduzir em staging.
Taxonomias: in_category versus has_term
in_category() é um atalho histórico para a taxonomia category. Não cobre product_cat, post_tag nem taxonomias registadas com register_taxonomy(). A sua página de referência lista dois parâmetros: a categoria (ID, nome, slug ou array destes) e um post opcional.
has_term( $term, $taxonomy, $post = null ) é a API geral. Para código novo, prefira has_term() mesmo em categorias nativas: a assinatura é explícita e o mesmo helper serve WooCommerce e CPTs.
Verificação nativa é correspondência exata. Um post só em maca não satisfaz in_category( 'fruta' ) só porque maca é filha de fruta. O core não oferece «está neste ramo da árvore» numa só chamada. Use get_term_children() (e object cache) num helper:
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 );
}Armadilha WooCommerce: is_product_category() descreve o arquivo de categoria de produto, não o produto no Loop. O WooCommerce tem as suas próprias condicionais de página (is_product(), is_shop(), is_product_category()), e todas descrevem o pedido. Dentro de um loop de produtos use has_term( $slug, 'product_cat', $post_id ).
Page cache com utilizadores autenticados no WordPress
Page cache (nginx fastcgi_cache, Cloudflare, plugins de full-page cache) serve HTML pronto a anónimos. Qualquer if ( is_user_logged_in() ) ou current_user_can() embutido nesse HTML é uma vulnerabilidade de vazamento de UI ou, no sentido inverso, de conteúdo em falta.
Padrões que funcionam:
- Separar: HTML público idêntico para todos os anónimos; UI autenticada via endpoint autenticado ou fragmento sem cache.
- Bypass: cookies de sessão fazem o proxy saltar o cache para utilizadores autenticados (custo: menos hit ratio).
- Não condicionar assets críticos da homepage com
is_user_logged_in()no PHP do documento em cache.
Condicionais «seguras» sob page cache anónimo: is_front_page(), is_singular( 'post' ), has_term( $slug, 'category', $queried_id ) - tudo o que depende só da URL/query pública. Condicionais «perigosas» no mesmo documento: capabilities, preferências de utilizador, geolocalização por IP sem vary, A/B sem cookie de vary.
Em projetos com edge cache na UE, documente a matriz vary (cookie, header) ao lado do helper. Sem isso, o próximo plugin de «barra de promo para clientes» volta a partilhar HTML.
Como testar conditional tags com PHPUnit
Condicionais inline em templates são difíceis de testar. Extraia predicados puros (ou quase puros) que recebem estado explícito:
function wppoland_should_load_map_assets( bool $is_contact_page, bool $user_can_edit ): bool {
return $is_contact_page || $user_can_edit;
}No 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 );
}
);Em PHPUnit (com WordPress test suite ou doubles), testa wppoland_should_load_map_assets( true, false ) sem bootstrap de $wp_query. O mesmo padrão aplica-se a ramos de taxonomia: passe $post_id e slugs, não leia globais dentro do predicado.
Convenções úteis:
- Prefixo de projeto no nome (
wppoland_) para evitar colisões. - Retorno
boolexplícito; sem eco de HTML dentro do helper. - Um helper = uma pergunta de negócio («é singular de notícias?»), não um switch com side effects.
Desempenho de has_term e get_term_children
has_term() e conditional tags sobre a query principal são baratas quando o object cache está quente. get_term_children() em taxonomias profundas pode custar; memorize o resultado por request (static local ou wp_cache_get / wp_cache_set com grupo próprio).
Evite chamar o mesmo predicado dezenas de vezes num Loop: calcule uma vez antes do Loop ou cache por $post_id. Em arquivos com 30 produtos e verificação recursiva de product_cat, a diferença nota-se em TTFB em hosting partilhado.
Não use SQL cru para «atalhos» de categoria se has_term() chega: perde object cache e preparações do $wpdb que o core já faz.
is_main_query e pre_get_posts: alterar só a query principal
Para mudar o que um arquivo mostra (número de posts, exclusão de uma categoria, ordenação), o hook certo é pre_get_posts, não uma WP_Query nova dentro do template. O hook recebe o objeto WP_Query por referência e corre para todas as queries: a principal, as secundárias de widgets e blocos, e as do painel de administração.
Duas regras da referência de pre_get_posts evitam a maioria dos erros:
- Use os métodos do objeto recebido (
$query->is_main_query(),$query->is_category()), não as funções globais. A documentação explica queis_main_query()testa a instância global$wp_query, não a que o hook passou. A página de is_main_query() chama a esse uso «circular». - Proteja o painel com
is_admin(). Sem essa guarda, a mesma regra altera também a lista de posts em 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 );
}
}
);A mesma referência avisa que, dentro de pre_get_posts, algumas conditional tags ainda não funcionam: is_front_page() falha, is_home() funciona. Se a regra é «só na página inicial estática», decida com $query->get( 'page_id' ) comparado com get_option( 'page_on_front' ), ou mova a lógica para template_redirect, onde a query já está resolvida.
Que páginas cobre is_archive e quando usar is_tax
is_archive() é um agregado. No código do núcleo (class-wp-query.php), a flag is_archive fica verdadeira quando a query é arquivo de tipo de post, de data, de autor, de categoria, de etiqueta ou de taxonomia personalizada. O manual de conditional tags resume: páginas de categoria, etiqueta, autor e data são todas arquivos.
Isto tem consequências diretas:
is_archive()sozinho carrega o mesmo CSS no arquivo de autor e no arquivo deproduct_cat. Se só quer arquivos de categoria do blog, useis_category().is_tax()devolve falso em arquivos de categoria e de etiqueta. A referência de is_tax() diz que é verdadeiro para arquivos de taxonomias personalizadas e falso para as taxonomias embutidas. Para cobrir as três, escrevais_category() || is_tag() || is_tax().is_post_type_archive( 'produto' )só é verdadeiro se o tipo de post foi registado comhas_archive => true, como lembra o mesmo manual.
Nenhuma destas funções diz algo sobre um post individual. Num single, is_tax() é falso mesmo que o post tenha o termo; aí a pergunta certa é has_term().
is_page_template em temas de blocos
is_page_template() também mudou de significado com os temas de blocos, sem mudar de assinatura. No núcleo (post-template.php), a função devolve falso fora de is_singular() e depois compara o argumento com get_page_template_slug() do objeto pedido, ou seja, com o valor guardado no post.
Num tema clássico esse valor é o nome do ficheiro PHP, por isso is_page_template( 'landing.php' ) funciona. Num tema de blocos, os modelos personalizados registados em customTemplates no theme.json usam como name o nome do ficheiro HTML sem extensão, segundo o manual de modelos personalizados. Um modelo /templates/landing.html é verificado com is_page_template( 'landing' ). Copiar condições de um tema clássico para um tema de blocos sem ajustar este argumento produz um if que nunca é verdadeiro, sem aviso nem erro.
Lista de verificação para lógica condicional no WordPress
- A conditional tag corre depois de a query principal estar pronta?
is_front_page()vsis_home()está alinhado com Definições > Leitura?- Termos e categorias usam ID de post explícito fora do Loop?
- Algum ramo depende de
current_user_can()no HTML em page cache? - A divergência de markup cabe na hierarquia de templates em vez de um
ifgigante? - O helper tem testes ou, no mínimo, casos manuais documentados (anónimo, editor, arquivo, singular)?
- Os callbacks de
pre_get_postsusam$query->is_main_query()e excluemis_admin()?
Resumo
Conditional tags leem a query principal; trate-as como API de contexto, não como parsing de URL. Separe hierarquia de templates (estrutura) de PHP condicional (variação fina e runtime). No FSE, o editor cobre templates; o PHP continua a cobrir assets, capabilities e regras de taxonomia. has_term() substitui in_category() em código novo; ramos autenticados ficam fora do HTML anónimo em cache. Helpers com argumentos explícitos são a forma de manter a lógica revisável e testável.
Lógica condicional mal colocada num template é o género de erro que só aparece no post que ninguém testou. Se quiser esses templates revistos por quem já apanhou o erro antes, veja a nossa prática de desenvolvimento WordPress.







