How to show recent posts from specific category in WordPress

A lifted card with a dashed outline and a round no-entry badge stands for the post being read, with a sticker saying You are here and the words left out. Below it, three solid post cards in a list, each with a coloured icon tile and a Guides chip, and a speech bubble saying 3 more from Guides. A magnifying glass peeks out from behind the list. Huge text reads Same category with category in tomato red and a wavy underline, over the lines recent posts, never the same post, and two chips say Under every post and WP_Query. An Updated guide ribbon hangs top right.

In short: To list recent posts from the same category as the post being read, use a Query Loop block filtered by that category with the current post excluded, or a short WP_Query in single.php that reads the post’s category IDs. Both worked on WordPress 7.1.3.

A common request on a blog theme: the post page loads through single.php, and below the post content the owner wants a short list of recent posts from the same category as the post on screen, without repeating the post the reader is already on. The options below cover a fixed category list and a list that follows the current post's categories.

Tested on WordPress 7.1.3 and PHP 8.3, in the Twenty Twenty-Five block theme and in a minimal classic theme with its own single.php.

Do I need code to show recent posts from one category?

No. The Latest Posts block can filter by category. Add the block, open its settings, go to the sorting and filtering section, choose the category and set how many posts to show.[1]

It has two limits, and both showed up in testing:

  • The category is fixed when you build the block. It does not follow the post being read.
  • It cannot leave out the current post. A Latest Posts block placed in a post of that category listed the post it sat in.

Use it for a sidebar or footer list such as "Latest from Guides". For a list that belongs under each post, use one of the next two routes.

How do I show posts from the same category without repeating the current one?

Use the Query Loop block. Select it, set Query type to Custom, and open the filters. If you do not see the filter options, the block is still on Default. Add the Taxonomies filter, pick the category, set the number of items, and turn on the option that excludes the current post.[2]

The exclude-current option arrived in WordPress 7.1; the 7.1 source documents it as the excludeCurrent query property.[3] On older versions the block cannot do this on its own, so use the code route below.

The category is still chosen by hand. A single template that must follow whichever category each post has cannot do that with the block alone, which is the case for the code below.

How do I show recent posts from the current post's category in single.php?

Put this function in the functions.php of a child theme (or a small plugin), then print its result in single.php after the post content.

Before you change anything: a typo in theme code can take a site down. Try this on a staging copy first, keep a backup of the file you edit, put it in a child theme so a theme update does not overwrite it, and make the change at your own risk. iLogr cannot see your site and accepts no responsibility for changes made to it.

PHP
function recent_posts_same_category() {
    $category_ids = wp_get_post_categories( get_the_ID() );
    if ( ! $category_ids ) {
        return '';
    }

    $query = new WP_Query( array(
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => 3,
        'category__in'   => $category_ids,
        'post__not_in'   => array( get_the_ID() ),
        'no_found_rows'  => true,
    ) );

    if ( ! $query->have_posts() ) {
        return '';
    }

    $html = '<ul class="same-category-posts">';
    while ( $query->have_posts() ) {
        $query->the_post();
        $html .= '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></li>';
    }
    $html .= '</ul>';
    wp_reset_postdata();

    return $html;
}
add_shortcode( 'same_category_posts', 'recent_posts_same_category' );

In single.php, after the_content();, add:

PHP
echo recent_posts_same_category();

What each part does:

  • wp_get_post_categories() returns the current post's category IDs by default, so a post filed in two categories gets recent posts from either.[4]
  • category__in matches those IDs only. The cat parameter also matches child categories; category__in does not.[5] Use cat with the IDs joined by commas if sub-categories should count.
  • post__not_in takes an array of post IDs, never a comma-separated string. It removes the current post.[5]
  • no_found_rows skips the extra query that counts all matching rows, which a three-item list never uses.[5] In a test it removed the FOUND_ROWS count query.
  • The function returns an empty string when the post has no category or no other post shares it, so the page does not print an empty heading or list.
  • wp_reset_postdata() puts the page's own post back after the loop.[6]

Change posts_per_page to show more or fewer posts. The output is escaped where it is built, so printing it with echo is safe.

Why did the first version of this snippet show nothing?

