Custom query – alternate posts by category

EDIT

From your comments, I have reworked the entire solution. I’m keeping the original answer just for reference

If I have 3 sticky posts, shouldn’t they be the first 3 posts on the first page, regardless of their post date? What’s happening is that only the sticky posts that happen to be in that first set of 10 posts based on their date are treated as sticky. The rest of the sticky posts show at the top of whatever page they fall into based on their date. How do I get them to show on the top of the FIRST page, regardless of date?

I have tested what you’ve said, and the behavior of the sticky posts are not as I expected it to be when using a tax query with a new instance of WP_Query.

Possible solutions

  • You can simply just run two queries, one for sticky posts and one as described in my original answer and just exclude sticky posts from the second query. You will just need to rework the original answer a bit. The drawback of this is that if you need exactly 10 posts per page, then this won’t work. To make it work you’ll have to make use of offsets

  • The solution that I went with, treat all conditions separately and later combining them all as one

SOLUTION

I went and tested a couple of solutions here as to get the one that will be the fastest and less resource intensive. The requirements set in the OP is quite unique, so this solution would most probably be overkill in other situations where you don’t need to alternate posts according to term, for instance if you just need posts according to date and sticky posts.

The complete solution uses up to four custom queries and you might think this is quite hard hitting the db, but it is not. On the contrary, this is actually two times faster than my original answer, because of the fact that you are not doing checks to see if a post belongs to a term or not. To speed this up significantly, I have made use of transients which makes this edit nearly four times faster than my original answer.

OK, lets code: (I’m not going into details on certain points as it has been handled in the original answer)

I have decided to post two possible used for the sticky posts as you will see. You will need to decide how you want to handle stickies, and then uncomment and comment code out according to your needs. Here are the two methods that I used

  • If your stickies are going to be all within the two set terms, you can simply just use the code as is

  • If your stickies are going include posts from outside the two set terms and you need to exclude those that does not belong to the two set terms, then you will need to uncomment the code that has been uncommented and then remove this line

    $q = array_merge( $sticky_post, $new_posts_array );
    

BASIC INNER WORKINGS OF THE CODE

The posts (we only retrieve the post ID’s here to make the queries faster) from a specific term is retrieved separately, the reason for this, it is faster than retrieving all posts at once and then sorting them according to term. To make sure that you don’t get duplicates, I have set the second query to exclude all posts that belongs to both terms. These posts are retrieved by the first query only

Each of the two returned arrays are then assigned new keys, one array will have even keys, the other array uneven keys. These arrays are then combined and sorted. The new array will now hold posts from both arrays sorted alternately by term

The final step is to add the sticky posts to this. What you now have is an array which holds post ID’s, sticky posts firsts, then posts sorted alternately by term. This array of ID’s is saved in a transient which will be updated every 7 days ( you can set this accordingly ) or when any post is updated, deleted or new post published.

This array of post ID’s is now used as normal to display the posts in a new WP_Query, and the sequence in which the post ID’s is will be used to sort the posts by. Here is the complete code

