How to output tag related posts first, then IF there are less than 4 tag related posts, fill the rest of the 4 spots with category related posts?


Sometimes a post has very few tags, or the tags it is labeled with have very fews posts. When outputting "related posts by tag," the area is either very sparse with 1 or 2 posts, or outright empty.

To solve this it would be good to show posts from related categories, if there are not enough tag related posts to satisfy posts_per_page => X.

So, something like this:

Scenario A -- if more than 4 tag-related posts exist, then:

Related Posts:

Show the below posts:

1. tag-related post #1
2. tag-related post #2
3. tag-related post #3
4. tag-related post #4

Do Not show the below posts:

5. tag-related post #5
6. tag-related post #6
7. tag-related post #7

Scenario B -- if only 2 tag-related posts exist, then:

Related Posts:

Show the below posts:

1. tag-related post #1
2. tag-related post #2  
3. category-related post #1
4. category-related post #2

Do Not show the below posts:

5. category-related post #3
6. category-related post #4
7. category-related post #5

What I've tried

The tax query I am using:

// start of the tax_query arguments
$args = array( 'posts_per_page'=>4, 'post__not_in' => array($post->ID), 'tax_query' => array( 'relation' => 'OR' ) );

// get current post tags
$tags = wp_get_object_terms( $post->ID, 'post_tag', array( 'fields' => 'ids' ) );

