Tips・WordPress templates and fetching posts

How to add pagination in WordPress

How to add pagination to a post archive. Covers the one-line the_posts_pagination, passing paged to a WP_Query sub-loop, and why page 2 sometimes 404s—plus how to fix it.

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 from get_query_var('paged'))
  • Pass total and current to paginate_links() (total is $query->max_num_pages)
  • Call wp_reset_postdata() at the end
TipsHow to fetch posts outside the loop (WP_Query)The basics of sub-loops, and why cleanup matters

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.

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

  1. Main-loop pagination is one line of the_posts_pagination(). Adjust the wording with arguments
  2. With WP_Query, 'paged' => get_query_var('paged') and total on paginate_links() are both required
  3. For 404s, try the page variable on static pages, and re-saving the permalink settings everywhere else
  4. 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.

FAQ

What's the quickest way to add pagination?
For a main-loop archive, write a single the_posts_pagination() after you close the loop. Arguments let you adjust things like prev_text and mid_size.
Pagination doesn't work on a list I built with WP_Query.
Without 'paged' => max(1, get_query_var('paged')) in your query arguments, page 1 is shown forever. Build the output with paginate_links(), passing 'total' => $query->max_num_pages.
Page 2 gives me a 404.
If you're running a sub-loop inside a static page, use get_query_var('page') rather than get_query_var('paged'). Otherwise, opening the Permalinks settings screen and pressing "Save Changes" regenerates the rules and usually fixes it.
I want to change how many posts appear per page.
For the whole site, change "Blog pages show at most" under "Settings" → "Reading." To change only a specific archive, set posts_per_page via pre_get_posts.