Cómo obtener posts de una categoría en WordPress
Conoce más sobre el desarrollo WordPress profesional en WPPoland.
Una de las tareas más comunes en el desarrollo de WordPress es recuperar posts de categorías específicas. Ya sea que estes construyendo un diseño de homepage personalizado, creando una plantilla de archivo de categoría o mostrando contenido relacionado, entender como consultar posts por categoría de forma eficiente es esencial para cualquier desarrollador WordPress.
Esta guía cubre enfoques prácticos para extraer listas de posts de categorías, desde implementaciones estándar hasta técnicas avanzadas de optimización. Al final, tendrás un kit de herramientas completo para manejar consultas basadas en categorías en cualquier proyecto WordPress.
Cómo guarda WordPress las categorías
Antes de profundizar en el código, es importante entender como WordPress maneja las categorías:
- Las categorías son una taxonomía incorporada en WordPress
- Cada post puede pertenecer a múltiples categorías
- Las categorías pueden ser jerárquicas (relaciones padre/hijo)
- Los datos de categoría se almacenan en las tablas
wp_termsywp_term_taxonomy - Las relaciones post-categoría se almacenan en
wp_term_relationships
Entender esta estructura te ayuda a escribir consultas más eficientes y soluciónar problemas cuando surjan.
Encontrar el ID de una categoría a partir de su slug o nombre
La mayoría de los argumentos de consulta aceptan tanto un slug como un ID numérico, pero los argumentos basados en ID (cat, category__in) son los más rápidos porque se saltan la búsqueda del término. Cuando solo conoces el slug, resuélvelo una vez y reutiliza el ID:
// From a slug
$term = get_category_by_slug('news');
$news_id = $term ? $term->term_id : 0;
// From a display name
$news_id = get_cat_ID('News'); // returns 0 if not found
// From any taxonomy (categories included)
$term = get_term_by('slug', 'news', 'category');Cachea el ID resuelto en una constante o en una opción si lo dejas fijo en una plantilla; llamar a get_cat_ID() en cada petición añade una consulta que puedes evitar.
Obtener posts por categoría con WP_Query
WP_Query es la clase principal de WordPress para consultar posts. Ofrece máxima flexibilidad y es el enfoque recomendado para la mayoría de los casos de uso.
Consulta por slug de categoría
$args = array(
'category_name' => 'noticias',
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC'
);
$query = new WP_Query($args);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
// Mostrar contenido del post
?>
<article>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<div class="entry-content">
<?php the_excerpt(); ?>
</div>
</article>
<?php
}
wp_reset_postdata();
}Consulta por ID de categoría
$args = array(
'cat' => 5, // ID de categoria
'posts_per_page' => 5
);
$query = new WP_Query($args);Consultar posts de varias categorías
// Posts en CUALQUIERA de estas categorias (relacion OR)
$args = array(
'category__in' => array(5, 10, 15),
'posts_per_page' => 10
);
// Posts en TODAS estas categorias (relacion AND)
$args = array(
'category__and' => array(5, 10),
'posts_per_page' => 10
);
// Excluir categorias específicas
$args = array(
'category__not_in' => array(3, 7),
'posts_per_page' => 10
);Incluir categorías hijas en WP_Query
// Obtener posts de la categoria y todas sus hijas
$id_categoria_padre = 5;
$args = array(
'cat' => $id_categoria_padre,
'posts_per_page' => 20
);
// WP_Query incluye automáticamente categorias hijas cuando usas 'cat'Paginar una consulta de categoría en una portada estática
Un loop personalizado pagina con paged, que se lee con get_query_var( 'paged' ). La excepción es una plantilla de página configurada como portada estática: ahí WordPress guarda el número de página en la variable page, por eso la referencia de WP_Query indica leer get_query_var( 'page' ). Si usas la equivocada, cada clic en “siguiente” vuelve a mostrar la página uno.
$paged = ( is_front_page() && is_page() ) ? get_query_var( 'page' ) : get_query_var( 'paged' );
$paged = max( 1, (int) $paged );
$query = new WP_Query( array(
'category_name' => 'noticias',
'posts_per_page' => 10,
'paged' => $paged,
) );Qué parámetros de categoría incluyen las categorías hijas
El mismo ID de categoría devuelve listas distintas según la interfaz con la que lo consultes. Este detalle explica la mayoría de los avisos del tipo “a la lista le faltan posts”, y las reglas están fijadas en el código del núcleo, no en ajustes:
| Interfaz | Argumento | Posts de categorías hijas |
|---|---|---|
| WP_Query | cat | incluidos |
| WP_Query | category_name | incluidos |
| WP_Query | category__in, category__and, category__not_in | no incluidos |
| WP_Query | tax_query | incluidos, salvo que include_children sea false |
| get_posts() | category | incluidos (es un alias de cat) |
| REST API | categories=5 | no incluidos |
| REST API | categories[terms]=5&categories[include_children]=true | incluidos |
| Bloque Query Loop | filtro de categoría | no incluidos |
| WP-CLI | --cat, --category_name | incluidos (pasan directamente a WP_Query) |
Las filas de WP_Query salen de WP_Query::parse_tax_query(): cat se convierte en una tax query con include_children en true, mientras que category__in, category__and y category__not_in se convierten en tax queries con include_children en false. La referencia de WP_Query dice lo mismo en sus ejemplos: category__in no muestra posts de las categorías hijas. La fila de get_posts() se deduce de su documentación, que describe category como alias de cat. Las filas de la REST API vienen del controlador de posts, donde include_children vale false por defecto y solo se ofrece para taxonomías jerárquicas. La fila del Query Loop viene de build_query_vars_from_query_block() en el blocks.php del núcleo, que fija include_children => false para cada filtro de taxonomía.
La consecuencia práctica: una plantilla PHP con cat => 5 y un front end headless que llama a /wp/v2/posts?categories=5 pueden mostrar posts distintos para la misma categoría padre. Decide qué comportamiento quieres y elige el argumento que lo da, en lugar de suponer que ambos coinciden.
get_posts por categoría en WordPress
Para casos de uso más simples, get_posts() proporciona una API más directa.
get_posts con ID de categoría
$posts = get_posts(array(
'category' => 5,
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC'
));
foreach ($posts as $post) {
setup_postdata($post);
?>
<article>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
</article>
<?php
}
wp_reset_postdata();get_posts con nombre de categoría
$posts = get_posts(array(
'category_name' => 'tecnologia', // el slug, sin tilde
'numberposts' => 5
));Qué cambia get_posts frente a WP_Query
get_posts() es un envoltorio fino que construye un WP_Query con sus propios valores por defecto. El código de la función muestra cuatro diferencias que importan en listas de categoría:
numberpostsvale 5 por defecto, así que una llamada sin límite devuelve cinco posts sin avisar.post_statusvalepublishysuppress_filtersvaletrue, de modo queposts_wherey otros filtros SQL de plugins no se ejecutan. La acciónpre_get_postssí se dispara.ignore_sticky_postsyno_found_rowsse fuerzan siempre atrue. No puedes construir una paginación numerada sobreget_posts(), porque el total nunca se calcula.includefijaposts_per_pageen el número de IDs que pasas y anula tu propio límite.
Hay una trampa más cuando llamas a get_posts() dentro de una función o del callback de un shortcode. setup_postdata() prepara las etiquetas de plantilla, pero the_title() y the_permalink() siguen leyendo el $post global. Declara global $post; antes del bucle foreach ( $posts as $post ), como hacen los ejemplos de la referencia, y termina con wp_reset_postdata(). Sin esa declaración, cada elemento de la lista imprime el título de la página donde está el shortcode.
Excluir una categoría de una consulta de WordPress
Excluir funciona de dos maneras, y cada una trata las categorías hijas de forma distinta. Un ID negativo en cat excluye la categoría y sus hijas, porque el núcleo construye una tax query NOT IN con include_children en true. category__not_in excluye solo los términos indicados, así que los posts archivados en una categoría hija siguen apareciendo.
// Excluye la categoría 12 y todas sus hijas
$args = array( 'cat' => '-12' );
// Excluye solo los posts archivados directamente en 12 o 34
$args = array( 'category__not_in' => array( 12, 34 ) );Para sacar una categoría de la página principal del blog, modifica la consulta principal en lugar de ejecutar un segundo loop. La referencia de pre_get_posts recomienda exactamente estas condiciones:
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
$query->set( 'cat', '-12' );
}
} );Shortcode para listar posts de una categoría
Crear un shortcode permite a los editores de contenido insertar listas de posts por categoría en cualquier lugar.
function shortcode_posts_categoria($atts) {
$atts = shortcode_atts(array(
'categoria' => '',
'posts' => 5,
'orderby' => 'date',
'order' => 'DESC'
), $atts);
$args = array(
'category_name' => $atts['categoria'],
'posts_per_page' => intval($atts['posts']),
'orderby' => $atts['orderby'],
'order' => $atts['order']
);
$query = new WP_Query($args);
ob_start();
if ($query->have_posts()) {
echo '<div class="lista-posts-categoria">';
while ($query->have_posts()) {
$query->the_post();
?>
<article class="post-categoria">
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<p><?php the_excerpt(); ?></p>
</article>
<?php
}
echo '</div>';
} else {
echo '<p>No se encontraron posts en esta categoria.</p>';
}
wp_reset_postdata();
return ob_get_clean();
}
add_shortcode('posts_categoria', 'shortcode_posts_categoria');Uso: [posts_categoria categoria="noticias" posts="5"]
Modificar el archivo de categoría con pre_get_posts
Cuando quieras cambiar que posts aparecen en las páginas de archivo de categoría, usa la acción pre_get_posts.
function modificar_consultas_categoria($query) {
// Solo modificar archivos de categoria en la consulta principal
if ($query->is_category() && $query->is_main_query() && !is_admin()) {
// Mostrar 20 posts por página en lugar del predeterminado
$query->set('posts_per_page', 20);
// Excluir posts de categoria específica en ciertas páginas de categoria
$cat_actual = get_queried_object();
if ($cat_actual->slug === 'destacados') {
$query->set('category__not_in', array(10)); // Excluir categoria ID 10
}
}
}
add_action('pre_get_posts', 'modificar_consultas_categoria');Dentro del callback usa las versiones de método de las etiquetas condicionales ($query->is_category(), $query->is_main_query()). Las funciones sueltas consultan el $wp_query global y no el objeto que recibe el hook, algo que la referencia del hook advierte de forma explícita.
Por qué query_posts no sirve para filtrar por categoría
Los tutoriales antiguos colocan query_posts( 'cat=5' ) al principio de una plantilla. El docblock de query_posts() en el núcleo dice que la función sustituye por completo la consulta principal, que no está pensada para plugins ni temas y que en la mayoría de los casos hay que usar la acción pre_get_posts. El cuerpo de la función es una sola línea que asigna un nuevo WP_Query al $wp_query global. Una plantilla se ejecuta después de la consulta principal, así que la llamada repite el trabajo en la base de datos y reemplaza el objeto que leen la paginación y las etiquetas condicionales. Cada tarea de categoría que usaba query_posts() se resuelve con pre_get_posts (cambiar el archivo) o con un WP_Query aparte (añadir una segunda lista).
Filtrar posts por categoría con tax_query
Los atajos cat y category_name son cómodos, pero tax_query es el argumento al que recurres cuando los requisitos se vuelven específicos: combinar categorías con etiquetas, excluir términos hijos o consultar una taxonomía personalizada. Además expresa la lógica AND/OR de forma explícita, algo que los argumentos abreviados no pueden hacer.
$args = array(
'posts_per_page' => 10,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'category',
'field' => 'slug',
'terms' => array('news'),
),
array(
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => array('featured'),
),
),
);
$query = new WP_Query($args);La clave include_children es el detalle práctico que la mayoría pasa por alto. Por defecto, una tax_query sobre category arrastra todos los términos descendientes, lo que suele ser lo que quiere un archivo pero rara vez lo que quiere un bloque curado de la homepage:
'tax_query' => array(
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array(5),
'include_children' => false, // only posts filed directly under term 5
),
),Como tax_query funciona contra cualquier taxonomía, el mismo patrón recupera posts de un product_cat de WooCommerce, un portfolio_type o cualquier taxonomía que registre tu tema, sin necesidad de una función distinta por taxonomía.
Los filtros de categoría también se combinan con campos personalizados. Una lista de eventos que muestra solo las próximas citas de dos categorías junta category__in con una meta_query y ordena por el campo de fecha:
$args = array(
'posts_per_page' => 10,
'category__in' => array( get_cat_ID( 'Conferencias' ), get_cat_ID( 'Talleres' ) ),
'meta_key' => 'fecha_evento',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => 'fecha_evento',
'value' => current_time( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
),
);get_cat_ID() recibe el nombre de la categoría, no el slug, y devuelve 0 si no encuentra nada. Un 0 dentro de category__in no coincide con ningún post, así que comprueba los IDs antes de ejecutar la consulta en una página de producción.
Acelerar consultas de categoría con caché
1. Usar Transients para consultas costosas
function obtener_posts_categoria_con_cache($id_categoria, $cantidad = 5) {
$clave_cache = 'cat_posts_' . $id_categoria . '_' . $cantidad;
$posts = get_transient($clave_cache);
if (false === $posts) {
$args = array(
'cat' => $id_categoria,
'posts_per_page' => $cantidad
);
$query = new WP_Query($args);
$posts = $query->posts;
// Cache por 1 hora
set_transient($clave_cache, $posts, HOUR_IN_SECONDS);
}
return $posts;
}2. Optimizar consultas de base de datos
// Solo recuperar los campos que necesitas
$args = array(
'category_name' => 'noticias',
'posts_per_page' => 10,
'fields' => 'ids' // Solo obtener IDs de posts para mejor rendimiento
);
$query = new WP_Query($args);El argumento fields acepta exactamente tres valores, según la referencia de WP_Query: all (por defecto), ids e id=>parent. Cualquier otro valor, incluido un array de nombres de columna, devuelve sin avisar objetos de post completos. Dos interruptores más ahorran trabajo en listas que solo imprimen título y enlace:
$args = array(
'category_name' => 'noticias',
'posts_per_page' => 5,
'no_found_rows' => true, // sin SQL_CALC_FOUND_ROWS, no hay paginación
'update_post_meta_cache' => false, // no se imprimen campos personalizados
'update_post_term_cache' => false, // no se imprimen términos
);no_found_rows evita que WordPress cuente el total de filas coincidentes, un dato que solo necesita para paginar. Los dos indicadores de caché valen true por defecto en el núcleo; la referencia dice que normalmente no hace falta tocarlos, pero desactivarlos ahorra las consultas extra de meta y términos cuando la lista nunca lee esos datos. Déjalos activados cuando el loop imprime categorías, etiquetas o campos personalizados, porque precargar la caché en una consulta sale más barato que una consulta por post.
3. Usar Object Caching
Si tu sitio usa un object cache (Redis, Memcached), los resultados de WP_Query se cachean automáticamente, mejorando el rendimiento para consultas repetidas.
Plantillas de categoría, AJAX, Query Loop y REST API
Plantillas personalizadas para archivos de categoría
Crea un archivo de plantilla category-noticias.php para estilos específicos de categoría:
<?php
/* Plantilla de archivo para el slug "noticias", la elige la jerarquía de plantillas */
get_header(); ?>
<div class="archivo-categoria">
<h1><?php single_cat_title(); ?></h1>
<?php if (have_posts()) : ?>
<div class="cuadricula-posts">
<?php while (have_posts()) : the_post(); ?>
<?php get_template_part('content', 'category'); ?>
<?php endwhile; ?>
</div>
<?php the_posts_págination(); ?>
<?php else : ?>
<p>No se encontraron posts en esta categoria.</p>
<?php endif; ?>
</div>
<?php get_footer(); ?>Carga AJAX para posts de categoría
Para una mejor experiencia de usuario, implementa carga AJAX. Hay dos detalles que hacen tropezar a mucha gente: ajaxurl solo está definido en wp-admin, así que en el front end tienes que pasarlo tú mismo, y cada petición necesita un nonce para superar una revisión de seguridad.
Encola el script y entrégale la URL de admin-ajax junto con un nonce:
function enqueue_category_loader() {
wp_enqueue_script(
'category-loader',
get_theme_file_uri('/js/category-loader.js'),
array('jquery'),
'1.0',
true
);
wp_localize_script('category-loader', 'catLoader', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('load_category_posts'),
));
}
add_action('wp_enqueue_scripts', 'enqueue_category_loader');La petición desde el front end lee entonces del objeto localizado:
jQuery(document).ready(function($) {
$('.load-more').on('click', function() {
var button = $(this);
$.ajax({
url: catLoader.ajaxurl,
type: 'POST',
data: {
action: 'load_category_posts',
nonce: catLoader.nonce,
category: button.data('category'),
page: button.data('page')
},
success: function(response) {
$('.posts-container').append(response);
button.data('page', button.data('page') + 1);
}
});
});
});Por último, registra el handler tanto para usuarios autenticados como anónimos (wp_ajax_ y wp_ajax_nopriv_), verifica el nonce y devuelve el marcado renderizado:
function load_category_posts_handler() {
check_ajax_referer('load_category_posts', 'nonce');
$category = sanitize_text_field($_POST['category'] ?? '');
$page = max(1, intval($_POST['page'] ?? 1));
$query = new WP_Query(array(
'category_name' => $category,
'posts_per_page' => 5,
'paged' => $page,
));
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
printf(
'<article><h3><a href="%s">%s</a></h3></article>',
esc_url(get_permalink()),
esc_html(get_the_title())
);
}
wp_reset_postdata();
}
wp_die(); // required so admin-ajax stops cleanly
}
add_action('wp_ajax_load_category_posts', 'load_category_posts_handler');
add_action('wp_ajax_nopriv_load_category_posts', 'load_category_posts_handler');Mostrar posts de categoría en un tema de bloques
En los temas de bloques (el tipo de tema por defecto desde que Twenty Twenty-Two llegó con WordPress 5.9) a menudo no necesitas PHP en absoluto. El bloque Query Loop filtrado por categoría muestra la misma lista desde el editor: inserta un Query Loop, abre los ajustes del bloque y, en Filtros, añade la categoría que quieras. Guárdalo como un patrón sincronizado y los editores podrán colocar la lista curada en cualquier página sin tocar una plantilla.
El filtro de categoría del bloque solo coincide con los términos que seleccionas. El núcleo lo convierte en una tax query con include_children en false, así que elegir una categoría padre no trae los posts archivados solo en sus hijas; añade también las categorías hijas al filtro.
Recurre a los enfoques con código de arriba cuando necesites lógica que el bloque no puede expresar: cache con transients, exclusiones condicionales por archivo o una salida que consuma algo distinto del tema (un boletín por correo, una respuesta REST).
Obtener posts de categoría a través de la REST API
Para front ends headless, una isla de React o una integración externa, WordPress expone los posts de categoría a través de la REST API sin necesidad de un endpoint personalizado:
GET /wp-json/wp/v2/posts?categories=5&per_page=10&_fields=id,title,link,excerptUsa categories_exclude para filtrar un término, _embed para traer las imágenes destacadas y los nombres de términos en una sola llamada, y _fields para reducir la carga a lo que el cliente realmente muestra. Para resolver un slug al ID que espera el endpoint, consulta primero /wp-json/wp/v2/categories?slug=news.
Tres detalles del manual de la REST API deciden si una lista headless sale completa:
- Las categorías hijas necesitan la forma de objeto.
categories=5solo coincide con el término 5. Para incluir sus hijas, envíacategories[terms]=5&categories[include_children]=true. El mismo objeto aceptacategories[operator]=ANDcuando un post debe tener todos los términos indicados. - Las páginas tienen un tope de 100 elementos. La guía de paginación limita
per_pagea un valor entre 1 y 100. Lee las cabecerasX-WP-TotalyX-WP-TotalPagesde la respuesta y pidepage=2,page=3hasta tenerlo todo. _fieldsadmite claves anidadas. Desde WordPress 5.3 el parámetro_fieldsacepta rutas comometa.fecha_evento, de modo que una lista puede pedir una sola clave meta registrada sin todo el objeto meta. WordPress deja de calcular los campos que no pediste, lo que también acorta el tiempo de respuesta.
Depurar una consulta de categoría que devuelve los posts equivocados
Cuando una consulta devuelve demasiados posts, muy pocos o posts inesperados, inspecciona el SQL que realmente ejecutó en lugar de adivinar con los argumentos. Instala Query Monitor y te mostrará todas las consultas de la página, incluida la que genera tu loop, con las lentas marcadas. Para una comprobación rápida sin plugin, vuelca la consulta ya interpretada:
$query = new WP_Query($args);
// The exact SQL WordPress built from your $args
error_log($query->request);Los dos culpables detrás de la mayoría de las sorpresas son la inclusión de términos hijos (mira include_children más arriba) y algún filtro pre_get_posts perdido de un plugin o del tema que altera la consulta principal. Si el SQL parece correcto pero el resultado está vacío, confirma que el término tenga realmente posts publicados asignados en el idioma o contexto actual. En sitios multilingües con Polylang o WPML, la categoría puede existir mientras los posts están asignados a su versión en otro idioma.
Listar posts de una categoría con WP-CLI
Para auditorías, migraciones y exportaciones puntuales, wp post list ejecuta el mismo WP_Query desde la línea de comandos. Cada opción --<campo>=<valor> pasa a WP_Query como argumento, así que las reglas de categoría de la tabla anterior se aplican sin cambios:
# ID del término y número de posts de cada categoría
wp term list category --fields=term_id,name,slug,count
# Posts publicados en "noticias" y sus categorías hijas, en CSV
wp post list --category_name=noticias --post_status=publish --fields=ID,post_title,post_date --format=csv
# Solo IDs, posts archivados directamente en la categoría 12 (sin hijas)
wp post list --category__in=12 --post_status=publish --format=ids
# Cuántos posts mostraría una plantilla para la categoría 12 y sus hijas
wp post list --cat=12 --post_status=publish --format=countDos valores por defecto difieren de un loop de plantilla. El código del comando fija posts_per_page en -1 y post_status en any, así que sin --post_status=publish la salida incluye borradores, posts pendientes y privados. Las columnas por defecto son ID, post_title, post_name, post_date y post_status, y --format acepta table, csv, ids, json, count y yaml. Comparar --format=count con --cat y con --category__in para el mismo ID es una forma rápida de ver cuántos posts están solo en categorías hijas.
Errores comunes con WP_Query y categorías
- No restablecer post data: Siempre llama a
wp_reset_postdata()después de loops personalizados - Consultar en cada carga de página: Usa cache para consultas costosas
- No verificar si existen posts: Siempre verifica
have_posts()antes de iterar - Modificar la consulta principal incorrectamente: Usa
pre_get_postsen lugar de crear nuevas consultas en páginas de archivo - Ignorar la páginación: Recuerda manejar la páginación para archivos de categoría grandes
Conclusión
WordPress proporciona múltiples formás de extraer posts de categorías, cada una adecuada para diferentes escenarios:
- WP_Query: Mejor para visualizaciones complejas y personalizadas
- get_posts(): Ideal para listas simples de posts
- Shortcodes: Perfecto para flexibilidad del editor de contenido
- pre_get_posts: Esencial para modificar páginas de archivo
Entender estos métodos y cuando usar cada uno te hará un desarrollador WordPress más efectivo. Recuerda siempre considerar el rendimiento, especialmente en sitios con grandes cantidades de contenido.
Para sitios en producción, implementa estrategias de cache y prueba tus consultas con herramientas como Query Monitor para asegurar un rendimiento óptimo.
Necesitas ayuda con consultas avanzadas de WordPress? Como desarrollador WordPress, puedo implementar soluciones optimizadas para tu proyecto. Contáctame.







