WP_Query (or the simpler get_posts() helper) to pull in exactly the posts you need.
Using WP_Query to list posts from a category
The most flexible way to grab posts from a specific category is a custom WP_Query. Here’s a complete, working example:
$query = new WP_Query( array(
'category_name' => 'news',
'posts_per_page' => 10,
) );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
// Do what ever you want with your posts
echo '<article id="post-' . get_the_ID() . '">';
echo '<h2><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></h2>';
echo '<div class="entry-summary">' . get_the_excerpt() . '</div>';
echo '</article>';
}
// Always reset the query after a custom WP_Query loop
wp_reset_postdata();
} else {
echo '<p>No posts found in this category.</p>';
}
A couple of notes on the arguments:
category_nameaccepts the category slug (not the display name). Use a comma-separated list of slugs to match any of several categories.- If you’d rather reference the category by its numeric ID, use
catinstead, e.g.'cat' => 5. Prefix the ID with a minus sign ('cat' => -5) to exclude that category instead. - By default,
WP_Queryautomatically includes posts from child categories of the one you specify, so you don’t need any extra logic to pull those in too. - Always pair a custom
WP_Queryloop withwp_reset_postdata()afterwards. It restores the global$postobject to the main query so the rest of your template behaves correctly.
A quicker option: get_posts()
If you just need a simple list of posts (for example, in a sidebar widget or a “related posts” block) and don’t need the full WP_Query object, get_posts() is a convenient shorthand. It returns a plain array of post objects:
$posts = get_posts( array(
'category_name' => 'news',
'numberposts' => 10,
) );
foreach ( $posts as $post ) {
setup_postdata( $post );
// Do what ever you want with your posts
echo '<h2><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></h2>';
}
wp_reset_postdata();
get_posts() accepts most of the same arguments as WP_Query (note it uses numberposts instead of posts_per_page). Because it still touches the global post data when you call setup_postdata(), remember to call wp_reset_postdata() once you’re done with the loop.
A more idiomatic alternative: pre_get_posts
If your goal is simply to change what shows up on the existing category archive page itself, rather than building a custom block somewhere in a template, you usually don’t need a separate query at all. Hook into pre_get_posts and modify the main query before it runs:
function ankurm_modify_category_query( $query ) {
if ( ! is_admin() && $query->is_main_query() && is_category( 'news' ) ) {
$query->set( 'posts_per_page', 10 );
}
}
add_action( 'pre_get_posts', 'ankurm_modify_category_query' );
This approach is generally preferred over a fresh WP_Query when you’re working with an actual category archive page, since it avoids running a duplicate database query and keeps pagination, SEO plugins, and other code that relies on the main query working correctly. Reach for a custom WP_Query or get_posts() call instead when you need a category listing in a spot that isn’t the main query, such as a widget, a shortcode, or a block on the front page.
FAQs
How do I list posts from a category by ID instead of slug?
Use the cat argument with the category’s numeric ID, e.g. 'cat' => 5, instead of category_name.
Will child categories be included automatically?
Yes. Both WP_Query and get_posts() include posts from child categories of the specified category by default.
Do I need wp_reset_postdata() with get_posts()?
Only if you call setup_postdata() inside your loop (which is needed for template tags like the_title() to work). If you access post properties directly instead, you can skip both calls.
Conclusion
For most category-listing needs, a custom WP_Query loop or the simpler get_posts() helper will get the job done. But if you’re customizing an existing category archive page rather than building a new listing elsewhere, hooking into pre_get_posts is the cleaner, more idiomatic solution.