Cleanly-done pagination with custom WP_Query objects

If you have ever created a custom WP_Query object, chances are you have potentially needed to also provide pagination for the resulting loop and display of posts. Sadly, WordPress core does not make this the easiest to achieve, but it is possible depending on which pagination-based functions you use. This tutorial aims to show how to achieve functional pagination without getting very “dirty” in your code.

What I mean by “dirty” in this case is having to utilize the global “$wp_query” variable name. Many of the following functions rely on that specifically, without allowing custom setting overrides. If you have ever come across a tutorial that brings up setting the original $wp_query object to a temporary variable, and then setting $wp_query to null before re-purposing it, then you know what I am referring to here.

Pagination functions to use

The biggest key is the ability to provide a “max pages” value. The WordPress core source code shows that the only two functions that provide the ability to custom set those are:

  • get_next_posts_link()
  • get_previous_posts_link()

They have a second parameter available that allows you to set your own max_num_pages value, instead of absolutely relying on the “$wp_query” variable name.

Imagine your custom WP_Query object is named “$my_query”, to use its own “max_num_pages” property, you just need to do the following:

printf( '<div>%s</div>', get_next_posts_link( 'Older posts', $my_query->max_num_pages ) );
printf( '<div>%s</div>', get_previous_posts_link( 'Newer posts', $my_query->max_num_pages ) );

This makes the two functions use the custom queries’ values instead of the global “$wp_query” values which get used if nothing is passed in.

Pagination functions to avoid for clean pagination

These functions do not allow a way to filter in or pass in “max pages” overriding values, and all rely on the global “$wp_query” object instead.

  • posts_nav_link()
  • get_posts_nav_link()
  • get_the_posts_navigation()
  • get_the_posts_pagination()
  • the_posts_pagination()

Full Basic WP_Query example

if ( get_query_var( 'paged' ) ) {
	$paged = get_query_var( 'paged' );
} else if ( get_query_var( 'page' ) ) {
	// This will occur if on front page.
	$paged = get_query_var( 'page' );
} else {
	$paged = 1;
$my_query = new WP_Query( array(
	'post_type'           => 'movie',
	'posts_per_page'      => 2,
	'paged'               => $paged,
) );
while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
	<h2><?php the_title(); ?></h2>
printf( '<div>%s</div>', get_next_posts_link( 'Older posts', $my_query->max_num_pages ) );
printf( '<div>%s</div>', get_previous_posts_link( 'Newer posts', $my_query->max_num_pages ) );

What this example does is grab the paged value for the current pagination, and pass it into the WP_Query calls, along with our movie post type and 2 posts per page. Afterwards, it’ll show the title and excerpt of any found movies, and pass in its own max number of pages into our pagination functions.


While not the most flexible, it does show that there is possible pagination available for custom WP_Query calls. At least in theory, re-purposing the “$wp_query” variable name specifically, should make the other functions work as well, but on a personal level, I’ve never been a fan of that detail. Thankfully we have a couple functions that allow overriding of the value so that we can provide a more “clean” option.

Updated Dec 12, 2022 9:09 PM