Different ‘posts_per_page’ setting for first, and rest of the paginated pages?

How do you set the posts_per_page query setting so that a different number of posts are displayed on the first of the paginated pages (of home, archive, search, etc.) and the rest of them?

For example, say I’d like to display 10 posts on the first of the paginated pages of category archives, and 15 on the rest of them. How do I do it?

Read More

This works:

function itsme_category_offset( $query ) {
  if( $query->is_category() && $query->is_main_query() ) {

    if( !$query->is_paged() ) {

      $query->set( 'posts_per_page', 10 );

    } else {

      $query->set( 'offset', 10 );
      $query->set( 'posts_per_page', 15 );

    }
  }
}
add_action( 'pre_get_posts', 'itsme_category_offset' );

But…

According to the Codex entry for WP_Query() pagination parameters:

offset (int) – number of post to displace or pass over. Warning: Setting the offset parameter overrides/ignores the paged parameter and breaks pagination

And according to the linked Codex entry for a workaround:

Specifying hard-coded offsets in queries can and will break pagination since offset is used by WordPress internally to calculate and handle pagination.

The indicated workaround uses a function that hooks into found_posts filter and establishes the offset. It supposes that I should do something like this:

function itsme_category_offset( $query ) {
  if( $query->is_category() && $query->is_main_query() ) {
    $paged = get_query_var( 'paged' );

    if( 0 == $paged ) {

      $query->set( 'posts_per_page', 10 );

    } else {

      $offset = 10 + ( ($paged - 2) * 15 );
      $query->set( 'offset', $offset );
      $query->set( 'posts_per_page', 15 );

    }
  }
}
add_action( 'pre_get_posts', 'itsme_category_offset' );

function itsme_adjust_category_offset_pagination( $found_posts, $query ) {
  $paged = get_query_var( 'paged' );
  if( $query->is_category() && $query->is_main_query() ) {
    if( 0 == $paged ) {

      $offset = 0;

    } else {

      $offset = 10 + ( ($paged - 2) * 15 );

    }

    return $found_posts - $offset;
  }
}
add_filter( 'found_posts', 'itsme_adjust_category_offset_pagination' );

Since my simpler function works already, is the Codex warning still correct? (i.e. should I do it as shown in the second code block?) Was this offset/pagination issue fixed in a recent version of WordPress? And if so: how?

Related posts

