Question

I'm using pre_get_posts to adjust the number of posts displayed on my homepage.

function lifelounge_query_adjust( $query ) {
    if ( is_home() ) {
        set_query_var( 'posts_per_page', 12 );
        return;
    }
}
add_filter( 'pre_get_posts', 'lifelounge_query_adjust' );

But I am running into a problem with sticky posts. Basically, if I have any sticky posts, the query will display more than the 12 posts I have specified, because it will display 12 plus any sticky posts. I could, of course, ignore sticky posts:

function lifelounge_query_adjust( $query ) {
    if ( is_home() ) {
        set_query_var( 'posts_per_page', 1 );
        set_query_var( 'ignore_sticky_posts', 1 );
        return;
    }
}
add_filter( 'pre_get_posts', 'lifelounge_query_adjust' );

But I don't think this is ideal. I think the sticky posts should be included in the limit of 12 posts, and not added to the limit. That is what makes the most sense to me. Is there a way to achieve that? Have I made a face-palm-worthy error?

Pretty much a duplicate of: Sticky Posts & Posts Per Page but that was weirdly closed as too localized. I disagree, obviously because I'm looking for an answer, but also because it is a question of why WordPress doesn't seem to respect the posts_per_page limit if you are using sticky posts. If you want 12 posts per page you should get 12, not 13, which is what you would get if you had a single sticky post.

Was it helpful?

Solution

Here is an approach to account for sticky posts by getting the number of sticky posts (if any) and include that in the calculation posts_per_page parameter:

add_action('pre_get_posts', 'ad_custom_query');
function ad_custom_query($query) {

    if ($query->is_main_query() && is_home()) {

        // set the number of posts per page
        $posts_per_page = 12;
        // get sticky posts array
        $sticky_posts = get_option( 'sticky_posts' );

        // if we have any sticky posts and we are at the first page
        if (is_array($sticky_posts) && !$query->is_paged()) {

            // counnt the number of sticky posts
            $sticky_count = count($sticky_posts);

            // and if the number of sticky posts is less than
            // the number we want to set:
            if ($sticky_count < $posts_per_page) {
                $query->set('posts_per_page', $posts_per_page - $sticky_count);

            // if the number of sticky posts is greater than or equal
            // the number of pages we want to set:
            } else {
                $query->set('posts_per_page', 1);
            }

        // fallback in case we have no sticky posts
        // and we are not on the first page
        } else {
            $query->set('posts_per_page', $posts_per_page);
        }
    }
}

Edit

In the case where the number of posts per page we wish to set is less than or equal to the number of sticky posts, I have set the posts_per_page to one and that will result in 13 or more posts $sticky_count + 1 (in this case) only on the first page (subsequent pages will have 12 posts). Maybe that is OK since this case is rare and +1 post on the first page may not be that significant.

This is because Wordpress will display all sticky posts first and on one page (the first page) even if their count is greater than the posts_per_page parameter, so we set the posts_per_page in this case to the minimal amount possible which is 1, because 0 and negative values will disable the posts_per_page parameter and that will make Wordpress to display all posts on the first page.

OTHER TIPS

There an issue if the sticky posts are in the first page.

The solution is to decrement the sticky post count for the sticky posts that are part of the first page.

function fix_posts_per_page_with_sticky_posts( $query ) {

    if ( $query->is_main_query() ) {

        // set the number of posts per page
        $posts_per_page = 12;

        // get sticky posts array
        $sticky_posts = get_option( 'sticky_posts' );

        // get queried post ids array
        $ids = array();
        $args = array(
            'post_type' => 'post',
            'post_per_page' => $posts_per_page,
            'paged' => 1
        );

        $posts = get_posts( $args );

        foreach ( $posts as $post ) {
            $ids[] = $post->ID;
        }

        // if we have any sticky posts and we are at the first page
        if ( is_array( $sticky_posts ) && ! $query->is_paged() ) {

            // count the number of sticky posts
            $sticky_count = count( $sticky_posts );

            foreach ( $sticky_posts as $sticky_post ) {
                if ( in_array( $sticky_post, $ids ) ) {
                    // decrement sticky posts count if the sticky post in on the page
                    $sticky_count--;
                }
            }

            // and if the number of sticky posts is less than
            // the number we want to set:
            if ( $sticky_count < $posts_per_page ) {
                $query->set( 'posts_per_page', $posts_per_page - $sticky_count );

            // if the number of sticky posts is greater than or equal
            // the number of pages we want to set:
            } else {
                $query->set( 'posts_per_page', 1 );
            }

        // fallback in case we have no sticky posts
        // and we are not on the first page
        } else {
            $query->set( 'posts_per_page', $posts_per_page );
        }
    }
}
add_action( 'pre_get_posts', 'fix_posts_per_page_with_sticky_posts'  );

I hope it will help

I cleaned up both of the above answers into one so that it does not load needless WP_Query, fixes if the sticky on first page, reduce the time to process the information with cleaner faster code.

function modify_main_query( $query ) {
   if ( ( $query->is_home() || is_front_page() ) && $query->is_main_query() ) {
         // set the number of posts per page
        $posts_per_page = 12;
        // get sticky posts array
        $sticky_posts = get_option( 'sticky_posts' );
        // if we have any sticky posts and we are at the first page
        if (is_array($sticky_posts) && !$query->is_paged()) {
            // make a second query to make sure the sticky posts will still work 
            // correctly when on the first page
            // Only reply with the ID's as that is all that is needed
            $args = [
                'post_type' => 'post',
                'post_per_page' => $posts_per_page,
                'paged' => 1,
                'fields' => 'ids'
            ];
            // Array flip to reduce the time taken by 
            // using isset and not in_array
            $posts = array_flip( get_posts( $args ) );

            // count the number of sticky posts
            $sticky_count = count($sticky_posts);

            // loop the posts from the 2nd query to see if the ID's of the sticky posts
            // sit inside it.
            foreach ( $sticky_posts as $sticky_post ) {
                if(isset($posts[$sticky_post])){
                    $sticky_count--;
                }
            }
            // and if the number of sticky posts is less than
            // the number we want to set:
            if ($sticky_count < $posts_per_page) {
               $query->set('posts_per_page', $posts_per_page - $sticky_count);
            } else {
                // if the number of sticky posts is greater than or equal
                // the number of pages we want to set:
                $query->set('posts_per_page', 1);
            }
        // fallback in case we have no sticky posts
        // and we are not on the first page
        } else {
            $query->set('posts_per_page', $posts_per_page);
        }
    } 
}

add_action( "pre_get_posts", 'modify_main_query' );

The answers written here get the job done, but WordPress provides a simpler way. As part of the WP_Query class, WordPress includes the current_post property. This property increases by one every time a new post is processed through the loop when you call $my_query->the_post(). The property is set initially to -1.

So, in order to ensure you always display the right number of posts, including sticky posts, you can set it up like this. In the example below, two blog posts will always be shown, no matter how many sticky posts there are.

<?php
$posts_per_page = 2;
$args           = array(
    'posts_per_page' => $posts_per_page,
);

$my_query = new WP_Query( $args );

if ( $my_query->have_posts() ) :

    while ( $my_query->have_posts() && $my_query->current_post + 1 < $posts_per_page ) :

        $my_query->the_post();

        // Run code to output your posts...

    endwhile;

endif;

This approach has only been tested on a custom query, but should work with the default WordPress loop as well.

Licensed under: CC-BY-SA with attribution
Not affiliated with wordpress.stackexchange
scroll top