The original answer on this page had four faults. They are worth knowing because the same shapes turn up in many copied snippets.

Field note: the old snippet, run on WordPress 7.1.3. On a post in a category called Guides, the old code returned three posts from other categories, because its category filter applied only to four hard-coded category names that were specific to the site it was first written for. The loop called the_post() and printed nothing, so no list reached the page. It never reset the post data, so after the loop the page's own post ID had changed from 4 to 18, and anything printed below it (a title, a comment form, a "next post" link) would belong to the wrong post. On a post filed in Guides and News, get_the_category() returned Guides first and News second, in name order, so using the first entry as "the" category is a guess, not a main category.

The fixes are the ones in the function above: use all of the post's category IDs, print a list, return nothing when there is nothing to show, and reset the post data.

How do I use it in a block theme or page builder?

A block theme builds post pages from block templates instead of single.php, so there is no PHP file to call the function from. The code above also registers a shortcode. Add a Shortcode block where the list should appear and type [same_category_posts]. In a test it listed three posts from the post's two categories and left out the post it sat in. The post content is where it was tested; theme templates were not.

On a block theme that supports the exclude option, the Query Loop route is usually simpler. Keep the shortcode for a site that needs the list to follow each post's own category.

Should I use a related posts plugin instead?

It depends on what "related" means. Category matching shows posts an editor filed under the same label. A related posts plugin looks for similar content, so it can find posts about the same thing under a different label. The trade-off is another plugin to maintain, and the cost depends on the plugin: Jetpack's related posts, for example, are worked out on WordPress.com servers and need a working Jetpack connection, so processing and privacy follow that choice.[8]

The three routes above compare like this:

Item Latest Posts block Query Loop block Short WP_Query
Follows each post's own category No No Yes
Leaves out the current post No Yes (WordPress 7.1 and later) Yes
Code to maintain None None About 30 lines
A decision path. First question: must the list follow each post's own category? If yes, use a short WP_Query or the shortcode. If no, second question: must it leave out the post being read? If yes, use the Query Loop block, on WordPress 7.1 and later. If no, use the Latest Posts block. A note says category matching finds the same label, not the same topic.
Which route fits depends on two questions.

Page caching hides most of the per-page cost of any of them, because a cached page does not run the query again. If you would rather install a plugin, check its directory listing first. The listings for Contextual Related Posts and Jetpack show releases tested up to WordPress 7.1.3, while the last YARPP release in the directory is from November 2024 and is listed as tested only up to WordPress 6.7.10.[7] That is a maintenance signal to weigh, not a verdict, and the plugins themselves were not tested for this page.

For matching by meaning rather than by label, see semantic search over WordPress and MySQL content.

References

Numbers in square brackets in the text link to these sources.

  1. Latest Posts block, WordPress documentation (opens in a new tab), last updated 27 August 2026.Used for: the category filter and the number-of-posts setting.
  2. Query Loop block, WordPress documentation (opens in a new tab), last updated 5 September 2026.Used for: Query type Custom, the Taxonomies filter and excluding the current post.
  3. WordPress 7.1.3 source: wp-includes/blocks.php, build_query_vars_from_query_block() (opens in a new tab).Used for: the exclude-current query property, added in 7.1.0.
  4. wp_get_post_categories(), WordPress developer reference (opens in a new tab).Used for: the default return value of category IDs.
  5. WordPress 7.1.3 source: wp-includes/class-wp-query.php, query parameters (opens in a new tab).Used for: what the category, post exclusion and row counting query parameters do.
  6. wp_reset_postdata(), WordPress developer reference (opens in a new tab).Used for: restoring the post after a separate loop.
  7. Plugin directory listings: Contextual Related Posts, Jetpack, YARPP (opens in a new tab).Used for: release dates and the WordPress versions each plugin is listed as tested up to.
  8. Related Posts, Jetpack support (opens in a new tab).Used for: where Jetpack works out related posts and what it needs.

2 Responses

  1. Useful stuff! though you have a typo in code which is showing html special chars in line 12. Thanks :)

Leave a Reply

Your email address will not be published. Required fields are marked * See the privacy policy for how comments are handled.