paginate_links()WP 2.1.0

Retrieve paginated link for archive post pages.

Technically, the function can be used to create paginated link list for any area. The 'base' argument is used to reference the url, which will be used to create the paginated links. The 'format' argument is then used for replacing the page number. It is however, most likely and by default, to be used on the archive post pages.

The 'type' argument controls format of the returned value. The default is 'plain', which is just a string with the links separated by a newline character. The other possible values are either 'array' or 'list'. The 'array' value will return an array of the paginated link list to offer full control of display. The 'list' value will place all of the paginated links in an unordered HTML list.

The 'total' argument is the total amount of pages and is an integer. The 'current' argument is the current page number and is also an integer.

An example of the 'base' argument is "http://example.com/all_posts.php%_%" and the '%_%' is required. The '%_%' will be replaced by the contents of in the 'format' argument. An example for the 'format' argument is "?page=%#%" and the '%#%' is also required. The '%#%' will be replaced with the page number.

You can include the previous and next links in the list by setting the 'prev_next' argument to true, which it is by default. You can set the previous text, by using the 'prev_text' argument. You can set the next text by setting the 'next_text' argument.

If the 'show_all' argument is set to true, then it will show all of the pages instead of a short list of the pages near the current page. By default, the 'show_all' is set to false and controlled by the 'end_size' and 'mid_size' arguments. The 'end_size' argument is how many numbers on either the start and the end list edges, by default is 1. The 'mid_size' argument is how many numbers to either side of current page, but not including current page.

It is possible to add query vars to the link by using the 'add_args' argument and see add_query_arg() for more information.

The 'before_page_number' and 'after_page_number' arguments allow users to augment the links themselves. Typically this might be to add context to the numbered links so that screen reader users understand what the links are for. The text strings are added before and after the page number - within the anchor tag.

Hooks from the function

Return

String|String[]|null. String of page links or array of page links, depending on 'type' argument. Void if total number of pages is less than 2.

Usage

paginate_links( $args );
$args(string|array)

Array or string of arguments for generating paginated links for archives.

Default: ''

  • base(string)
    Base of the paginated url.
    Default: ''

  • format(string)
    Format for the pagination structure.
    Default: ''

  • total(int)
    The total amount of pages.
    Default: value WP_Query's max_num_pages or 1

  • current(int)
    The current page number. query var or 1.
    Default: 'paged'

  • aria_current(string)
    The value for the aria-current attribute. Possible values are 'page', 'step', 'location', 'date', 'time', 'true', 'false'.
    Default: 'page'

  • show_all(true|false)
    Whether to show all pages.
    Default: false

  • end_size(int)
    How many numbers on either the start and the end list edges.
    Default: 1

  • mid_size(int)
    How many numbers to either side of the current pages.
    Default: 2

  • prev_next(true|false)
    Whether to include the previous and next links in the list.
    Default: true

  • prev_text(string)
    The previous page text.
    Default: '« Previous'

  • next_text(string)
    The next page text.
    Default: 'Next »'

  • type(string)
    Controls format of the returned value. Possible values are 'plain', 'array' and 'list'.
    Default: 'plain'

  • add_args(array)
    An array of query args to add.
    Default: false

  • add_fragment(string)
    A string to append to each link.
    Default: ''

  • before_page_number(string)
    A string to appear before the page number.
    Default: ''

  • after_page_number(string)
    A string to append after the page number.
    Default: ''

Examples

0

#1 Pagination, analogous to wp_pagenavi

To add pagination to a search results page or an archives page, use this code:

function my_pagenavi() {
	global $wp_query;

	$big = 999999999; // unique (int) to replace

	$args = array(
		'base'    => str_replace( $big, '%#%', get_pagenum_link( $big ) ),
		'format'  => '',
		'current' => max( 1, get_query_var('paged') ),
		'total'   => $wp_query->max_num_pages,
	);

	$result = paginate_links( $args );

	// remove the pagination add-on for the first page
	$result = preg_replace( '~/page/1/?([\'"])~', '', $result );

	echo $result;
}

