Advanced pagination control for WordPress

So, you’re developing your own WordPress theme, or you are heavily customizing the one you are already using, and you need to have different number of posts per page depending on the page you are in.
How do you do that?
WordPress only allows you to set a predetermined limit of posts per page, and applies that limit to all listing pages by default. It’s simple, elegant, usable, some more random keywords here, but far from ideal in real world themes and real world users (kinda O.C.D. like me), where we might (definitely) want the height of your content to roughly (exactly) match the height of our sidebar in every single page.
In order to intercept the default WordPress options and apply our own rules, we are going to hook onto the pre_get_posts action. This action fires before the query is executed (so there is no performance penalty) and passes as a parameter a reference to the WP_Query object, so every change to the parameter changes the immediately the query itself.
So, open your theme’s functions.php file, and let’s write our hook which will be the placeholder for the rest of our code.
Notice that we have named the wp_query object reference as $wp. You can name it anything you want.
Let’s make it very clear. We don’t want to mess with the admin side of WordPress. So, the very first thing we need to do inside our new function, is to detect whether it’s an admin page and return immediately.
This did nothing. Now what?
Relax, and get a sip of coffee.
Let’s assume you have thought of your blog’s structure, navigation and SEO techniques, and you have determined that you need your blog to display the following:
- Home page: 5 posts per page
- Category and Tag listings: 10 posts per page
- Other archive pages (taxonomies, custom post types, date-based archives): 25 posts per page
Additionally, you have need to keep the default WordPress setting so that it applies in all other cases, and you want to be able to change it whenever you want from within the WordPress admin panel.
Finally, you have a category Exotic Flowers, with the slug exotic-flowers and you don’t want any pagination at all, i.e. you want to display all posts in a single page.
Now, let’s start thinking. Shall we?
The general idea is to create a variable ($num) that will hold a default number of posts, and replace that number depending on the case. Where we need to be careful is that we need to replace this value going from the more generic cases, to the more specific ones. Let’s start.
Let’s get the user setting from the WordPress panel.
Here, we’ve said that we want $num to be whatever the user has set, and just in case there is no such record in the database, set 15 posts as a default.
Let’s continue by setting a number for our home page.
In the same spirit, we continue adding cases, depending on what our needs are. Keep in mind the what we mentioned before about working from the more generic cases to the more specific ones. The following code is what handles the cases for archives, categories, tags, and the Exotic Flowers in the right order.
Don’t use the following code
The following snippet is meant to show an example of wrong ordering of the IF statements, and why they are wrong. DO NOT copy this code as it contains logical errors.
Why is this bit of code wrong? Well, let’s say you are browsing through the Exotic Flowers category. According to our specification on the top of the article, there should be no pagination at all, and all posts should be listed in a single page. Now, the first condition, if( is_category() or is_tag() ) is TRUE because we are in a category listing page, and the $num variable gets properly assigned the number 10. No problem here.
The second condition, if( is_category(‘exotic-flowers’) ), is also TRUE because we are browsing through that specific category. So the $num variable gets properly overwritten with the number -1. No problem here either.
The third condition, if( is_archive() ) is TRUE, because the category listing pages ARE archive pages, so the $num variable gets overwritten with the number 25. But this is wrong! Yes, this is a logical bug, because the more generic case comes later than the more specific cases. In fact, the specific example will result in all categories displaying 25 posts per page instead of 10.
Just be careful to avoid this pitfall.
Get on with it
Anyway, forgetting for a moment the logical traps that we can fall in, this is great! We can add/remove IF’s as much as we want, and WordPress conditional tags are here to assist. At this point your code should look like something like this:
In order to change the query object to take into account the number of posts we determined we need, we need to modify its posts_per_page query variable. Query variables are in an array named query_vars within the query object. Accessing it and assigning our number is as simple as:
Don’t forget that $wp is a reference to the query object, so we can access its member variables (and functions) directly. Just by adding the line above to the end of our function, is enough to change the whole WordPress paging behavior. But what about all the side-effects?
Protection from side-effects
Just by adding the $wp->query_vars[‘posts_per_page’] = $num; line to our function, affects each and every one query that is going to execute. This includes the main query (primary loop), any other custom/secondary queries and loops, as well as most widgets that use the WP_Query object somehow (e.g. recent posts, popular posts, etc).
We don’t want that, do we?
First line of defense
In order to restrict the effects of our function, we are going to leave custom queries alone. One way to determine which queries are custom and which are not is to examine the WP_Query object (well, the $wp variable) before we modify it. What we will see is that the $wp->query_vars[‘posts_per_page’] is sometimes set to a number, and sometimes is not set at all.
What does this mean? Well, when it’s set and assigned a number, it means that a custom query, loop, widget, slider, etc. requested a specific number of posts. This is where we should stop messing with the query and stop the execution of our function.
When it is not set at all, a number of posts was not passed as a parameter to query_posts() or to a new WP_Query() object. This allows WordPress to behave as it wants, i.e. use the number of posts the user has set from the admin panel. This is where we want to intervene and assign our own number.
This is how to do just that. Instead of:
wrap it within an IF statement that executes only when the query variable is not set:
If posts_per_page was set, execution will skip this step and the query will be left untouched.
Second line of defense
Although at this point our custom pagination hook will work, come rain or come shine, you might want to restrict the effectiveness of our function even further, by applying it only on the main query of a page. This means only the main loop of each page template, that may or may not be changed by the theme using the query_posts() function.
In order to do that, we can use the incredibly useful conditional function that WordPress provides, is_main_query(), which only returns TRUE if called somewhere from within the main loop. All we need to do, is extend our previous IF statement to include this condition as well.
Conclusion
There you have it. Your own custom pagination rules. Add or remove rules as you see fit, and make it your own.
Let us know if you find this article useful, if you implemented it in your own blogs and themes, and how you have improved it.
The complete code for this tutorial is: