Pagination

How paged, page and max_num_pages fit together, and how to paginate both the main query and a custom one.

Syntaxthe_posts_pagination( array( 'mid_size' => 2, 'prev_text' => 'Previous', 'next_text' => 'Next' ) );

Pagination in WordPress is three things working together: a query var carrying the requested page, a LIMIT derived from it, and a function that renders links from max_num_pages.

paged versus page

They are different query vars. paged is the page number of a post listing — archives, the blog index, search results. page is the page number inside a single post or page that has been split with the <!--nextpage--> tag. The trap is a site using a static front page for its posts: there paged is empty and WordPress populates page instead. Read them with get_query_var( string $query_var, mixed $default_value = '' ); the default parameter exists since WordPress 3.9.0.

Rendering the links

the_posts_pagination( array $args = array() ) echoes a numbered nav block; get_the_posts_pagination() returns it. Its arguments pass through to paginate_links(), plus screen_reader_text (default 'Posts pagination'), aria_label (added in 5.3.0) and class (default 'pagination', added in 5.5.0). Note that the wrapper defaults mid_size to 1. It is the shorthand; paginate_links() is the longhand it wraps, whose total defaults to $wp_query->max_num_pages, current to the paged query var, mid_size to 2 and type to 'plain'. paginate_links() returns null when the total number of pages is less than 2.

Custom queries

A WP_Query you build does not know about the URL. Pass 'paged' in yourself, and render links with paginate_links() using your object's max_num_pages.

Example

Example · php
<?php
// wp-content/themes/your-theme/archive.php — main query pagination.
if ( have_posts() ) :
	while ( have_posts() ) :
		the_post();
		get_template_part( 'template-parts/content', get_post_type() );
	endwhile;

	// Shorthand. Reads $wp_query->max_num_pages and the 'paged' query var.
	the_posts_pagination(
		array(
			'mid_size'           => 1, // default
			'prev_text'          => __( 'Previous', 'your-theme' ),
			'next_text'          => __( 'Next', 'your-theme' ),
			'screen_reader_text' => __( 'Posts navigation', 'your-theme' ),
			'aria_label'         => __( 'Posts', 'your-theme' ), // since WP 5.3
			'class'              => 'pagination',               // since WP 5.5
		)
	);
endif;

When to use it

  • Add numbered pagination to a theme's archive template
  • Paginate a custom post type listing built with new WP_Query
  • Fix a listing that never leaves page one on a static front page
  • Swap numbered links for older/newer navigation on a large archive

More examples

Paginating a custom WP_Query

This is the full custom-query pagination shape. The front-page branch is the part people omit, and it produces a listing that never advances past page one on sites using a static front page for posts.

Example · php
<?php
// 'paged' is not set on a static front page; WordPress uses 'page' there.
$paged = (int) get_query_var( 'paged', 1 );
if ( is_front_page() && ! is_home() ) {
	$paged = (int) get_query_var( 'page', 1 );
}
$paged = max( 1, $paged );

$books = new WP_Query(
	array(
		'post_type'      => 'book',
		'posts_per_page' => 12,
		'paged'          => $paged, // the query cannot read the URL for you
		// Do NOT also set 'offset' — it would override this.
	)
);

while ( $books->have_posts() ) {
	$books->the_post();
	the_title( '<h2>', '</h2>' );
}

// Longhand: the_posts_pagination() would use $wp_query, so call
// paginate_links() with this object's totals instead.
echo wp_kses_post(
	paginate_links(
		array(
			'total'   => $books->max_num_pages,
			'current' => $paged,
			'type'    => 'list', // 'plain' (default), 'array' or 'list'
		)
	)
);

wp_reset_postdata();

the_posts_pagination() shorthand vs paginate_links() longhand

Use the shorthand for a normal archive: it emits the nav element, the screen-reader heading and the aria-label for you. Drop to paginate_links() when you need markup the wrapper will not produce, or a total that is not the main query's.

Example · php
<?php
// Shorthand: one call, standard nav markup, main query.
the_posts_pagination( array( 'mid_size' => 1 ) );

// Longhand: the same defaults written out explicitly.
// paginate_links() returns null when there is nothing to paginate.
$links = paginate_links(
	array(
		'total'     => $GLOBALS['wp_query']->max_num_pages, // the default
		'current'   => max( 1, (int) get_query_var( 'paged', 1 ) ), // the default
		'mid_size'  => 1,
		'end_size'  => 1,
		'prev_next' => true,
		'type'      => 'plain',
	)
);

if ( null !== $links ) {
	printf(
		'<nav class="pagination" aria-label="%s">%s</nav>',
		esc_attr__( 'Posts', 'your-theme' ),
		wp_kses_post( $links )
	);
}

Simple next/previous links instead of numbers

Numbered pagination costs a total-row count; next/previous does not, so this is the cheaper option on very large archives where you could also set 'no_found_rows'. next_posts_link() takes max_num_pages as its second argument, which is what makes it usable outside the main query.

Example · php
<?php
// Main query, no page numbers. Note the reversed direction: on a
// reverse-chronological archive, "next posts" means older posts.
the_posts_navigation(
	array(
		'prev_text' => __( 'Older posts', 'your-theme' ),
		'next_text' => __( 'Newer posts', 'your-theme' ),
	)
);

// For a custom query, the low-level pair still works because they
// accept an explicit max page count:
next_posts_link( __( 'Older books', 'your-theme' ), $books->max_num_pages );
previous_posts_link( __( 'Newer books', 'your-theme' ) );

Discussion

  • Be the first to comment on this lesson.