2 comments

  1. Case #1: Simple Offset

    You want to ‘offset’ posts of a category archive by ‘n’, i.e. you simply don’t want to show the first/latest ‘n’ posts in an archive.

    That is, (considering the posts_per_page setting in WP Dashboard > Settings > Reading is set to 10) you want posts 11 to 20 to be shown on the first page (e.g. example.com/category/tech/), 21 to 30 on the second (e.g. example.com/category/tech/page/2/), 31 to 40 on the third, and so on.

    Anyway, here’s how you’d do that:

    /*
     * Offset posts by 10 on 'Techonology (tech)' category archive
     */
    function itsme_category_offset( $query ) {
        $offset = 10;
        $ppp = get_option( 'posts_per_page' );
        $paged = $query->query_vars[ 'paged' ];
    
        if( $query->is_category( 'tech' ) && $query->is_main_query() ) {
            if( !is_paged() ) {
    
                $query->set( 'offset', $offset );
    
            } else {
    
                $paged_offset = $offset + ( ($paged - 1) * $ppp );
                $query->set( 'offset', $paged_offset );
    
            }
        }
    }
    add_action( 'pre_get_posts', 'itsme_category_offset' );
    

    Then, there’s one more thing. Before creating pagination, WordPress looks at the total number of posts that the WP_Query class reports finding when it runs a query.

    So, for the pagination to work properly, you need to remove the offset from the total number of posts that the query finds (because the first 10 posts are not being shown). And this is how you’d do it:

    function itsme_adjust_category_offset_pagination( $found_posts, $query ) {
        $offset = 10;
    
        if( $query->is_category( 'tech' ) && $query->is_main_query() ) {
            return( $found_posts - $offset );
        }
    }
    add_filter( 'found_posts', 'itsme_adjust_category_offset_pagination', 10, 2 );
    

    That’s it!

    PS: (A more technical explanation can be found here.)


    Case #2: Conditional Offset

    As it is in my case, you want to show ‘m’ number of posts on the first page of an archive, and ‘n’ posts on the others.

    That is, (considering you want to show only 5 posts on the first page of the archive, and have the rest of the pages adhere to the posts_per_page setting in WP Dashboard > Settings > Reading, which is set to 10) you want posts 1 to 5 to be shown on the first page (e.g. example.com/category/tech/), 6 to 15 on the second (e.g. example.com/category/tech/page/2/), 16 to 25 on the third, and so on.

    Here’s how you’d do that:

    /*
     * Show a different no. of posts on the first page, and the rest
     * of the pages of 'Techonology (tech)' category archive
     */
    function itsme_category_offset( $query ) {
        $ppp = get_option( 'posts_per_page' );
        $first_page_ppp = 5;
        $paged = $query->query_vars[ 'paged' ];
    
        if( $query->is_category( 'tech' ) && $query->is_main_query() ) {
            if( !is_paged() ) {
    
                $query->set( 'posts_per_page', $first_page_ppp );
    
            } else {
    
                // Not going to explain the simple math involved here
                $paged_offset = $first_page_ppp + ( ($paged - 2) * $ppp );
                $query->set( 'offset', $paged_offset );
    
                /*
                 * As we are not adding a custom `$query->set( 'posts_per_page', ... );`,
                 * the default `posts_per_page` setting from WP Dashboard > Settings > Reading
                 * will be applied here.
                 */
    
            }
        }
    }
    add_action( 'pre_get_posts', 'itsme_category_offset' );
    

    This case is complex. So, first lets look at how the found_posts function should look like:

    function itsme_adjust_category_offset_pagination( $found_posts, $query ) {
        $ppp = get_option( 'posts_per_page' );
        $first_page_ppp = 5;
    
        if( $query->is_category( 'tech' ) && $query->is_main_query() ) {
            if( !is_paged() ) {
    
                return( $found_posts );
    
            } else {
    
                return( $found_posts - ($first_page_ppp - $ppp) );
    
            }
        }
        return $found_posts;
    }
    add_filter( 'found_posts', 'itsme_adjust_category_offset_pagination', 10, 2 );
    

    Unlike in Case #1, here we are not eliminating any posts from the total; we need to show all of them; except we want to show a set no. of posts on the first page of the category archive, and a different no. of posts on the rest of the paginated pages.

    The problem is, in order to calculate the no. of pages for generating pagination, $query (WP_Query) looks at the posts_per_page setting for the current page. Since we set it as ’10’ for all other pages except the first, when on any other page except the first, $query assumes it’s the same for all pages (including the previous/next ones i.e. including the first page) and starts calculating pagination based on that.

    So for example, on 2nd page (if we take total no. of posts as ’20’), according to:

    • $query pagination should be: 10 + 10
    • But correct pagination would be: 5 + 10 + 5 (because in the initial pre_get_posts function we set pagination for the first page as ‘5’)

    So, when on 2nd page (or any page except the first), as per our example, we need to make $query believe that the total no. of posts is 25 so that it generates a correct pagination like this…

    5 posts on 1st page + 10 posts on 2nd + 5 posts on 3rd

    …thinking it’s actually doing it like this:

    10 posts on 1st page + 10 posts on 2nd + 5 posts on 3rd

    And since found_posts filter has no effect whatsoever on how many posts are actually displayed on a given page, our posts_per_page settings from the first function will prevail, with no pagination issues.

    That should clear things up. (And do let me know if anything’s off.)


    So, to answer the question directly, yes, you pretty much always need to follow up with a function that hooks into found_posts filter hook, to adjust the $query according to your custom rules, and make sure the pagination doesn’t get messed up.

  2. I don’t have enough stackexchange points to comment on the above, but I do have a correction.

    Case #1: Simple Offset worked for my site very well. It just needs a fix so that it returns the default offset when the special condition isn’t met:

    function itsme_adjust_category_offset_pagination( $found_posts, $query ) {
        $offset = 10;
    
        if( $query->is_category( 'tech' ) && $query->is_main_query() ) {
            return( $found_posts - $offset );
        } else {
            return $found_posts;
        }
    }
    add_filter( 'found_posts', 'itsme_adjust_category_offset_pagination', 10, 2 );
    

Comments are closed.