if ( !empty( $tags ) ) {
    $args['tax_query'][] = array(
        'taxonomy' => 'post_tag',
        'field'    => 'id',
        'terms'    => $tags

// get current post categories
$categories = wp_get_object_terms( $post->ID, 'category', array( 'fields' => 'ids' ) );

if ( !empty( $categories ) ) {
    $args['tax_query'][] = array(
        'taxonomy' => 'category',
        'field'    => 'id',
        'terms'    => $categories

// the query
$related_query = new WP_Query( $args );

As I understand it, that tax query says "get posts that are in the same categories, then get posts that are in the same tags, then output posts until 4 are on screen."

Yet it keeps outputting category posts first (of which there are many) which satisfies the 4 posts on screens rule and leaves out the most important tag-related posts. I've tried moving the code around, using AND instead of OR, which didn't work and made no sense to me anyways.

I've also seen these posts: WordPress - producing a list of posts filtered by tag and then category and Wordpress query posts by tag and category, but they are about outputting a list of posts that are filtered by tag AND category. I need posts first related by tag, and if 4 or more, then just output those top 4. If less than 4, then output up to as many category-related posts to meet the 4 posts criteria.

Clearly I am misunderstanding the query and/or the problem, so any help would be appreciated.


Peter's solution works perfectly -- thank you Peter!

I've further built on it a bit for my own purposes, and using your awesomely commented code, I was able to also Exclude "Recent Posts" from this query -- woohoo!


1 Answer 1


You cannot do this in one query, this is a bit too advanced for what WP_Query can do at this point in time. We will need to run at least two (I'm going to use 3) queries here to achieve what you want


The first query will query for posts according to post_tag, that will also exclude the current post. We will only get post ID's


This query will handle the "fill-up" posts which will come from the category taxonomy. For this query to be successful, we will need the following

  • Exclude the current post and the posts from the first query to avoid duplicate posts

  • Get the amount of posts in the first query, subtract that from 4, and then use that diffrence to set the amount of posts that should be retrieved by the this query

We will also just get post ID's here


In order to keep the integrity from the query object, we will combine all the ID's from the previousquery and query the final post objects. You might think this is expensive, but it is not. Because the first two queries uses get_posts() and only get post ID's, they are really super fast. By querying only ID's, we also do not updates caches which makes these queries even faster


I prefer creating a function for such big pieces as code as it keeps my templates clean. Please note, the following is untested and requires PHP 5.4

function get_max_related_posts( $taxonomy_1 = 'post_tag', $taxonomy_2 = 'category', $total_posts = 4 )
    // First, make sure we are on a single page, if not, bail
    if ( !is_single() )
        return false;

    // Sanitize and vaidate our incoming data
    if ( 'post_tag' !== $taxonomy_1 ) {
        $taxonomy_1 = filter_var( $taxonomy_1, FILTER_SANITIZE_STRING );
        if ( !taxonomy_exists( $taxonomy_1 ) )
            return false;

    if ( 'category' !== $taxonomy_2 ) {
        $taxonomy_2 = filter_var( $taxonomy_2, FILTER_SANITIZE_STRING );
        if ( !taxonomy_exists( $taxonomy_2 ) )
            return false;

    if ( 4 !== $total_posts ) {
        $total_posts = filter_var( $total_posts, FILTER_VALIDATE_INT );
            if ( !$total_posts )
                return false;

    // Everything checks out and is sanitized, lets get the current post
    $current_post = sanitize_post( $GLOBALS['wp_the_query']->get_queried_object() );

    // Lets get the first taxonomy's terms belonging to the post
    $terms_1 = get_the_terms( $current_post, $taxonomy_1 );

    // Set a varaible to hold the post count from first query
    $count = 0;
    // Set a variable to hold the results from query 1
    $q_1   = [];

    // Make sure we have terms
    if ( $terms_1 ) {
        // Lets get the term ID's
        $term_1_ids = wp_list_pluck( $terms_1, 'term_id' );

        // Lets build the query to get related posts
        $args_1 = [
            'post_type'      => $current_post->post_type,
            'post__not_in'   => [$current_post->ID],
            'posts_per_page' => $total_posts,
            'fields'         => 'ids',
            'tax_query'      => [
                    'taxonomy'         => $taxonomy_1,
                    'terms'            => $term_1_ids,
                    'include_children' => false
        $q_1 = get_posts( $args_1 );
        // Count the total amount of posts
        $q_1_count = count( $q_1 );

        // Update our counter
        $count = $q_1_count;

    // We will now run the second query if $count is less than $total_posts
    if ( $count < $total_posts ) {
        $terms_2 = get_the_terms( $current_post, $taxonomy_2 );
        // Make sure we have terms
        if ( $terms_2 ) {
            // Lets get the term ID's
            $term_2_ids = wp_list_pluck( $terms_2, 'term_id' );

            // Calculate the amount of post to get
            $diff = $total_posts - $count;

            // Create an array of post ID's to exclude
            if ( $q_1 ) {
                $exclude = array_merge( [$current_post->ID], $q_1 );
            } else {
                $exclude = [$current_post->ID];

            $args_2 = [
                'post_type'      => $current_post->post_type,
                'post__not_in'   => $exclude,
                'posts_per_page' => $diff,
                'fields'         => 'ids',
                'tax_query'      => [
                        'taxonomy'         => $taxonomy_2,
                        'terms'            => $term_2_ids,
                        'include_children' => false
            $q_2 = get_posts( $args_2 );

            if ( $q_2 ) {
                // Merge the two results into one array of ID's
                $q_1 = array_merge( $q_1, $q_2 );

    // Make sure we have an array of ID's
    if ( !$q_1 )
        return false;

    // Run our last query, and output the results
    $final_args = [
        'ignore_sticky_posts' => 1,
        'post_type'           => $current_post->post_type,
        'posts_per_page'      => count( $q_1 ),
        'post__in'            => $q_1,
        'order'               => 'ASC',
        'orderby'             => 'post__in',
        'suppress_filters'    => true,
        'no_found_rows'       => true
    $final_query = new WP_Query( $final_args );

    return $final_query;

You can now use the function as follow in your single template

$query = get_max_related_posts();
if ( $query ) {

    while ( $query->have_posts() ) {

            echo get_the_title() . '</br>';



  • The defaults are already set to post_tag, category and 4 for the three parameters respectively, so you do need need to pass any values to the function when calling it

  • If you need to swop the taxonomies around or set different taxonomies or set the posts per page to anything else than 4, simply pass them in the correct order to the function

    $query = get_max_related_posts( 'tax_1', 'tax_2', 6 );
  • Sorry, there where a few issues which I now sorted. My code is tested and working as it should Commented Mar 3, 2016 at 14:07
  • Amazing answer, Peter! Not only is it everything I need, but it's also a tremendous learning tool for me -- thank you! I have since expanded it a bit to exclude "Recent Posts" from a previous on-page query, and am now trying to extend it to "post_type" when there are not enough in tags or cats. Commented Mar 4, 2016 at 5:33
  • 1
    My pleasure, feel free to start a new question if you get stuck with this. Enjoy, and good luck on your project ;-) Commented Mar 4, 2016 at 5:53
  • You got it! For your pleasure :D Commented Mar 4, 2016 at 6:19
  • Also, please copy/paste your answer here so I can accept it and up vote you. Commented Mar 4, 2016 at 6:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.