How can I create a category landing page followed by pages of posts?

This is quite an interesting question (which I have upvoted, specially for your approach and research). The big curveball here is the first page of the query:

  • You cannot set the query to return 0 posts on the first page

  • By moving the page content of every page up by one page, you will loose the last page as the query will still only have the same amount of posts so the $max_num_pages property will still remain the same

We will need to somehow “trick” the WP_Query class to return our posts corectly with the offset of one page, and also get the correct number of pages in order not to loose the last page in the query

Lets look at the following idea and try to put everything in code. Before we do, I would however like to raise a few notes here

IMPORTANT NOTES:

  • Everything is untested, so it might be buggy. Make sure to test this locally with debug turned on

  • The code requires at least PHP 5.3, any version below 5.3 will cause a fatal error. Note, if are still using any version below PHP 5.5, you should have upgraded long time ago already

  • Modify and abuse the code as you see fit to suit your exact needs

THE LIGHTBULB IDEA:

WHAT WE WILL NEED

In order to make everything work, we will need the following:

  • The current page number being viewed

  • The posts_per_page option set in reading settings

  • Custom offset

  • Modify the $found_posts property of the query to correct the $max_num_pages property

Pagination is WP_Query comes down a very simple few lines of code

if ( empty($q['nopaging']) && !$this->is_singular ) {
    $page = absint($q['paged']);
    if ( !$page )
        $page = 1;
    // If 'offset' is provided, it takes precedence over 'paged'.
    if ( isset( $q['offset'] ) && is_numeric( $q['offset'] ) ) {
        $q['offset'] = absint( $q['offset'] );
        $pgstrt = $q['offset'] . ', ';
    } else {
        $pgstrt = absint( ( $page - 1 ) * $q['posts_per_page'] ) . ', ';
    }
    $limits="LIMIT " . $pgstrt . $q['posts_per_page'];
}

What basically happens, once an offset is explicitely set, the paged parameter is ignored. The first parameter of the SQL LIMIT clause is recalculated from the offset and will be the number of posts to be skipped in the generated SQL query.

From your question, apparently when setting offset to 0, the offset query fails, which is strange, as the following check should return true

if ( isset( $q['offset'] ) && is_numeric( $q['offset'] ) )

0is a valid number and should return true. If this does not on your installation, you should debug the issue

To come back to the issue at hand, we will use the same kind of logic to calculate and set our offset to get post 1 on page 2 and from there paginate the query. For the first page, we will not alter anything, so the posts that is suppose to be on page 1 will still be on page as normal, we would just need to “hide” them later on so that we do not display them on page 1

add_action( 'pre_get_posts', function ( $q )
{
    if (    !is_admin() // Only target the front end, VERY VERY IMPORTANT
         && $q->is_main_query() // Only target the main query, VERY VERY IMPORTANT
         && $q->is_cateory( 'news' ) // Only target the news category
    ) {
        $current_page = $q->get( 'paged' ); // Get the current page number
        // We will only need to run this from page 2 onwards
        if ( $current_page != 0 ) { // You can also use if ( is_paged() ) {
            // Get the amount of posts per page
            $posts_per_page = get_option( 'posts_per_page' );
            // Recalculate our offset
            $offset = ( ( $current_page - 1) * $posts_per_page ) - $posts_per_page; // This should work on page 2 where it returns 0

            // Set our offset
            $q->set( 'offset', $offset );
        }
    }
});

You should be seeing the same posts from page 1 on page 2. As I previously stated, if this does not happen, either is_numeric( 0 ) is returning false (which it should not) or you have another pre_get_posts action that is also trying to set an offset or you are using making use of the posts_* clause filters (more specifically, the post_limits filter). This would be something you would need to debug by yourself.

The next issue is to correct pagination, beacause as I previously stated, you will be a page short. For this, we will need to add the value of get_option( 'posts_per_page' ) to the amount of posts found in the query as we are offsetting the query by that amount. by doing this, we are effectively adding 1 to the $max_num_pages property.

add_action( 'found_posts', function ( $found_posts, $q )
{
    if (    !is_admin() // Only target the front end, VERY VERY IMPORTANT
         && $q->is_main_query() // Only target the main query, VERY VERY IMPORTANT
         && $q->is_cateory( 'news' ) // Only target the news category
    ) {
        $found_posts = $found_posts + get_option( 'posts_per_page');
    }
}, 10, 2 );

This should sort everything, except the first page.

ALL TOGETHER NOW (and specially for @ialocin – Yellow Submarine)

This should all go into functions.php

add_action( 'pre_get_posts', function ( $q )
{
    if (    !is_admin() // Only target the front end, VERY VERY IMPORTANT
         && $q->is_main_query() // Only target the main query, VERY VERY IMPORTANT
         && $q->is_cateory( 'news' ) // Only target the news category
    ) {
        $current_page = $q->get( 'paged' ); // Get the current page number
        // We will only need to run this from page 2 onwards
        if ( $current_page != 0 ) { // You can also use if ( is_paged() ) {
            // Get the amount of posts per page
            $posts_per_page = get_option( 'posts_per_page' );
            // Recalculate our offset
            $offset = ( ( $current_page - 1) * $posts_per_page ) - $posts_per_page; // This should work on page 2 where it returns 0

            // Set our offset
            $q->set( 'offset', $offset );
        }
    }
});

add_filter( 'found_posts', function ( $found_posts, $q )
{
    if (    !is_admin() // Only target the front end, VERY VERY IMPORTANT
         && $q->is_main_query() // Only target the main query, VERY VERY IMPORTANT
         && $q->is_cateory( 'news' ) // Only target the news category
    ) {
        $found_posts = $found_posts + get_option( 'posts_per_page');
    }
    return $found_posts;
}, 10, 2 );

FIRST PAGE OPTIONS

There are a few options here:

OPTION 1

I would most probably go for this option. What you would want to do here is to create a category-news.php (if you haven’t yet done this). This will be the template that will be used whenever the news category is viewed. This template will be very simply

Example

<?php
get_header()

if ( !is_paged() ) { // This is the first page
    get_template_part( 'news', 'special' );
} else { // This is not the first page
    get_template_part( 'news', 'loop' );
}

get_sidebar();
get_footer();

As you can see, I have included two template parts, news-special.php and news-loop.php. Now, the basics of the two custom templates are:

  • news-special.php -> This template part will be whatever you would want to display on the first page. Add all your custom static info here. Be very careful not to call the loop in this template as this would display the posts of the first page.

  • news-loop.php -> This is the template where we will invoke the loop. This section will look something like this:

    global $wp_query;
    while ( have_posts() ) {
    the_post();
    
        // Your template tags and markup
    
    }
    

OPTION 2

Create a separate template with your static content and simply use the category_template filter to use this template when we view the first page of the news category. Also, be sure not to invoke the default loop in this template. Also, make sure that your naming convention here does not collide with template names within the template hierarchy

I hope this is useful. Feel free toleave comments with concerns

EDIT

Thanks to the OP, there is a definite bug in the WP_Query class, check the trac ticket #34060. The code I have posted is from WordPress v4.4, and the bug is fixed in this version.

I went back to the source code of v4.3, where the bug is, and I can confirm that 0 is ignored when set as value to offset as the code simply checks whether the offset parameter is empty. 0 is taken as empty in PHP. I am not sure if this behavior (bug) is only found in v4.3 only or in all previous versions (according to the ticket, this bug is in v4.3), but there is a patch for this bug which you can check out in the trac ticket. Like I said, this bug has definitely being fixed in v4.4

Leave a Comment