9

I'm trying to create a category template in my theme that will display static content if the user is on the first page of the archive, and then display the posts in the category on the following pages. I'm basically trying to replicate the category behavior on Wired.com where http://www.wired.com/category/design is a landing page while /category/design/page/1 shows a reverse chronological listing of posts like you would expect on a category archive.

The key here is that I'm not showing any posts from the category archive on the first page, so I need the next page to start with the first post. I first tried doing this using offset and manual pagination by setting the offset of the query to 0 if the 'paged' query var was 2. But offset is ignored when it's set to zero, so the best I can do is set the offset to 1 and start with the second post in the category.

This is what I added to functions.php:

add_action('pre_get_posts', 'category_query_offset', 1 );
function category_query_offset(&$query) {
  // Before anything else, make sure this is the right query
  if (!$query->is_category('news')) {
    return;
  }

  // Next, determine how many posts per page
  $ppp = get_option('posts_per_page');

  //Next, detect and handle pagination
  if ($query->is_paged) {
    // Manually determine page query offset
    $page_offset = (($query->query_vars['paged']-1) * $ppp) - $ppp;
    // Apply adjust page offset
    $query->set('offset', $page_offset );
  }
}

What would be better, and what it appears Wired does, is to use the default pagination so the posts start on page 1, but insert a page 0 for the static content. Hypothetically, this seems like it would be possible by displaying the static content if 'paged' is 0, and displaying the first page of posts if 'paged' is 1. The problem is that 'paged' is never 1 because Wordpress sets 'paged' to zero when the user requests the first page. That means 'paged' is 0 for both /category/news/page/1 and /category/news.

Is there a way to check if the user requested /category/news/page/1 as opposed to /category/news? Failing that, is there a way to display all the posts in the category starting on page 2?

7

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

  • 2
    Thanks for your detailed answer. This is actually pretty much what I implemented in my first attempt. The problem was that setting 'offset' to 0 wasn't working. It turns out it was actually a bug in the WordPress Core core.trac.wordpress.org/ticket/34060. Patching it made everything work. The only thing your code is missing is return $found_posts; after the if statement in the found_posts action. Thanks! – willcwelch Oct 21 '15 at 13:22
  • Thank you for your positive feedback. I went back to version 4.3 source code in query.php, and yes, 0 will be ignored because the check here is for empty, and 0 is taken as empty. The code in my answer is the update code that is shipped with v4.4, so I can confirm that the bug is fixed in 4.4. I will update my answer accordingly. On the found_posts issue, good catch, it is actually a filter and should be returned. Enjoy – Pieter Goosen Oct 21 '15 at 13:58
  • haha, thanks :) and for all that are wondering why... one, two, three, four, can I have a little more ...the actual link to the song all together now – Nicolai Oct 21 '15 at 14:25
  • @ialocin hahaha, I will remember this one ;-) – Pieter Goosen Oct 21 '15 at 14:43
  • 1
    it is pretty catchy :) but no wonder, because it is pretty much written like a children song – Nicolai Oct 21 '15 at 15:00
0

Update:

add_action('pre_get_posts', 'category_query_offset', 1 );
function category_query_offset($query) {
 // Before anything else, make sure this is the right query
 if (!$query->is_category('news')) {
    return;
 }

 // Next, determine paged
 $ppp = get_option('paged');
}
// Next, handle paged
if($ppp > 1){
    $query->set('paged', $ppp-1);
}

Try some thing like

$paged = ( get_query_var('paged') ) ? get_query_var('paged')-1 : 1;
$args = array (
    'paged' => $paged,
);
  • I've corrected my explanation for how the 'paged' variable works. It looks like what you're trying to do is lower each page number by 1, so a request for page 2 gives you page 1. The problem is that when page 1 is queried, ‘paged’ is set to 0. Also, get_query_var() should be used instead of get_option for 'paged'. With that fix, when this runs, each time the query is changed it loops again until 'paged' is 0 for any page request. – willcwelch Oct 20 '15 at 21:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.