if ( false === ( $q = get_transient( 'ordered_posts' ) ) ) {

    $sticky_post = get_option( 'sticky_posts' );
    /* 
     * Only do this if sticky posts can belong to terms outside the given two terms
     * and you only need to include sticky posts that belongs to the given two terms
     * Uncomment this part if needed
    */
    /* 
    if( $sticky_post ) {
        $sticky_args = array(
            'post_type'         => 'services', 
            'posts_per_page'    => -1,
            'post__in'          => $sticky_post,
            'fields'            => 'ids',
            'tax_query'         => array(
                array(
                    'taxonomy'  => 'location',
                    'field'     => 'slug',
                    'terms'     => array ('location-a', 'location-b' )
                )
            )
        );
        $sticky_query = get_posts( $sticky_args );
    }
    */

    $args1 = array(
        'post_type'         => 'services', 
        'posts_per_page'    => -1,
        'post__not_in'      => $sticky_post,
        'fields'            => 'ids',
        'tax_query'         => array(
            array(
                'taxonomy'  => 'location',
                'field'     => 'slug',
                'terms'     => array ('location-a')
            )
        )
    );
    $query1 = get_posts( $args1 );

    $new_posts_array1 = [];

    if( $query1 ) {
        $counter1 = 0;

        foreach ( $query1 as $post ) {
            $new_posts_array1[$counter1++ * 2] = $post;
        }
        unset( $post );
    }

    $args2 = array(
        'post_type'         => 'services', 
        'posts_per_page'    => -1,
        'post__not_in'      => $sticky_post,
        'fields'            => 'ids',
        'tax_query'         => array(
                array(
                    'taxonomy'  => 'location',
                    'field'     => 'slug',
                    'terms'     => array ('location-b')
                ),
                array(
                    'taxonomy'  => 'location',
                    'field'     => 'slug',
                    'terms'     => array ('location-a'),
                    'operator'  => 'NOT IN',
                )
            )
    );
    $query2 = get_posts( $args2 );

    $new_posts_array2 = [];

    if( $query2 ) {
        $counter2 = 0;

        foreach ( $query2 as $post ) {
            $new_posts_array2[($counter2++ * 2) + 1] = $post;
        }
        unset( $post );
    }


    $new_posts_array = $new_posts_array1 + $new_posts_array2;
    ksort( $new_posts_array );

    // Comment this line out if you uncommented the other block of code
    $q = array_merge( $sticky_post, $new_posts_array );

    /* 
     * If you going to use the first block of commented-out code
     * then you will need to uncomment this piece of code. Just remember
     * to comment the piece of code above out as stated. You cannot have both
     * pieces of code going at once
    */
    /*
    if( isset( $sticky_query ) ) {
        $q = array_merge( $sticky_query, $new_posts_array );
    }else{
        $q = $new_posts_array;
    }   
    */

    set_transient( 'ordered_posts', $q, 7 * DAY_IN_SECONDS );
}

$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
 'post_type' => 'services',
    'paged'                 => $paged,
    'posts_per_page'        => 5,
    'post__in'              => $q,
    'ignore_sticky_posts'   => 1,
    'orderby'               => 'post__in',
);
$query = new WP_Query( $args );

if ( $query->have_posts() ) {
    while ( $query->have_posts() ) {
        $query->the_post();

        get_template_part( 'content', get_post_format() );

    }

    next_posts_link( 'Older Entries', $query->max_num_pages ); //Remember the $max_pages parameter with custom queries
    previous_posts_link( 'Newer Entries' );

    wp_reset_postdata();
}

Then, in your functions.php, add the following code. This will delete the transient if a new post is published or a post is updated or the status change of a post

add_action( 'transition_post_status', function ( $new_status, $old_status, $post )
{
        delete_transient( 'ordered_posts' );

}, 10, 3 );

DEBUGGING THE ABOVE CODE

Once a transient is set, you cannot modify the code inside it. This changes will only show up once the transient expires or when it is deleted. If the code does not work, do the following

  • First, update any post to delete the transient or manually delete it from the db

  • Then remove the two following lines

    if ( false === ( $q = get_transient( 'ordered_posts' ) ) ) {
    
  • and

    set_transient( 'ordered_posts', $q, 7 * DAY_IN_SECONDS );
        }
    
  • This will remove the transient from being set.

  • With the transient being been removed, do a var_dump( $var ) of all the variables and check if you get the desired output. Specially check the queries and what is returned

    • Example: To dump what is returned by query $query1

      var_dump( $query1 );
      

This should give you an idea where the problem is. If you get the output as desired, return the lines that you have deleted for the transient. Just make double sure that the transient was deleted before resetting it, otherwise you will get the previous results

ORIGINAL ANSWER

This question actually gave me a change to play around with ideas. I think this question needs a proper answer. I hope this answers all your issues.

You will be able to do this with only one query. The first thing to do will be to get all your posts from the two terms in one go as you have already done. Just a note, you don’t need to set sorting order as date and DESC are defaults.

The custom taxonomy “location” is shared by several different post types. But I only want to query one post type at a time.

Just add the post_type parameter to your query arguments to target a specific post type

<?php 
$args = array(
    'post_type' => 'services', 
    'posts_per_page' => '-1',
    'tax_query' => array(
            array(
                'taxonomy' => 'location',
                'field' => 'slug',
                'terms' => array ('location-a', 'location-b')
            )
        )
);
$query = new WP_Query( $args );

