Better WordPress pagination

UPDATED 6/6/09: Added query string to work with search pagination.
Ever wanted to have better pagination controls in WordPress? Those “Newer posts / Older posts” links are a bit limited. Much better is the ubiquitous numbered list, a la Google’s results page.
There is at least one plugin for this. As ever, I’m keen to keep plugin overhead to a minimum, and where possible to just include a simple function in my custom themes for basic functionality like this. Here’s the function:
function pagination( $query, $baseURL ) {
$page = $query->query_vars["paged"];
if ( !$page ) $page = 1;
$qs = $_SERVER["QUERY_STRING"] ? "?".$_SERVER["QUERY_STRING"] : "";
// Only necessary if there's more posts than posts-per-page
if ( $query->found_posts > $query->query_vars["posts_per_page"] ) {
echo '<ul class="paging">';
// Previous link?
if ( $page > 1 ) {
echo '<li class="previous"><a href="'.$baseURL.'page/'.($page-1).'/'.$qs.'">« previous</a></li>';
}
// Loop through pages
for ( $i=1; $i <= $query->max_num_pages; $i++ ) {
// Current page or linked page?
if ( $i == $page ) {
echo '<li class="active">'.$i.'</li>';
} else {
echo '<li><a href="'.$baseURL.'page/'.$i.'/'.$qs.'">'.$i.'</a></li>';
}
}
// Next link?
if ( $page < $query->max_num_pages ) {
echo '<li><a href="'.$baseURL.'page/'.($page+1).'/'.$qs.'">next »</a></li>';
}
echo '</ul>';
}
}
A little explanation is in order…
The function has been written with “custom loops” as well as the standard WordPress Loop in mind. It can be used when you define your own loop with the WP_Query object, as well as on normal templates that use the have_posts() construct. So, for custom loops built using the instructions in the last linked article, pass $recentPosts (or whatever you call your custom loop query object) as the first parameter. On normal loop templates, pass $wp_query. In either case, this query object contains some very useful info (like max_num_pages) that really help keep the above code simple.
I’ll leave the second parameter up to you to construct. It just has to be the basic URL of the listing page, onto which pagination stuff will be appended. It should include the trailing slash.
I’ve also included the query string in the right place, which should make this function work for pagination on WordPress search results. One oddity in this respect is that when you have something other than the front page of your site as your main post listing page (on this site it’s /blog/), the paging URLs don’t seem to work. So /blog/page/2/ just returns a 404. I’ve fudged around this by setting the $baseURL parameter of the call to this function on the blog to just / (the root). It kind of works. Anyone know why including the page slug doesn’t work?
Well, the rest should be self-explanatory. Again, as with many of my WordPress tips here, this is aimed at theme developers who know their way around WP and PHP a bit. If you’re not quite there yet, maybe try the plugin I linked to at the start of this post…
Oh, as an added bonus, the CSS I use for the pagination list!
ul.paging { list-style: none; padding: 0; font-weight: bold; }
ul.paging li { float: left; margin: 0 6px 6px 0; }
ul.paging li a, ul.paging li.active { display: block; padding: 3px 6px; background-color: #f5f5f5; border: 1px solid #ccc; }
ul.paging li.active { background-color: #000; border-color: #000; color: #fff; }
Welcome! I build websites - mostly based on the brilliant, free & open 
Patternhead (9th July 2009)
This looks really useful.
Thanks for sharing :)
WP Tim (11th July 2009)
Please post the where this function is called or how to incorporate in a page. Thanks!
Steve Taylor (11th July 2009)
Tim, you would just call the function where you want the pagination controls. The basic call (say, after The Loop in
index.php) would be:pagination( $wp_query, "/" );If often include one before The Loop too, but set to only appear if not on the first page:
if ( $paged ) slt_pagination( $wp_query, "/" );For
archive.php, you’ll probably have to set up the$baseURLparameter dynamically, depending on the type of archive being shown. (Hint: the WP functionsget_category_link()andget_month_link()help out a lot!)If you’re not following, then it’s possible that a plugin might be your cup of tea. For the most part I’m addressing theme developers here.
Good luck!
sean (24th August 2009)
Hello, I’m trying to improve my search.php page with your code. No more than 5 posts should be displayed per page.
Here’s the code before yours was added:
<a href="" title=""></a>Brand: <!-- End of Byline Containing Div -->
<a href="" title="">more...</a>
Here’s how I’ve added yours:
query_vars["paged"];
if ( !$page ) $page = 1;
$qs = $_SERVER["QUERY_STRING"] ? "?".$_SERVER["QUERY_STRING"] : "";
// Only necessary if there's more posts than posts-per-page
if ( $query->found_posts > $query->query_vars["posts_per_page"] ) {
echo '';
// Previous link?
if ( $page > 1 ) {
echo '<a href="'.$baseURL.'page/'.($page-1).'/'.$qs.'" rel="nofollow">« previous</a>';
}
// Loop through pages
for ( $i=1; $i max_num_pages; $i++ ) {
// Current page or linked page?
if ( $i == $page ) {
echo ''.$i.'';
} else {
echo '<a href="'.$baseURL.'page/'.$i.'/'.$qs.'" rel="nofollow">'.$i.'</a>';
}
}
// Next link?
if ( $page max_num_pages ) {
echo '<a href="'.$baseURL.'page/'.($page+1).'/'.$qs.'" rel="nofollow">next »</a>';
}
echo '';
}
} ?>
<a href="" title=""></a>
Brand: <!-- End of Byline Containing Div -->
<a href="" title="">more...</a>
No Results Found
Please try your search again using different keywords.
This is my first attempt at pagination, but I’ve used WordPress for 5-6 projects now.
Steve Taylor (24th August 2009)
Hi Sean, from the code you posted it’s not really clear what your issue is. Could you be more specific as to what the issue is? (Please don’t post more code unless necessary, it needed a lot of tidying up!)
If you don’t understand the code enough to explain what the problem is, you might be better off with a plugin – check out the link at the top of this post.
Valid Information (11th November 2009)
Looks complicated but very useful information
ayman (9th January 2010)
how can i limit number of pages that displayed ?not all pages
Steve Taylor (9th January 2010)
Hi Ayman, you mean the number of total pages? Or to abbreviate the pagination numbers when there’s a lot, like this?
« previous 1 2 3 4 ... 23 24 25 next »Abbreviating the pagination numbers is certainly something I’ll try and get round to one day – should be pretty easy. Maybe one of the current pagination plugins does that?
Regarding limiting the total number of pages, these pages should help:
http://weblogtoolscollection.com/archives/2008/04/13/define-your-own-wordpress-loop-using-wp_query/
http://digwp.com/2009/12/limit-posts-without-plugin/
Pete (20th February 2010)
Works brilliantly. So simple.
Pete (20th February 2010)
Am I the only one that’s get recursive querystring addition when implementing this pagination script?
I have had to alter the make up of the href a little, but the bulk of the code is still the same.
I could really do with a little bit of help with this one.
Eg: http://website/?s=lorem&paged=2&paged=1&paged=3 this just keeps getting longer and longer dependent on how many links are clicked. Am I just going to have to write a string manipulation function to sort this out?
Steve Taylor (21st February 2010)
Pete, could you give a URL to an example where this is happening? The only thing I can guess at from your sample URL is that perhaps the script doesn’t work properly without “pretty permalinks“. I’m not sure why you wouldn’t have a pretty permalink structure, but if you’re stuck with “ugly” query string-based links—well, it should be easy to adapt. Do post your solution if you need to add anything.
Steven Wilson (4th March 2010)
Work a treat !! .. Thanks for sharing.
Peter (22nd March 2010)
ayman (9th January 2010) and Steve, to limit the number of pages displayed change
if ( $i == $page ) {echo ''.$i.'';
} else {
echo '<a href="'.$baseURL.'page/'.$i.'/'.$qs.'">'.$i.'</a>';
to
if ( $i == $page ) {echo ''.$i.'';
} elseif ($i < 10) {
echo '<a href="'.$baseURL.'page/'.$i.'/'.$qs.'">'.$i.'</a>';
P.s thanks steve you rock!
Steve Taylor (22nd March 2010)
Thanks Peter! (BTW, do use pastebin.ca for snippets, saves on formatting issues.)
Ryan (11th May 2010)
Hey Steve! First of all, great site.
Secondly, I have quick question:
If you notice on my “News” page (http://abletonlife.com/news), when you click on the “2″ at the bottom of the page, it loops back to the same page. I know I have more than 10 posts, so why isn’t it showing the other ones like on the home page?
Any help is appreciated! Thanks!
- Ryan
Steve Taylor (12th May 2010)
Ryan, glad you like the site! As to your issue, hard to tell what’s going on from the outside. However, it seems clear that the issue is probably nothing to do with this pagination code. The URL http://abletonlife.com/news/page/2 is showing the same as http://abletonlife.com/news, so WP’s paging itself isn’t working. The only thing I can spot as a possible starting point is that there’s no trailing slash on the URLs. If one is added, it gets removed. I can’t remember whether it’s a server-level thing or a WP thing, but in my experience the normal behaviour is for trailing slashes to be added if they’re missing. I wonder if the paging URL without a trailing slash isn’t being recognized by WP? If that lead goes nowhere, maybe check the WP forums. Good luck!
Nadeeshyama Talagala (4th July 2010)
This is great. However I found a way to eliminate the need of $baseUrl. You can add the following to make the $baseUrl for any category eliminating the second argument to the function.
$url = get_bloginfo('url');
$baseUrl = '';
$category_name = $query->query_vars['category_name'];
if (is_string($category_name)) {
$category_id = get_cat_ID($category_name);
if (isset($category_id)) {
$category_link = get_category_link($category_id);
if (is_string($category_link)) {
$baseUrl = $category_link . '/';
}
}
}
Steve Taylor (4th July 2010)
Thanks Nadeeshyama. I still find $baseURL useful for when you’re outputting posts in a loop on some custom page separate from the main blog, e.g. /news/. This is usually when I’ve fudged different post “types” using categories, though of course WP 3.0 custom post types will make this kind of thing easier, and maybe $baseURL becomes even more relevant?