Construir secções de notícias relacionadas, carrosséis de artigos temáticos, páginas de destino dedicadas ou blocos personalizados no editor Gutenberg exige, na maioria dos projetos, a extração de conteúdos agrupados por categorias. Embora o ecossistema do WordPress forneça interfaces simples para obter esses dados, uma implementação descuidada em temas ou extensões pode degradar severamente o tempo de resposta do servidor da base de dados MySQL.
Muitos programadores copiam pequenos fragmentos de código sem compreenderem o impacto de consultas que calculam linhas desnecessárias ou disparam dezenas de leituras em tabelas de metadados. Este guia técnico disseca os mecanismos de consulta do WordPress (get_posts, WP_Query e REST API), detalha parâmetros de filtragem taxonómica e apresenta técnicas de otimização orientadas a ambientes de elevado tráfego.
Como o WordPress guarda as categorias
Antes de escolher o argumento de consulta, convém saber onde os dados vivem:
- As categorias são uma taxonomia nativa do WordPress, registada como
category. - Cada artigo pode pertencer a várias categorias.
- As categorias são hierárquicas, com relações pai e filho.
- Os dados das categorias ficam nas tabelas
wp_termsewp_term_taxonomy. - As ligações entre artigos e categorias ficam em
wp_term_relationships.
Encontrar o ID de uma categoria a partir do slug ou do nome
Quando só conhece o slug, resolva-o uma vez e reutilize o ID:
// A partir do slug
$termo = get_category_by_slug( 'noticias' );
$id_noticias = $termo ? $termo->term_id : 0;
// A partir do nome apresentado
$id_noticias = get_cat_ID( 'Notícias' ); // devolve 0 se não existir
// A partir de qualquer taxonomia (categorias incluídas)
$termo = get_term_by( 'slug', 'noticias', 'category' );Se fixar o ID num modelo, guarde-o numa constante ou numa opção em vez de chamar get_cat_ID() em cada pedido.
get_posts ou WP_Query para listar posts por categoria
No desenvolvimento para o ecossistema WordPress, a obtenção de publicações no servidor divide-se primariamente entre a função auxiliar get_posts() e a instanciação direta da classe WP_Query.
get_posts()
├── Adequado para: listagens estáticas, menus laterais, listas simples de 3 a 5 artigos
├── Comportamento padrão: desativa paginação, suprime filtros de terceiros (suppress_filters = true)
└── Retorno: array de objetos WP_Post prontos a iterar com foreach
new WP_Query()
├── Adequado para: listagens paginadas, grelhas filtráveis dinâmicas, modelos complexos
├── Comportamento padrão: executa o ciclo completo de consulta, respeita filtros globais
└── Retorno: objeto de consulta com métodos de controlo de estado (have_posts, the_post)get_posts para listagens simples sem paginação
A função get_posts() é a opção mais limpa quando pretende apenas obter um lote definido de registos sem interferir com os dados de navegação do ecrã:
/**
* Obtém os cinco artigos mais recentes de uma categoria pelo respetivo slug.
*/
function wppoland_obter_artigos_categoria( string $categoria_slug ): array {
$argumentos = array(
'posts_per_page' => 5,
'category_name' => sanitize_text_field( $categoria_slug ),
'post_status' => 'publish',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
);
$artigos = get_posts( $argumentos );
if ( empty( $artigos ) ) {
return array();
}
$saida = array();
foreach ( $artigos as $artigo ) {
// Prepara as variáveis globais das tags de modelo (o $post global não muda)
setup_postdata( $artigo );
$saida[] = sprintf(
'<li><a href="%s">%s</a> <time datetime="%s">%s</time></li>',
esc_url( get_permalink( $artigo->ID ) ),
esc_html( get_the_title( $artigo->ID ) ),
esc_attr( get_the_date( 'c', $artigo->ID ) ),
esc_html( get_the_date( '', $artigo->ID ) )
);
}
// Restabelece a variável global $post da consulta principal
wp_reset_postdata();
return $saida;
}O que get_posts muda em relação à WP_Query
get_posts() é um invólucro fino que constrói uma WP_Query com valores por defeito próprios. O código da função mostra quatro diferenças relevantes para listas de categoria:
numberpostsvale 5 por defeito, por isso uma chamada sem limite devolve cinco artigos sem aviso.post_statusvalepublishesuppress_filtersvaletrue, pelo queposts_wheree outros filtros SQL de plugins não correm. A açãopre_get_postscontinua a disparar.ignore_sticky_postseno_found_rowssão sempre forçados atrue. Não é possível construir paginação numerada sobreget_posts(), porque o total nunca é calculado. Por isso, no exemplo acima, essas duas linhas são redundantes.includedefineposts_per_pagecomo o número de IDs passados e anula o seu próprio limite.
Há mais uma armadilha quando chama get_posts() dentro de uma função ou do callback de um shortcode e usa the_title() ou the_permalink() sem argumentos. setup_postdata() prepara as tags de modelo, mas essas funções continuam a ler o $post global. Declare global $post; antes do ciclo foreach ( $artigos as $post ), como fazem os exemplos da referência, e termine com wp_reset_postdata(). O exemplo acima evita o problema porque passa o ID a cada função.
WP_Query para listagens com paginação
Quando necessita de paginação numérica ou de combinar múltiplos critérios de ordenação, a instanciação direta de WP_Query é indispensável:
$pagina_atual = max( 1, get_query_var( 'paged', 1 ) );
$consulta_categoria = new \WP_Query( array(
'cat' => 18, // Preferência pelo ID da categoria para acelerar a pesquisa
'posts_per_page' => 10,
'paged' => $pagina_atual,
'post_status' => 'publish',
'ignore_sticky_posts' => true,
) );
if ( $consulta_categoria->have_posts() ) : ?>
<div class="bloco-artigos-categoria">
<?php while ( $consulta_categoria->have_posts() ) : $consulta_categoria->the_post(); ?>
<article id="post-<?php the_ID(); ?>" class="cartao-resumo">
<h3><a href="<?php echo esc_url( get_permalink() ); ?>"><?php echo esc_html( get_the_title() ); ?></a></h3>
<div class="resumo-texto">
<?php echo wp_kses_post( get_the_excerpt() ); ?>
</div>
</article>
<?php endwhile; ?>
</div>
<?php
// Geração de paginação numérica acessível
echo paginate_links( array(
'total' => $consulta_categoria->max_num_pages,
'current' => $pagina_atual,
) );
wp_reset_postdata();
else : ?>
<p><?php esc_html_e( 'Nenhum artigo encontrado nesta categoria.', 'wppoland' ); ?></p>
<?php endif; ?>Consultar artigos de várias categorias
// Artigos em QUALQUER uma destas categorias (relação OU)
$argumentos = array(
'category__in' => array( 5, 10, 15 ),
'posts_per_page' => 10,
);
// Artigos em TODAS estas categorias (relação E)
$argumentos = array(
'category__and' => array( 5, 10 ),
'posts_per_page' => 10,
);O argumento cat também aceita uma lista separada por vírgulas ('cat' => '2,6,17'), e category_name aceita 'noticias,eventos' para OU e 'noticias+eventos' para E, como mostram os exemplos da referência da WP_Query.
Paginar uma consulta de categoria numa página inicial estática
Um ciclo personalizado pagina com paged, que se lê com get_query_var( 'paged' ). A exceção é um modelo de página definido como página inicial estática: aí o WordPress guarda o número da página na variável page, e por isso a referência da WP_Query manda ler get_query_var( 'page' ). Com a variável errada, cada clique em “seguinte” volta a mostrar a página um.
$paged = ( is_front_page() && is_page() ) ? get_query_var( 'page' ) : get_query_var( 'paged' );
$paged = max( 1, (int) $paged );
$consulta = new WP_Query( array(
'category_name' => 'noticias',
'posts_per_page' => 10,
'paged' => $paged,
) );Filtrar posts por categoria com cat ou tax_query
No WordPress, as categorias padrão pertencem à taxonomia registada internamente como category. Existem várias formas de declarar a restrição nos argumentos da consulta:
cat(Inteiro): O identificador numérico da categoria ('cat' => 12). Suporta valores negativos para exclusão ('cat' => -12).category_name(Texto): O slug textual da categoria ('category_name' => 'tecnologia').category__in(Array de inteiros): Permite listar artigos que pertençam a pelo menos uma de várias categorias especificadas, sem incluir subcategorias automaticamente.tax_query(Array relacional estruturado): O formato moderno e universal para qualquer taxonomia.
Quando usar tax_query em vez de cat
Ao trabalhar com hierarquias complexas ou arquiteturas que combinam categorias normais com taxonomias personalizadas (por exemplo, setor ou tipo_projeto), a declaração explícita via tax_query oferece maior rigor semântico:
$argumentos = array(
'post_type' => 'post',
'posts_per_page' => 6,
'tax_query' => array(
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 14, 28 ),
'operator' => 'IN',
'include_children' => false, // Evita a inclusão automática de categorias descendentes
),
),
);Nota de desempenho na base de dados: Nos dois casos, a classe WP_Tax_Query converte os termos em term_taxonomy_id com uma consulta de termos antes de montar a junção (JOIN) com wp_term_relationships, por isso o SQL final compara sempre inteiros. Com include_children ativo, um slug obriga a uma conversão extra para term_id antes de recolher as subcategorias; com IDs esse passo não existe. A diferença é, no máximo, uma consulta de termos a menos, não uma junção mais lenta (código da classe).
Combinar categorias com campos personalizados
Os filtros de categoria também se combinam com campos personalizados. Uma lista de eventos que mostra só as próximas datas de duas categorias junta category__in com uma meta_query e ordena pelo campo de data:
$argumentos = array(
'posts_per_page' => 10,
'category__in' => array( get_cat_ID( 'Conferências' ), get_cat_ID( 'Workshops' ) ),
'meta_key' => 'data_evento',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => 'data_evento',
'value' => current_time( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
),
);get_cat_ID() recebe o nome da categoria, não o slug, e devolve 0 quando nada corresponde. Um 0 dentro de category__in não corresponde a nenhum artigo, por isso confirme os IDs antes de correr a consulta numa página de produção.
Que parâmetros de categoria incluem as subcategorias
O mesmo ID de categoria devolve listas diferentes consoante a interface usada para o consultar. Este pormenor está por trás da maioria dos relatos de “faltam artigos na lista”, e as regras estão fixadas no código do núcleo, não em definições:
| Interface | Argumento | Artigos das subcategorias |
|---|---|---|
| WP_Query | cat | incluídos |
| WP_Query | category_name | incluídos |
| WP_Query | category__in, category__and, category__not_in | não incluídos |
| WP_Query | tax_query | incluídos, exceto se include_children for false |
| get_posts() | category | incluídos (é um alias de cat) |
| REST API | categories=5 | não incluídos |
| REST API | categories[terms]=5&categories[include_children]=true | incluídos |
| Bloco Query Loop | filtro de categoria | não incluídos |
| WP-CLI | --cat, --category_name | incluídos (passam diretamente para a WP_Query) |
As linhas da WP_Query vêm de WP_Query::parse_tax_query(): cat passa a uma tax query com include_children a true, enquanto category__in, category__and e category__not_in passam a tax queries com include_children a false. A referência da WP_Query diz o mesmo nos exemplos: category__in não mostra artigos das subcategorias. A linha de get_posts() decorre da documentação da função, que descreve category como alias de cat. As linhas da REST API vêm do controlador de artigos, onde include_children é false por defeito e só existe para taxonomias hierárquicas. A linha do Query Loop vem de build_query_vars_from_query_block() no blocks.php do núcleo, que fixa include_children => false para cada filtro de taxonomia.
A consequência prática: um modelo PHP com cat => 5 e um front end headless que chama /wp/v2/posts?categories=5 podem mostrar artigos diferentes para a mesma categoria-mãe. Decida qual o comportamento de que precisa e escolha o argumento que o produz, em vez de presumir que os dois coincidem.
Excluir uma categoria de uma consulta WordPress
A exclusão funciona de duas formas, e cada uma trata as subcategorias de maneira diferente. Um ID negativo em cat exclui a categoria e as suas filhas, porque o núcleo constrói uma tax query NOT IN com include_children a true. category__not_in exclui apenas os termos indicados, por isso os artigos arquivados numa subcategoria continuam a aparecer.
// Exclui a categoria 12 e todas as filhas
$argumentos = array( 'cat' => '-12' );
// Exclui apenas artigos arquivados diretamente em 12 ou 34
$argumentos = array( 'category__not_in' => array( 12, 34 ) );Alterar o arquivo de categoria com pre_get_posts
Para mudar os artigos que aparecem nos arquivos de categoria, ou para retirar uma categoria da página inicial do blogue, altere a consulta principal em vez de correr um segundo ciclo. A referência de pre_get_posts recomenda exatamente estas condições:
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_home() ) {
$query->set( 'cat', '-12' ); // sem a categoria 12 na página do blogue
}
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 20 );
}
} );Use as versões de método das tags condicionais ($query->is_category(), $query->is_main_query()). As funções soltas testam o $wp_query global e não o objeto que o gancho recebe, algo que a referência do gancho assinala de forma explícita.
Porque é que query_posts não serve para filtrar por categoria
Tutoriais antigos colocam query_posts( 'cat=5' ) no topo de um modelo. O docblock de query_posts() no núcleo diz que a função substitui por completo a consulta principal, que não se destina a plugins nem a temas e que, na maioria dos casos, deve dar lugar à ação pre_get_posts. O corpo da função é uma única linha que atribui uma nova WP_Query ao $wp_query global. Um modelo corre depois da consulta principal, por isso a chamada repete o trabalho na base de dados e substitui o objeto que a paginação e as tags condicionais leem. Cada tarefa de categoria que usava query_posts() resolve-se com pre_get_posts (alterar o arquivo) ou com uma WP_Query separada (acrescentar uma segunda lista).
Acelerar a WP_Query com no_found_rows e fields
A maioria dos problemas de lentidão provocados por consultas secundárias decorre de operações secundárias executadas silenciosamente pelo motor do WordPress. Se o seu objetivo for apenas desenhar uma lista de títulos e hiperligações, pode desligar grande parte dessa sobrecarga:
1. Desativar a contagem total com no_found_rows
Sempre que a WP_Query é executada sem a diretiva no_found_rows => true, o WordPress adiciona a instrução SQL SQL_CALC_FOUND_ROWS à consulta principal e dispara uma segunda consulta imediata com SELECT FOUND_ROWS() para determinar o número global de páginas para paginação.
Esta contagem obriga o MySQL a contar todos os registos correspondentes, mesmo quando a lista mostra apenas alguns. Se a sua listagem apresenta apenas um limite fixo (por exemplo, os 5 artigos mais recentes na barra lateral), declare sempre:
'no_found_rows' => true,2. Desligar o pré-carregamento de termos e metadados
Por norma, o WordPress tenta ser eficiente pré-carregando em memória todos os metadados (wp_postmeta) e termos (wp_terms) associados aos artigos devolvidos na consulta.
Os dois indicadores valem true por defeito, e a referência da WP_Query diz que normalmente não é preciso mexer-lhes. Contudo, se a sua listagem imprime unicamente o título, a data e a hiperligação, esse pré-carregamento representa trabalho inútil:
'update_post_meta_cache' => false, // Não carrega a tabela wp_postmeta para estes artigos
'update_post_term_cache' => false, // Não carrega categorias e etiquetas adicionais3. Limitar a extração a identificadores (fields => ids)
Quando utiliza um sistema de cache de objetos em memória (como Redis ou Memcached), uma estratégia possível consiste em extrair unicamente os identificadores numéricos e deixar que as funções de modelo resolvam os objetos a partir da cache:
$ids_artigos = get_posts( array(
'cat' => 12,
'posts_per_page' => 10,
'fields' => 'ids',
'no_found_rows' => true,
) );O argumento fields aceita exatamente três valores, segundo a referência da WP_Query: all (por defeito), ids e id=>parent. Qualquer outro valor, incluindo um array de nomes de colunas, devolve sem aviso objetos de artigo completos.
Guardar listas de categoria em cache com transients
Em portais de elevado tráfego, executar consultas dinâmicas de categoria a cada carregamento de página gera concorrência desnecessária no pool de ligações do MySQL. A resposta correta passa pelo armazenamento temporário dos resultados HTML ou da lista de objetos através da API de Transientes do WordPress.
/**
* Apresenta a listagem de artigos em cache e renova-a periodicamente.
*/
function wppoland_renderizar_artigos_categoria_com_cache( int $categoria_id ): void {
$chave_cache = 'wppoland_cat_' . $categoria_id . '_html';
$conteudo_em_cache = get_transient( $chave_cache );
if ( false !== $conteudo_em_cache ) {
echo $conteudo_em_cache;
return;
}
$consulta = new \WP_Query( array(
'cat' => $categoria_id,
'posts_per_page' => 4,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
) );
if ( ! $consulta->have_posts() ) {
return;
}
ob_start();
echo '<ul class="lista-categoria-destaque">';
while ( $consulta->have_posts() ) {
$consulta->the_post();
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink() ),
esc_html( get_the_title() )
);
}
echo '</ul>';
wp_reset_postdata();
$html_final = ob_get_clean();
// Grava o resultado em memória durante 12 horas
set_transient( $chave_cache, $html_final, 12 * HOUR_IN_SECONDS );
echo $html_final;
}Limpar a cache ao publicar um artigo
Para assegurar que novos artigos surgem imediatamente na listagem sem esperar pela expiração natural do transiente, adicione um gancho no evento de gravação:
/**
* Limpa a cache da categoria quando um artigo associado for atualizado ou publicado.
*/
function wppoland_invalidar_cache_categoria( int $post_id ): void {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
$categorias = wp_get_post_categories( $post_id );
foreach ( $categorias as $cat_id ) {
delete_transient( 'wppoland_cat_' . $cat_id . '_html' );
}
}
add_action( 'save_post', 'wppoland_invalidar_cache_categoria' );Mostrar artigos de uma categoria num tema de blocos
Nos temas de blocos, o tipo de tema por defeito desde que o Twenty Twenty-Two chegou com o WordPress 5.9, muitas vezes não precisa de PHP. O bloco Query Loop filtrado por categoria apresenta a mesma lista a partir do editor: insira um Query Loop, abra as definições do bloco e, em Filtros, acrescente a categoria pretendida.
O filtro de categoria do bloco só corresponde aos termos que seleciona. O núcleo converte-o numa tax query com include_children a false, por isso escolher uma categoria-mãe não traz os artigos arquivados apenas nas filhas; acrescente também as subcategorias ao filtro. Recorra às abordagens com código quando precisar de lógica que o bloco não exprime: cache com transientes, exclusões condicionais por arquivo ou dados consumidos fora do tema.
Obter posts de uma categoria pela REST API
Com a transição para arquitecturas modulares e blocos dinâmicos em React, a extração de dados pode ocorrer no cliente ou durante a compilação estática através da API REST nativa do WordPress:
# Obter os 5 artigos mais recentes da categoria com identificador 15
GET /wp-json/wp/v2/posts?categories=15&per_page=5&_fields=id,title,link,dateAo especificar o parâmetro _fields, o WordPress filtra o documento JSON resultante na origem, poupando largura de banda e reduzindo a utilização de memória no servidor web.
Para converter um slug no ID que o endpoint espera, consulte primeiro /wp-json/wp/v2/categories?slug=noticias. Use categories_exclude para retirar um termo e _embed para trazer imagens de destaque e nomes de termos num só pedido. Três pormenores do manual da REST API decidem se uma lista headless fica completa:
- As subcategorias exigem a forma de objeto.
categories=5corresponde apenas ao termo 5. Para incluir as filhas, enviecategories[terms]=5&categories[include_children]=true. O mesmo objeto aceitacategories[operator]=ANDquando um artigo tem de ter todos os termos indicados. - As páginas têm um teto de 100 elementos. O guia de paginação limita
per_pagea um valor entre 1 e 100. Leia os cabeçalhos de respostaX-WP-TotaleX-WP-TotalPagese peçapage=2,page=3até ter tudo. _fieldsaceita chaves aninhadas. Desde o WordPress 5.3, o parâmetro_fieldsaceita caminhos comometa.data_evento, pelo que uma lista pode pedir uma única chave meta registada sem o objeto meta inteiro.
Listar artigos de uma categoria com WP-CLI
Para auditorias, migrações e exportações pontuais, wp post list corre a mesma WP_Query a partir da linha de comandos. Cada opção --<campo>=<valor> passa para a WP_Query como argumento, por isso as regras de categoria da tabela acima aplicam-se sem alterações:
# ID do termo e número de artigos de cada categoria
wp term list category --fields=term_id,name,slug,count
# Artigos publicados em "noticias" e nas subcategorias, em CSV
wp post list --category_name=noticias --post_status=publish --fields=ID,post_title,post_date --format=csv
# Apenas IDs, artigos arquivados diretamente na categoria 12 (sem filhas)
wp post list --category__in=12 --post_status=publish --format=ids
# Quantos artigos um modelo mostraria para a categoria 12 e as filhas
wp post list --cat=12 --post_status=publish --format=countDois valores por defeito diferem de um ciclo de modelo. O código do comando define posts_per_page como -1 e post_status como any, por isso sem --post_status=publish o resultado inclui rascunhos, artigos pendentes e privados. As colunas por defeito são ID, post_title, post_name, post_date e post_status, e --format aceita table, csv, ids, json, count e yaml. Comparar --format=count para --cat e para --category__in com o mesmo ID mostra depressa quantos artigos estão só em subcategorias.
Depurar uma consulta de categoria que devolve os artigos errados
Quando uma consulta devolve artigos a mais, a menos ou inesperados, inspecione o SQL que realmente correu em vez de adivinhar pelos argumentos. O plugin Query Monitor mostra todas as consultas da página, incluindo a gerada pelo seu ciclo. Para uma verificação rápida sem plugin, registe a consulta já interpretada:
$consulta = new WP_Query( $argumentos );
// O SQL exato que o WordPress construiu a partir de $argumentos
error_log( $consulta->request );Os dois culpados por trás da maioria das surpresas são a inclusão de subcategorias (ver a tabela acima) e um filtro pre_get_posts de um plugin ou do tema que altera a consulta principal. Se o SQL parecer correto mas o resultado vier vazio, confirme que o termo tem mesmo artigos publicados atribuídos no idioma ou contexto atual; em sites multilingues, os artigos podem estar associados à versão da categoria noutro idioma.
Conclusão e boas práticas
A manipulação de listagens por categoria é uma rotina básica que exige atenção à eficiência para não comprometer a escalabilidade do projeto:
- Use
get_posts()para blocos auxiliares simples e reserveWP_Querypara navegação estruturada. - Ative sempre
no_found_rows => truecaso não exista paginação numérica associada. - Filtre prioritariamente por IDs inteiros (
catouterm_id), reduzindo a sobrecarga de junções textuais no MySQL. - Finalize qualquer ciclo personalizado com a invocação estrita de
wp_reset_postdata(). - Proteja consultas recorrentes com a Transients API e invalidação acionada por eventos de publicação.
Para projetos empresariais com arquiteturas complexas, milhares de termos taxonómicos ou lojas com catálogo extenso, consulte os nossos serviços de engenharia e manutenção técnica de sistemas WordPress.
Se o tempo de carregamento continuar alto depois destas otimizações, uma auditoria Core Web Vitals mede o impacto na experiência real dos visitantes.