Now, where you want to output the pagination:

my_pagenavi();
0

#2 Alternative to this function

This function always returns HTML, even if parameter type=array you get an array of ready-made <a> tags. This may not work when you need to completely change the HTML structure of your pagination. Below is a small function that returns an array of objects instead of HTML.

/**
 * Generates array of pagination links.
 *
 * @author Kama (wp-kama.com)
 * @varsion 2.5
 *
 * @param array $args {
 *
 *     @type int    $total        Maximum allowable pagination page.
 *     @type int    $current      Current page number.
 *     @type string $url_base     URL pattern. Use `{pagenum}` placeholder.
 *     @type string $first_url    URL to first page. Default: '' - taken automaticcaly from $url_base.
 *     @type int    $mid_size     Number of links before/after current: 1 ... 1 2 [3] 4 5 ... 99. Default: 2.
 *     @type int    $end_size     Number of links at the edges: 1 2 ... 3 4 [5] 6 7 ... 98 99. Default: 1.
 *     @type bool   $show_all     true - Show all links. Default: false.
 *     @type string $a_text_patt  `%s` will be replaced with number of pagination page. Default: `'%s'`.
 *     @type bool   $is_prev_next Whether to show prev/next links. « Previou 1 2 [3] 4 ... 99 Next ». Default: false.
 *     @type string $prev_text    Default: `« Previous`.
 *     @type string $next_text    Default: `Next »`.
 * }
 *
 * @return array
 */
function kama_paginate_links_data( array $args ): array {
	global $wp_query;

	$args += [
		'total'        => 1,
		'current'      => 0,
		'url_base'     => '/{pagenum}',
		'first_url'    => '',
		'mid_size'     => 2,
		'end_size'     => 1,
		'show_all'     => false,
		'a_text_patt'  => '%s',
		'is_prev_next' => false,
		'prev_text'    => '« Previous',
		'next_text'    => 'Next »',
	];

	$rg = (object) $args;

	$total_pages = max( 1, (int) ( $rg->total ?: $wp_query->max_num_pages ) );

	if( $total_pages === 1 ){
		return [];
	}

	// fix working parameters

	$rg->total = $total_pages;
	$rg->current = max( 1, abs( $rg->current ?: get_query_var( 'paged', 1 ) ) );

	$rg->url_base = $rg->url_base ?: str_replace( PHP_INT_MAX, '{pagenum}', get_pagenum_link( PHP_INT_MAX ) );
	$rg->url_base = wp_normalize_path( $rg->url_base );

	if( ! $rg->first_url ){
		// /foo/page(d)/2 >>> /foo/ /foo?page(d)=2 >>> /foo/
		$rg->first_url = preg_replace( '~/paged?/{pagenum}/?|[?]paged?={pagenum}|/{pagenum}/?~', '', $rg->url_base );
		$rg->first_url = user_trailingslashit( $rg->first_url );
	}

	// core array

	if( $rg->show_all ){
		$active_nums = range( 1, $rg->total );
	}
	else {

		if( $rg->end_size > 1 ){
			$start_nums = range( 1, $rg->end_size );
			$end_nums = range( $rg->total - ($rg->end_size - 1), $rg->total );
		}
		else {
			$start_nums = [ 1 ];
			$end_nums = [ $rg->total ];
		}

		$from = $rg->current - $rg->mid_size;
		$to = $rg->current + $rg->mid_size;

		if( $from < 1 ){
			$to = min( $rg->total, $to + absint( $from ) );
			$from = 1;

		}
		if( $to > $rg->total ){
			$from = max( 1, $from - ($to - $rg->total) );
			$to = $rg->total;
		}

		$active_nums = array_merge( $start_nums, range( $from, $to ), $end_nums );
		$active_nums = array_unique( $active_nums );
		$active_nums = array_values( $active_nums ); // reset keys
	}

	// fill by core array

	$pages = [];

	if( 1 === count( $active_nums ) ){
		return $pages;
	}

	$item_data = static function( $num ) use ( $rg ){

		$data = [
			'is_current'   => false,
			'page_num'     => null,
			'url'          => null,
			'link_text'    => null,
			'is_prev_next' => false,
			'is_dots'      => false,
		];

		if( 'dots' === $num ){

			return (object) ( [
			   'is_dots' => true,
			   'link_text' => '…',
			] + $data );
		}

		$is_prev = 'prev' === $num && ( $num = max( 1, $rg->current - 1 ) );
		$is_next = 'next' === $num && ( $num = min( $rg->total, $rg->current + 1 ) );

		$data = [
			'is_current'   => ! ( $is_prev || $is_next ) && $num === $rg->current,
			'page_num'     => $num,
			'url'          => 1 === $num ? $rg->first_url : str_replace( '{pagenum}', $num, $rg->url_base ),
			'is_prev_next' => $is_prev || $is_next,
		] + $data;

		if( $is_prev ){
			$data['link_text'] = $rg->prev_text;
		}
		elseif( $is_next ) {
			$data['link_text'] = $rg->next_text;
		}
		else {
			$data['link_text'] = sprintf( $rg->a_text_patt, $num );
		}

		return (object) $data;
	};

	foreach( $active_nums as $indx => $num ){

		$pages[] = $item_data( $num );

		// set dots
		$next = $active_nums[ $indx + 1 ] ?? null;
		if( $next && ($num + 1) !== $next ){
			$pages[] = $item_data( 'dots' );
		}
	}

	if( $rg->is_prev_next ){
		$rg->current !== 1 && array_unshift( $pages, $item_data( 'prev' ) );
		$rg->current !== $rg->total && $pages[] = $item_data( 'next' );
	}

	return $pages;
}

