25

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.

5 Answers 5

14

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.

4
  • Great!! I think you need to change $sticky_count + (12 - $sticky_count) to 12- $sticky_count though. For example if I have 1 sticky, then your math still works out to 12, and then WP adds the sticky post to make 13. Oh, and if if ($sticky_count > $posts_per_page) and we set to 12, won't that mean we'll show 24+? Dec 19, 2012 at 19:44
  • @helgatheviking: you are right. I always make such silly mistakes, calculations was never that interesting to me. And yes, that would result in 24 posts. I have updated the code to account for that, and I added a check for a page number. This works fine, but now there will be one case where $posts_per_page will be equal to $sticky_count, and here I set the posts_per_page parameter to be 1, and that I think will be OK since this case maybe rare and that will be only on the first page ($sticky_count + 1).
    – Ahmad M
    Dec 19, 2012 at 21:25
  • Thanks for the edit! I think this is the best solution we can get using sticky posts. I think that I might eventually sort by a simple meta key for whether a post is featured or not. That behaves more normally for my understanding. Dec 20, 2012 at 0:52
  • this fails as a solution if the sticky posts are part of the originally wanted posts_per_page. The total number of posts will reduce but the sticky posts will not push that number back up as they are part of the normal date ordered set. Aug 1, 2018 at 7:45
3

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

5
  • 1
    Are you sure there isn't an easier and quicker solution to that? Hint: You know the amount of sticky posts and the posts per page...
    – kaiser
    Nov 28, 2014 at 16:53
  • I didn't found better so far.. This is more a fix for something that should be in the WP core in my opinion
    – csag
    Dec 1, 2014 at 19:02
  • If it would be in core, other scenarios wouldn't work.
    – kaiser
    Dec 1, 2014 at 20:25
  • This is a known bug and being tracked at core.trac.wordpress.org/ticket/27282
    – Will
    Sep 25, 2017 at 21:06
  • @kaiser Ahmad M's solution doesn't account for sticky posts that would have appeared on the first page regardless of their sticky state. It can result in too few posts appearing on the first page (WordPress v4.9.7). This answer is better because it accounts for that. Jul 17, 2018 at 15:14
3

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.

5
  • 1
    Here's an example of how to do this in the default loop. <?php while ( have_posts() && $wp_query->current_post + 1 < get_option( 'posts_per_page' ) ) : the_post(); ?>
    – MarcGuay
    Aug 18, 2021 at 16:06
  • 1
    Thanks @MarcGuay! The only suggestion I'd make is to store get_option( 'posts_per_page' ) in a variable before the while loop. That way you're not hitting the database over and over again during the loop.
    – jg314
    Aug 18, 2021 at 20:01
  • I believe WP caches but you're right that it's probably more efficient to store it in a var before the loop, thanks!
    – MarcGuay
    Aug 18, 2021 at 20:22
  • This is the best way to go. Telling the client to not make new posts sticky was unreliable, and this is a single line fix to limit the count of rendered posts.
    – roshnet
    Jan 6, 2023 at 7:41
  • This is the better solution then trying to fiddle with pre_get_posts. When you fiddle with that action, you also, per WP docs, have to tinker with found_posts filter or you risk screwing up pagination. You end up "removing" posts from the count, which affect the max_num_pages calculation, giving you more "pages" that land you in a 404 situation. Oct 28, 2023 at 0:08
1

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' );
0

I was not really happy with any of answers, so I came up with my solution:

$sticky_posts  = get_option( 'sticky_posts' );
$regular_posts = get_posts( [ 'posts_per_page' => 12, 'fields' => 'ids' ] );
$posts_in      = array_merge( $regular_posts, $sticky_posts );

$query = new WP_Query(
    [
        'posts_per_page'      => '12',
        'ignore_sticky_posts' => true,
        'post__in'            => $posts_in,
        'orderby'             => 'date',
    ]
);

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.