if( $query->have_posts() ) {

You will now need to split your array of returned posts into two separate arrays, one array for the terms location-aand location-b and the one for sticky posts. The returned posts array is held in $query->posts

What happens if a post has been categorized in both categories? I don’t want it to show twice.

You’ll need to decide before hand how you want to treat this, either as location-a or location-b, otherwise you might get duplication. Lets say, we treat them as location-a

I need to retain the ability to make certain posts sticky.

Sticky posts will be at the top of your returned array, and after all the sorting, we would still like to keep them on top, regardless of term. So, lets exclude any sticky posts from the sorting and just return them later as-is

So, you can do something like this

$counter1 = 0;
$counter2 = 0;

$new_posts_array = [];
$sticky = [];
foreach ( $query->posts as $post ) {
    if( is_sticky() ) {
       $sticky[] = $post;
    }elseif( has_term( 'location-a', 'location' ) && has_term( 'location-b', 'location' ) || has_term( 'location-a', 'location' ) ) {
       $new_posts_array[$counter1++ * 2] = $post;
    }else{
       $new_posts_array[($counter2++ * 2) + 1] = $post;
    }
}

All the issues has been addressed now. We have two arrays now, one holding the array of sticky posts and one the reordered keys with an array of posts

We have now have to sort the array of posts naturally so that the keys are in normal sort order starting from 0. (We can also “reset” all the keys, although this is not necessary)

ksort($new_posts_array);

We can now merge our two arrays so that we again have one array. This array will be the one that will display our posts

$q = array_merge( $sticky, $new_posts_array );

All that is left now is to unset the original post array, resetting the original posts array with our reordered array, rewinding the loop and rerunning it to output our list

unset( $query->posts );
$query->posts = $q;

$query->rewind_posts();

while( $query->have_posts() ) { 
   $query->the_post();

  //Display loop elements

}
wp_reset_postdata();

}

ALL TOGETHER NOW!!!

<?php 
$args = array(
    'post_type' => 'services', 
    'posts_per_page' => '-1',
    'tax_query' => array(
            array(
                'taxonomy' => 'location',
                'field' => 'slug',
                'terms' => array ('location-a', 'location-b')
            )
        )
);
$query = new WP_Query( $args );

if( $query->have_posts() ) {

    $counter1 = 0;
    $counter2 = 0;

    $new_posts_array = [];
    $sticky = [];
    foreach ( $query->posts as $post ) {
        if( is_sticky() ) {
           $sticky[] = $post;
        }elseif( has_term( 'location-a', 'location' ) && has_term( 'location-b', 'location' ) || has_term( 'location-a', 'location' ) ) {
           $new_posts_array[$counter1++ * 2] = $post;
        }else{
           $new_posts_array[($counter2++ * 2) + 1] = $post;
        }
    }

    ksort($new_posts_array);

    $q = array_merge( $sticky, $new_posts_array );

    unset( $query->posts );
    $query->posts = $q;

    $query->rewind_posts();

    while( $query->have_posts() ) { 
       $query->the_post();

      //Display loop elements like
      echo get_the_term_list( $post->ID, 'location');
      the_title(); 

    }
    wp_reset_postdata();

}

QUERY WITH PAGINATION

// set the "paged" parameter (use 'page' if the query is on a static front page)
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
    'post_type'         => 'services', 
    'paged'             => $paged,
    'posts_per_page'    => '5',
    'tax_query'         => array(
            array(
                'taxonomy'  => 'location',
                'field'     => 'slug',
                'terms'     => array ('location-a', 'location-b')
            )
        )
);
$query = new WP_Query( $args );

if( $query->have_posts() ) {

    $counter1 = 0;
    $counter2 = 0;

    $new_posts_array = [];
    $sticky = [];
    foreach ( $query->posts as $post ) {
        if( is_sticky() ) {
           $sticky[] = $post;
        }elseif( has_term( 'location-a', 'location' ) && has_term( 'location-b', 'location' ) || has_term( 'location-a', 'location' ) ) {
           $new_posts_array[$counter1++ * 2] = $post;
        }else{
           $new_posts_array[($counter2++ * 2) + 1] = $post;
        }
    }

    ksort($new_posts_array);

    $q = array_merge( $sticky, $new_posts_array );

    unset( $query->posts );
    $query->posts = $q;

    $query->rewind_posts();

    while( $query->have_posts() ) { 
       $query->the_post();

      //Display loop elements like
      echo get_the_term_list( $post->ID, 'location');
      the_title(); 

    }

    next_posts_link( 'Older Entries', $query->max_num_pages ); //Remember the $max_pages parameter with custom queries
    previous_posts_link( 'Newer Entries' );


    wp_reset_postdata();

}

Leave a Comment