WP_Query: Why is sticky post not first item in loop?

If you look ate the source code where stickies are included, we find the following condition before WP_Query carries on to include sticky posts

if (    $this->is_home 
     && $page <= 1 
     && is_array($sticky_posts) 
     && !empty($sticky_posts)  
     && !$q['ignore_sticky_posts'] 
) {

The big game player is the is_home condition. Conditionals inside WP_Query is set according to the arguments passed to it, and not the page where the query happens. Because you pass cat as an argument, is_home will be set to false, and is_category will be set to true. Because is_home is set to false, the above condition fails and stickies are not included.

POSSIBLE WORK-AROUND – Untested

What we can do, is to manually set is_home to true via pre_get_posts. All we need to do is to pass 'wpse_is_home' => true, to your custom WP_Query instance

Then we run our pre_get_posts action as follow

add_action( 'pre_get_posts', function ( $q )
{
    if ( true === $q->get( 'wpse_is_home' ) ) 
        $q->is_home = true;
});

EDIT

We would probably only want to show stickies which has relevance to the queried category. In that case, we should remove stickies that does not belong to the specific queried category

We can try the following, still inside our pre_get_posts action

add_action( 'pre_get_posts', function ( $q )
{
    remove_action( current_action(), __FUNCTION__ );

    if ( true !== $q->get( 'wpse_is_home' ) )
        return;

    // Make sure get_sub_field exists, if not, bail
    if ( !function_exists( 'get_sub_field' ) )
        return;

    // Lets query everything to keep code clean
    $meta_cat = get_sub_field( 'category' ); // Copied from your code
    $meta_cat = filter_var( $meta_cat, FILTER_VALIDATE_INT );
    // Make sure we have a value, if not, bail
    if ( !$meta_cat )
        return;

    $q->set( 'cat', $meta_cat );

    // Set pagination if recent_ppp exists
    $ppp = get_sub_field( 'recent_ppp');
    $ppp = filter_var( $ppp, FILTER_VALIDATE_INT );
    if ( $ppp )
        $q->set( 'posts_per_page', $ppp );      

    // Set is_home to true 
    $q->is_home = true;

    // Get all stickies
    $stickies = get_option( 'sticky_posts' );
    // Make sure we have stickies, if not, bail
    if ( !$stickies )
        return;

    // Query the stickies according to category
    $args = [
        'post__in'            => $stickies,
        'posts_per_page'      => -1,
        'ignore_sticky_posts' => 1, // Ignore stickies
        'cat'                 => $meta_cat,
        'orderby'             => 'post__in',
        'order'               => 'ASC',
        'fields'              => 'ids' // Get only post ID's
    ];
    $valid_sticky_ids = get_posts( $args );

    // Make sure we have valid ids
    if ( !$valid_sticky_ids ) {
        $q->set( 'post__not_in', $stickies );
        return;
    }

    // Remove these ids from the sticky posts array
    $invalid_ids = array_diff( $stickies, $valid_sticky_ids );

    // Check if we still have ids left in $invalid_ids
    if ( !$invalid_ids )
        return;

    // Lets remove these invalid ids from our query
    $q->set( 'post__not_in', $invalid_ids );
});

Before we run our custom query, we would need to make sure that get_sub_field exists, and most importantly, that get_sub_field( 'category' ) is set and valid. This will avoid catastrophic failure which will lead to all posts being returned, regardless.

Your WP_Query can look something like this as we only need to pass 'wpse_is_home' => true:

if (    function_exists( 'get_sub_field' )
     && filter_var( get_sub_field( 'category' ), FILTER_VALIDATE_INT )
) {
    $args = [
        'wpse_is_home' => true
    ];
    $posts_array = new WP_Query( $args ); // DO NOT USE $posts

    // Run your loop as normal
}

LAST EDIT

Code is tested and working as expected

Leave a Comment