WordPress paginate_links Example: Build Clean Archive Pagination
Use paginate_links to build clean WordPress archive pagination with stable URLs, correct current-page state, and predictable output.
Published
April 27, 2026
Reading Time
2 min read
Updated
April 27, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 27, 2026.
Full Report
Last reviewed: April 27, 2026
Archive pagination looks trivial until a theme needs accessible markup, stable URLs, or custom query arguments that survive page changes. Many WordPress sites still hand-roll page links even though paginate_links() already handles most of the hard parts.
This guide shows how to use paginate_links() for clean archive pagination with explicit base URLs, current page handling, and predictable output.
Build pagination from the current query
<?php
$current = max( 1, get_query_var( 'paged' ) );
$total = max( 1, (int) $wp_query->max_num_pages );
$links = paginate_links(
array(
'base' => str_replace( 999999999, '%#%', esc_url( get_pagenum_link( 999999999 ) ) ),
'format' => '?paged=%#%',
'current' => $current,
'total' => $total,
'mid_size' => 1,
'end_size' => 1,
'prev_text' => 'Previous',
'next_text' => 'Next',
'type' => 'list',
)
);
if ( $links ) {
echo $links;
}
The placeholder pattern with 999999999 is standard because WordPress needs a value it can reliably replace with each page number.
Let the helper render accessible current state
paginate_links() can set the current page state automatically, including an aria-current value. That is better than manually styling one link and leaving screen readers with no context.
Use the right output type
plainreturns a string of links.arrayreturns each link separately for custom markup control.listreturns a ready-made list structure that fits many themes.
For most blogs and archives, list is the best default because it is easy to style and already grouped semantically.
Preserve extra query arguments only when needed
$links = paginate_links(
array(
'base' => str_replace( 999999999, '%#%', esc_url( get_pagenum_link( 999999999 ) ) ),
'format' => '?paged=%#%',
'current' => $current,
'total' => $total,
'add_args' => array(
'sort' => 'recent',
),
)
);
Only carry forward arguments that are part of the real UX. Blindly forwarding everything from the request creates messy URLs fast.
Production checklist
- Use the active query’s real current page and total page count.
- Generate the base URL with
get_pagenum_link(). - Prefer
type => 'list'for clean semantic markup. - Carry forward only the query arguments the UX genuinely needs.
- Render nothing when the total page count is less than two.
- Test pagination on archives, search results, and filtered lists separately.
Common mistakes
- Hardcoding page URLs. Permalink structures differ between sites.
- Using the wrong current page variable. Pagination breaks quickly on custom queries.
- Forwarding every request argument. URLs become noisy and inconsistent.
- Skipping empty-output checks. Single-page archives should not show pagination wrappers.
- Building pagination manually without accessibility state. That adds work for worse output.
Related reading
If the archive query itself is customized, pair this with the pre_get_posts guide. If the page layout differs by archive type, combine it with the template hierarchy article.