What the function outputs:

$links_data = kama_paginate_links_data( [
	'total'    => 3,
	'current'  => 2,
	'url_base' => 'http://site.com/page-name/paged/{pagenum}',
	'mid_size' => 2,
] );

print_r( $links_data );

/*
Array
(
	[0] => stdClass Object
		(
			[is_current] =>
			[page_num] => 288
			[url] => http://site.com/page-name/paged/288
			[is_prev_next] => 1
			[link_text] => « Previous
			[is_dots] =>
		)

	[1] => stdClass Object
		(
			[is_current] =>
			[page_num] => 1
			[url] => http://site.com/page-name/
			[is_prev_next] =>
			[link_text] => 1
			[is_dots] =>
		)

	[2] => stdClass Object
		(
			[is_dots] => 1
			[link_text] => …
			[is_current] =>
			[page_num] =>
			[url] =>
			[is_prev_next] =>
		)

	[3] => stdClass Object
		(
			[is_current] =>
			[page_num] => 285
			[url] => http://site.com/page-name/paged/285
			[is_prev_next] =>
			[link_text] => 285
			[is_dots] =>
		)

	[4] => stdClass Object
		(
			[is_current] =>
			[page_num] => 286
			[url] => http://site.com/page-name/paged/286
			[is_prev_next] =>
			[link_text] => 286
			[is_dots] =>
		)

	[5] => stdClass Object
		(
			[is_current] => 1
			[page_num] => 287
			[url] => http://site.com/page-name/paged/287
			[is_prev_next] =>
			[link_text] => 287
			[is_dots] =>
		)

)
*/

Now use this function in the loop:

<?php

$links_data = kama_paginate_links_data( [
	'total' => 3,
	'current' => 2,
	'url_base' => 'http://site.com/page-name/paged/{pagenum}',
] );

if( $links_data ){
	?>

	<ul>
		<?php foreach( $links_data as $link ) { ?>
		<li>
			<?php if ( $link->is_current ) { ?>
				<strong><?php _e( $link->page_num ) ?></strong>
			<?php } else { ?>
				<a href="<?php esc_attr_e( $link->url ) ?>"><?php _e( $link->page_num ) ?></a>
			<?php } ?>
		</li>
		<?php } ?>
	</ul>

	<?php
}

