Pagination
How paged, page and max_num_pages fit together, and how to paginate both the main query and a custom one.
the_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
<?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.
<?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.
<?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.
<?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