Pagination for a post archive takes one line in the main loop.
<?php the_posts_pagination(); ?>Where people get stuck is lists built with WP_Query. Those need to be told “which page are we on?” Let’s go through it.
Pagination in the main loop
For the headline list of a page—home.php, archive.php, category.php and friends—use the_posts_pagination(). You can set the wording too.
<?php
the_posts_pagination([
'mid_size' => 2, // how many numbers to show either side of the current page
'prev_text' => 'Previous',
'next_text' => 'Next',
'screen_reader_text' => 'Posts navigation', // heading (for screen readers)
]);
?>The HTML it outputs looks like this. Target these classes with your CSS.
<nav class="navigation pagination">
<div class="nav-links">
<span aria-current="page" class="page-numbers current">1</span>
<a class="page-numbers" href="…/page/2/">2</a>
<a class="next page-numbers" href="…/page/2/">Next</a>
</div>
</nav>.pagination .nav-links { display: flex; gap: 0.5rem; justify-content: center; }
.pagination .page-numbers {
padding: 0.4em 0.8em;
border: 1px solid #ddd;
border-radius: 6px;
text-decoration: none;
}
.pagination .page-numbers.current { background: #333; color: #fff; }If “previous / next” is all you need, there are two even lighter functions.
<?php previous_posts_link('Newer posts'); ?>
<?php next_posts_link('Older posts'); ?>Pagination for a WP_Query sub-loop
A query you built yourself shows page 1 forever unless you tell it which page you’re on. This is the biggest stumbling block.
<?php
$paged = max(1, (int) get_query_var('paged')); // the current page number (1 if absent)
$query = new WP_Query([
'post_type' => 'shohin',
'posts_per_page' => 12,
'paged' => $paged, // ← this is required
]);
?>
<?php if ($query->have_posts()) : ?>
<div class="cards">
<?php while ($query->have_posts()) : $query->the_post(); ?>
<article class="card"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></article>
<?php endwhile; ?>
</div>
<?php
// Output the pagination (this isn't the main query, so we build it ourselves)
echo '<nav class="pagination">';
echo paginate_links([
'total' => $query->max_num_pages, // total number of pages
'current' => $paged,
'mid_size' => 2,
'prev_text' => 'Previous',
'next_text' => 'Next',
]);
echo '</nav>';
?>
<?php endif; ?>
<?php wp_reset_postdata(); ?>Three things to remember.
- Pass
'paged'to the query (take it fromget_query_var('paged')) - Pass
totalandcurrenttopaginate_links()(totalis$query->max_num_pages) - Call
wp_reset_postdata()at the end
When page 2 gives a 404
There are roughly three causes. Check them in order.
1. A sub-loop inside a static page (page, not paged)
Only when you run a sub-loop inside a static page (page.php, page-shohin.php), the page number arrives in the query variable page rather than paged. That’s because paged is already used for something else on static pages.
<?php
$paged = max(1, (int) get_query_var('paged'), (int) get_query_var('page'));
?>Picking up both with max() means the same code works on static pages and archives alike.
2. Stale permalink rules
After adding a post type or taxonomy, the URL rewrite rules need rebuilding. Just open “Settings” → “Permalinks” in the admin screen and press “Save Changes” and they’re regenerated (you don’t need to change anything). A surprising number of custom-post-type 404s are fixed this way.
3. A mismatch between page 1 and the post count
Leave 'posts_per_page' => -1 (fetch everything) and use paginate_links(), and the total page count stays at 1 so pagination does nothing. On any list with pagination, always specify a count.
Changing how many posts appear per page
- The whole site—“Settings” → “Reading” → “Blog pages show at most”
- One specific archive—set it via
pre_get_posts
function my_posts_per_page($query) {
if (is_admin() || !$query->is_main_query()) {
return;
}
if (is_post_type_archive('shohin')) {
$query->set('posts_per_page', 12);
}
}
add_action('pre_get_posts', 'my_posts_per_page');This is the right way to change the count on a main list. Rebuilding it with WP_Query puts it out of sync with pagination and breadcrumbs.
If you want a “Load more” button
The foundation is the same when you want a button that loads more instead of numbers. Use the URL from next_posts_link() in place of paginate_links(), fetch the next page’s HTML with JavaScript and insert it. The safe way to proceed is to get pagination working correctly first, then build on top of it.
Summary
- Main-loop pagination is one line of
the_posts_pagination(). Adjust the wording with arguments - With
WP_Query,'paged' => get_query_var('paged')andtotalonpaginate_links()are both required - For 404s, try the
pagevariable on static pages, and re-saving the permalink settings everywhere else - Change the main list’s count with
pre_get_posts. Don’t rebuild the query
Working pagination is what keeps a site navigable as the articles pile up.