We get it:

<ul>
	<li>
		<a href="http://site.com/page-name/paged/1">1</a>
	</li>
	<li>
		<strong>2</strong>
	</li>
	<li>
		<a href="http://site.com/page-name/paged/3">3</a>
	</li>
</ul>

0

#3 Example with an arbitrary WP_Query query

When the posts are output by a separate query using new WP_Query, you can set the parameter total, in which you specify the WP_Query::$max_num_pages property to output pagination. Let's look at an example:

This is just a demo, because it doesn't take into account the main request, which may result in a 404 page and this code won't get to it at all. Also, it may not work correctly on a separate page of posts.

<?php

$paged = absint( get_query_var( 'paged' ) ? : 1 ); // 1 default value

$the_query = new WP_Query( [
	'posts_per_page' => 5,
	'category_name'  => 'gallery',
	'paged'          => $paged,
] );

// loop the output of received posts
while( $the_query->have_posts() ){
	$the_query->the_post();
	?>
	<!-- HTML every post -->
	<?php
}
wp_reset_postdata();

// pagination for custom query
$big = 999999999; // unique (int)

echo paginate_links( array(
	'base'    => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
	'current' => max( 1, get_query_var('paged') ),
	'total'   => $the_query->max_num_pages
) );
?>
0

#4 Pagination for custom WP_Query

In the example below we will display WooCommerce products (post_type=product) on a separate post page (post_type=post). I.e. we will use arbitrary query and make pagination for it.

// Requesting products
$query = new WP_Query( [
	'post_type'      => 'product',
	'posts_per_page' => 5,
	'paged'          => get_query_var( 'page' ),
] );

// Process the products received in the query, if any
if ( $query->have_posts() ) {

	while ( $query->have_posts() ) {
		$query->the_post();

		the_title();
	}

	wp_reset_postdata();
}

// Output pagination if there are more products than the requested amount
echo paginate_links( [
	'base'    => user_trailingslashit( wp_normalize_path( get_permalink() .'/%#%/' ) ),
	'current' => max( 1, get_query_var( 'page' ) ),
	'total'   => (int) $query->max_num_pages,
] );

We specified to output 5 products, if there are 22, for example, then there will be 5 pagination elements, 4 of which will be links of the following kind:

Current page, no link displayed (5 products)
http://example.com/название-записи/2/ (5 products)
http://example.com/название-записи/3/ (5 products)
http://example.com/название-записи/4/ (5 products)
http://example.com/название-записи/5/ (2 products)

Note:

  • In the parameter base where the pagination link view is formed, do not use words like page or paged. Because with page there will be a redirect from the pagination page to the post itself, and with paged there will be a 404 error on the pagination page.

  • Use get_query_var( 'paged' ) instead of the usual get_query_var( 'page' ) to get the pagination page number.

  • This code will not work on pages with content divided into several pages by the <!--nextpage--> tag, more details see here.

0

#5 Improving Accessibility

To add context to the numbered links to ensure that screen reader users understand what the links are for:

global $wp_query;

$big = 999999999; // need an unlikely integer
$translated = __( 'Page', 'mytextdomain' ); // Supply translatable string

echo paginate_links( array(
	'base'    => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
	'format'  => '?paged=%#%',
	'current' => max( 1, get_query_var('paged') ),
	'total'   => $wp_query->max_num_pages,
	'before_page_number' => '<span class="screen-reader-text">'. $translated .'</span>',
	'prev_text' => is_rtl() ? 'Previous Page &rarr;' : 'Previous Page &larr;',
	'next_text' => is_rtl() ? 'Next Page &larr;' : 'Next Page &rarr;', 
) );
0

#6 Add a prefix for all classes BEM

Let's say we changed the navigation template via the navigation_markup_template hook and now we want all nested elements to correspond to BEM classes.

We have the BEM block class hlpagination:

<?php
/// change the navigation (pagination) template
add_filter( 'navigation_markup_template', 'hl_navigation_template', 10, 2 );

function hl_navigation_template( $template, $class ){

	ob_start();
	?>
	<nav class="hlpagination %1$s" role="navigation">
		<div class="hlpagination__nav-links">%3$s</div>
	</nav>
	<?php
	return ob_get_clean();
}

%3$s is replaced by what paginate_links() returns and all classes must also have a prefix added:

/// fix html pagination for paginate_links()
add_filter( 'paginate_links_output', 'hl_fix_paginate_links' );

function hl_fix_paginate_links( $html ){

	$html = preg_replace_callback( '/ class=[\'"][^\'"]+[\'"]/', static function( $mm ){

		return strtr( $mm[0], [
			'current'      => '--current',
			'prev'         => 'hlpagination__prev',
			'next'         => 'hlpagination__next',
			'dots'         => 'hlpagination__dots',
			'page-numbers' => 'hlpagination__numbers',
		] );

	}, $html );

	return $html;
}

Notes

  • Global. WP_Query. $wp_query WordPress Query object.
  • Global. WP_Rewrite. $wp_rewrite WordPress rewrite component.

Changelog

Since 2.1.0 Introduced.
Since 4.9.0 Added the aria_current argument.

paginate_links() code WP 6.5.2

function paginate_links( $args = '' ) {
	global $wp_query, $wp_rewrite;

	// Setting up default values based on the current URL.
	$pagenum_link = html_entity_decode( get_pagenum_link() );
	$url_parts    = explode( '?', $pagenum_link );

	// Get max pages and current page out of the current query, if available.
	$total   = isset( $wp_query->max_num_pages ) ? $wp_query->max_num_pages : 1;
	$current = get_query_var( 'paged' ) ? (int) get_query_var( 'paged' ) : 1;

	// Append the format placeholder to the base URL.
	$pagenum_link = trailingslashit( $url_parts[0] ) . '%_%';

	// URL base depends on permalink settings.
	$format  = $wp_rewrite->using_index_permalinks() && ! strpos( $pagenum_link, 'index.php' ) ? 'index.php/' : '';
	$format .= $wp_rewrite->using_permalinks() ? user_trailingslashit( $wp_rewrite->pagination_base . '/%#%', 'paged' ) : '?paged=%#%';

	$defaults = array(
		'base'               => $pagenum_link, // http://example.com/all_posts.php%_% : %_% is replaced by format (below).
		'format'             => $format, // ?page=%#% : %#% is replaced by the page number.
		'total'              => $total,
		'current'            => $current,
		'aria_current'       => 'page',
		'show_all'           => false,
		'prev_next'          => true,
		'prev_text'          => __( '&laquo; Previous' ),
		'next_text'          => __( 'Next &raquo;' ),
		'end_size'           => 1,
		'mid_size'           => 2,
		'type'               => 'plain',
		'add_args'           => array(), // Array of query args to add.
		'add_fragment'       => '',
		'before_page_number' => '',
		'after_page_number'  => '',
	);

	$args = wp_parse_args( $args, $defaults );

	if ( ! is_array( $args['add_args'] ) ) {
		$args['add_args'] = array();
	}

	// Merge additional query vars found in the original URL into 'add_args' array.
	if ( isset( $url_parts[1] ) ) {
		// Find the format argument.
		$format       = explode( '?', str_replace( '%_%', $args['format'], $args['base'] ) );
		$format_query = isset( $format[1] ) ? $format[1] : '';
		wp_parse_str( $format_query, $format_args );

		// Find the query args of the requested URL.
		wp_parse_str( $url_parts[1], $url_query_args );

		// Remove the format argument from the array of query arguments, to avoid overwriting custom format.
		foreach ( $format_args as $format_arg => $format_arg_value ) {
			unset( $url_query_args[ $format_arg ] );
		}

		$args['add_args'] = array_merge( $args['add_args'], urlencode_deep( $url_query_args ) );
	}

	// Who knows what else people pass in $args.
	$total = (int) $args['total'];
	if ( $total < 2 ) {
		return;
	}
	$current  = (int) $args['current'];
	$end_size = (int) $args['end_size']; // Out of bounds? Make it the default.
	if ( $end_size < 1 ) {
		$end_size = 1;
	}
	$mid_size = (int) $args['mid_size'];
	if ( $mid_size < 0 ) {
		$mid_size = 2;
	}

	$add_args   = $args['add_args'];
	$r          = '';
	$page_links = array();
	$dots       = false;

	if ( $args['prev_next'] && $current && 1 < $current ) :
		$link = str_replace( '%_%', 2 == $current ? '' : $args['format'], $args['base'] );
		$link = str_replace( '%#%', $current - 1, $link );
		if ( $add_args ) {
			$link = add_query_arg( $add_args, $link );
		}
		$link .= $args['add_fragment'];

		$page_links[] = sprintf(
			'<a class="prev page-numbers" href="%s">%s</a>',
			/**
			 * Filters the paginated links for the given archive pages.
			 *
			 * @since 3.0.0
			 *
			 * @param string $link The paginated link URL.
			 */
			esc_url( apply_filters( 'paginate_links', $link ) ),
			$args['prev_text']
		);
	endif;

	for ( $n = 1; $n <= $total; $n++ ) :
		if ( $n == $current ) :
			$page_links[] = sprintf(
				'<span aria-current="%s" class="page-numbers current">%s</span>',
				esc_attr( $args['aria_current'] ),
				$args['before_page_number'] . number_format_i18n( $n ) . $args['after_page_number']
			);

			$dots = true;
		else :
			if ( $args['show_all'] || ( $n <= $end_size || ( $current && $n >= $current - $mid_size && $n <= $current + $mid_size ) || $n > $total - $end_size ) ) :
				$link = str_replace( '%_%', 1 == $n ? '' : $args['format'], $args['base'] );
				$link = str_replace( '%#%', $n, $link );
				if ( $add_args ) {
					$link = add_query_arg( $add_args, $link );
				}
				$link .= $args['add_fragment'];

				$page_links[] = sprintf(
					'<a class="page-numbers" href="%s">%s</a>',
					/** This filter is documented in wp-includes/general-template.php */
					esc_url( apply_filters( 'paginate_links', $link ) ),
					$args['before_page_number'] . number_format_i18n( $n ) . $args['after_page_number']
				);

				$dots = true;
			elseif ( $dots && ! $args['show_all'] ) :
				$page_links[] = '<span class="page-numbers dots">' . __( '&hellip;' ) . '</span>';

				$dots = false;
			endif;
		endif;
	endfor;

	if ( $args['prev_next'] && $current && $current < $total ) :
		$link = str_replace( '%_%', $args['format'], $args['base'] );
		$link = str_replace( '%#%', $current + 1, $link );
		if ( $add_args ) {
			$link = add_query_arg( $add_args, $link );
		}
		$link .= $args['add_fragment'];

		$page_links[] = sprintf(
			'<a class="next page-numbers" href="%s">%s</a>',
			/** This filter is documented in wp-includes/general-template.php */
			esc_url( apply_filters( 'paginate_links', $link ) ),
			$args['next_text']
		);
	endif;

	switch ( $args['type'] ) {
		case 'array':
			return $page_links;

		case 'list':
			$r .= "<ul class='page-numbers'>\n\t<li>";
			$r .= implode( "</li>\n\t<li>", $page_links );
			$r .= "</li>\n</ul>\n";
			break;

		default:
			$r = implode( "\n", $page_links );
			break;
	}

	/**
	 * Filters the HTML output of paginated links for archives.
	 *
	 * @since 5.7.0
	 *
	 * @param string $r    HTML output.
	 * @param array  $args An array of arguments. See paginate_links()
	 *                     for information on accepted arguments.
	 */
	$r = apply_filters( 'paginate_links_output', $r, $args );

	return $r;
}