pagination.php

I’ll build this as a reusable pagination.php helper for your RX theme: numbered pagination, prev/next, accessibility, AJAX/load-more support hooks, schema-safe markup, Bootstrap/Tailwind-friendly classes, and WordPress fallback compatibility.

Yes, friend. Create this file:

rx-theme/template-parts/pagination.php
or
rx-theme/inc/pagination.php

Then include it in functions.php:

require_once get_template_directory() . '/inc/pagination.php';

WordPress officially supports numbered pagination through paginate_links(), and the current page is usually read with get_query_var( 'paged', 1 ). For static front-page templates, WordPress may use page instead of paged.

Advanced pagination.php for RX Theme

<?php
/**
 * RX Theme Advanced Pagination System
 *
 * File: inc/pagination.php
 *
 * Features:
 * - Numbered pagination
 * - Previous / Next buttons
 * - First / Last page links
 * - Mobile-friendly compact mode
 * - Screen-reader accessible labels
 * - Works with main query and custom WP_Query
 * - Supports archive, search, category, tag, author, CPT archive
 * - Supports static front page pagination
 * - Supports query-string preservation
 * - Supports AJAX/load-more attributes
 * - Supports Bootstrap/Tailwind/custom CSS classes
 * - Adds schema-friendly navigation markup
 * - Includes fallback older/newer links
 *
 * @package RX_Theme
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Get current pagination page safely.
 *
 * @return int
 */
if ( ! function_exists( 'rx_get_current_page_number' ) ) {
	function rx_get_current_page_number() {
		$paged = get_query_var( 'paged' );

		if ( ! $paged ) {
			$paged = get_query_var( 'page' );
		}

		if ( ! $paged ) {
			$paged = 1;
		}

		return max( 1, absint( $paged ) );
	}
}

/**
 * Get total pages from query.
 *
 * @param WP_Query|null $query Query object.
 * @return int
 */
if ( ! function_exists( 'rx_get_total_pages' ) ) {
	function rx_get_total_pages( $query = null ) {
		global $wp_query;

		if ( ! $query || ! $query instanceof WP_Query ) {
			$query = $wp_query;
		}

		$total = isset( $query->max_num_pages ) ? absint( $query->max_num_pages ) : 1;

		return max( 1, $total );
	}
}

/**
 * Preserve safe query args during pagination.
 *
 * Example:
 * /search/?s=test&sort=latest
 *
 * @return array
 */
if ( ! function_exists( 'rx_get_pagination_query_args' ) ) {
	function rx_get_pagination_query_args() {
		$allowed_args = apply_filters(
			'rx_pagination_allowed_query_args',
			array(
				's',
				'post_type',
				'orderby',
				'order',
				'cat',
				'tag',
				'author',
				'year',
				'monthnum',
				'day',
				'filter',
				'sort',
			)
		);

		$args = array();

		foreach ( $allowed_args as $arg ) {
			if ( isset( $_GET[ $arg ] ) ) {
				$args[ sanitize_key( $arg ) ] = sanitize_text_field( wp_unslash( $_GET[ $arg ] ) );
			}
		}

		return $args;
	}
}

/**
 * Main RX numbered pagination.
 *
 * Usage:
 * rx_pagination();
 *
 * Custom query usage:
 * $custom_query = new WP_Query( $args );
 * rx_pagination( array( 'query' => $custom_query ) );
 *
 * @param array $args Pagination arguments.
 * @return void
 */
if ( ! function_exists( 'rx_pagination' ) ) {
	function rx_pagination( $args = array() ) {
		global $wp_query;

		$defaults = array(
			'query'              => $wp_query,
			'current'            => rx_get_current_page_number(),
			'total'              => null,
			'mid_size'           => 2,
			'end_size'           => 1,
			'show_all'           => false,
			'prev_next'          => true,
			'first_last'         => true,
			'show_page_count'    => true,
			'show_mobile_simple' => true,
			'preserve_query'     => true,
			'ajax'               => false,
			'ajax_container'     => '#rx-posts-wrapper',
			'ajax_action'        => 'rx_ajax_pagination',
			'aria_label'         => esc_html__( 'Posts pagination', 'rx-theme' ),
			'before'             => '',
			'after'              => '',
			'wrapper_class'      => 'rx-pagination-wrapper',
			'nav_class'          => 'rx-pagination',
			'list_class'         => 'rx-pagination__list',
			'item_class'         => 'rx-pagination__item',
			'link_class'         => 'rx-pagination__link',
			'current_class'      => 'rx-pagination__link rx-pagination__link--current',
			'disabled_class'     => 'rx-pagination__link rx-pagination__link--disabled',
			'prev_text'          => esc_html__( 'Previous', 'rx-theme' ),
			'next_text'          => esc_html__( 'Next', 'rx-theme' ),
			'first_text'         => esc_html__( 'First', 'rx-theme' ),
			'last_text'          => esc_html__( 'Last', 'rx-theme' ),
			'screen_reader_text' => esc_html__( 'Pagination', 'rx-theme' ),
		);

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

		$query = $args['query'];

		if ( ! $query || ! $query instanceof WP_Query ) {
			$query = $wp_query;
		}

		$total = $args['total'] ? absint( $args['total'] ) : rx_get_total_pages( $query );

		if ( $total <= 1 ) {
			return;
		}

		$current = max( 1, absint( $args['current'] ) );

		$big = 999999999;

		$base = str_replace(
			$big,
			'%#%',
			esc_url( get_pagenum_link( $big ) )
		);

		$add_args = false;

		if ( ! empty( $args['preserve_query'] ) ) {
			$add_args = rx_get_pagination_query_args();
		}

		$pagination_args = array(
			'base'      => $base,
			'format'    => '',
			'current'   => $current,
			'total'     => $total,
			'show_all'  => (bool) $args['show_all'],
			'end_size'  => absint( $args['end_size'] ),
			'mid_size'  => absint( $args['mid_size'] ),
			'prev_next' => (bool) $args['prev_next'],
			'prev_text' => '<span aria-hidden="true">&larr;</span> <span>' . esc_html( $args['prev_text'] ) . '</span>',
			'next_text' => '<span>' . esc_html( $args['next_text'] ) . '</span> <span aria-hidden="true">&rarr;</span>',
			'type'      => 'array',
			'add_args'  => $add_args,
		);

		$links = paginate_links( apply_filters( 'rx_paginate_links_args', $pagination_args, $args, $query ) );

		if ( empty( $links ) || ! is_array( $links ) ) {
			rx_pagination_fallback();
			return;
		}

		$ajax_attrs = '';

		if ( ! empty( $args['ajax'] ) ) {
			$ajax_attrs .= ' data-rx-pagination="ajax"';
			$ajax_attrs .= ' data-rx-container="' . esc_attr( $args['ajax_container'] ) . '"';
			$ajax_attrs .= ' data-rx-action="' . esc_attr( $args['ajax_action'] ) . '"';
			$ajax_attrs .= ' data-rx-current="' . esc_attr( $current ) . '"';
			$ajax_attrs .= ' data-rx-total="' . esc_attr( $total ) . '"';
		}

		echo wp_kses_post( $args['before'] );

		echo '<div class="' . esc_attr( $args['wrapper_class'] ) . '">';

		if ( ! empty( $args['screen_reader_text'] ) ) {
			echo '<h2 class="screen-reader-text">' . esc_html( $args['screen_reader_text'] ) . '</h2>';
		}

		if ( ! empty( $args['show_page_count'] ) ) {
			echo '<div class="rx-pagination__count" aria-live="polite">';
			printf(
				esc_html__( 'Page %1$s of %2$s', 'rx-theme' ),
				esc_html( number_format_i18n( $current ) ),
				esc_html( number_format_i18n( $total ) )
			);
			echo '</div>';
		}

		if ( ! empty( $args['show_mobile_simple'] ) ) {
			echo '<div class="rx-pagination__mobile">';

			if ( $current > 1 ) {
				echo '<a class="' . esc_attr( $args['link_class'] ) . ' rx-pagination__mobile-prev" href="' . esc_url( get_pagenum_link( $current - 1 ) ) . '">';
				echo '<span aria-hidden="true">&larr;</span> ' . esc_html( $args['prev_text'] );
				echo '</a>';
			} else {
				echo '<span class="' . esc_attr( $args['disabled_class'] ) . ' rx-pagination__mobile-prev" aria-disabled="true">';
				echo '<span aria-hidden="true">&larr;</span> ' . esc_html( $args['prev_text'] );
				echo '</span>';
			}

			echo '<span class="rx-pagination__mobile-count">';
			printf(
				esc_html__( '%1$s / %2$s', 'rx-theme' ),
				esc_html( number_format_i18n( $current ) ),
				esc_html( number_format_i18n( $total ) )
			);
			echo '</span>';

			if ( $current < $total ) {
				echo '<a class="' . esc_attr( $args['link_class'] ) . ' rx-pagination__mobile-next" href="' . esc_url( get_pagenum_link( $current + 1 ) ) . '">';
				echo esc_html( $args['next_text'] ) . ' <span aria-hidden="true">&rarr;</span>';
				echo '</a>';
			} else {
				echo '<span class="' . esc_attr( $args['disabled_class'] ) . ' rx-pagination__mobile-next" aria-disabled="true">';
				echo esc_html( $args['next_text'] ) . ' <span aria-hidden="true">&rarr;</span>';
				echo '</span>';
			}

			echo '</div>';
		}

		echo '<nav class="' . esc_attr( $args['nav_class'] ) . '" aria-label="' . esc_attr( $args['aria_label'] ) . '"' . $ajax_attrs . ' itemscope itemtype="https://schema.org/SiteNavigationElement">';
		echo '<ul class="' . esc_attr( $args['list_class'] ) . '">';

		if ( ! empty( $args['first_last'] ) ) {
			if ( $current > 1 ) {
				echo '<li class="' . esc_attr( $args['item_class'] ) . ' rx-pagination__first">';
				echo '<a class="' . esc_attr( $args['link_class'] ) . '" href="' . esc_url( get_pagenum_link( 1 ) ) . '" aria-label="' . esc_attr__( 'Go to first page', 'rx-theme' ) . '">';
				echo esc_html( $args['first_text'] );
				echo '</a>';
				echo '</li>';
			} else {
				echo '<li class="' . esc_attr( $args['item_class'] ) . ' rx-pagination__first is-disabled">';
				echo '<span class="' . esc_attr( $args['disabled_class'] ) . '" aria-disabled="true">';
				echo esc_html( $args['first_text'] );
				echo '</span>';
				echo '</li>';
			}
		}

		foreach ( $links as $link ) {
			$is_current = false;

			if ( false !== strpos( $link, 'current' ) ) {
				$is_current = true;
			}

			$link = str_replace( 'page-numbers', esc_attr( $args['link_class'] ), $link );
			$link = str_replace( 'current', esc_attr( $args['current_class'] ) . ' current', $link );
			$link = str_replace( 'dots', 'rx-pagination__dots dots', $link );

			if ( $is_current ) {
				$link = str_replace( '<span', '<span aria-current="page"', $link );
			}

			echo '<li class="' . esc_attr( $args['item_class'] ) . ( $is_current ? ' is-current' : '' ) . '">';
			echo wp_kses_post( $link );
			echo '</li>';
		}

		if ( ! empty( $args['first_last'] ) ) {
			if ( $current < $total ) {
				echo '<li class="' . esc_attr( $args['item_class'] ) . ' rx-pagination__last">';
				echo '<a class="' . esc_attr( $args['link_class'] ) . '" href="' . esc_url( get_pagenum_link( $total ) ) . '" aria-label="' . esc_attr__( 'Go to last page', 'rx-theme' ) . '">';
				echo esc_html( $args['last_text'] );
				echo '</a>';
				echo '</li>';
			} else {
				echo '<li class="' . esc_attr( $args['item_class'] ) . ' rx-pagination__last is-disabled">';
				echo '<span class="' . esc_attr( $args['disabled_class'] ) . '" aria-disabled="true">';
				echo esc_html( $args['last_text'] );
				echo '</span>';
				echo '</li>';
			}
		}

		echo '</ul>';
		echo '</nav>';

		echo '</div>';

		echo wp_kses_post( $args['after'] );
	}
}

/**
 * Simple older/newer fallback pagination.
 *
 * @return void
 */
if ( ! function_exists( 'rx_pagination_fallback' ) ) {
	function rx_pagination_fallback() {
		if ( ! get_next_posts_link() && ! get_previous_posts_link() ) {
			return;
		}
		?>
		<nav class="rx-pagination-fallback" aria-label="<?php esc_attr_e( 'Posts navigation', 'rx-theme' ); ?>">
			<div class="rx-pagination-fallback__older">
				<?php next_posts_link( esc_html__( 'Older Posts', 'rx-theme' ) ); ?>
			</div>

			<div class="rx-pagination-fallback__newer">
				<?php previous_posts_link( esc_html__( 'Newer Posts', 'rx-theme' ) ); ?>
			</div>
		</nav>
		<?php
	}
}

/**
 * Previous and next post navigation for single.php.
 *
 * Usage:
 * rx_single_post_navigation();
 *
 * @return void
 */
if ( ! function_exists( 'rx_single_post_navigation' ) ) {
	function rx_single_post_navigation() {
		if ( ! is_single() ) {
			return;
		}

		the_post_navigation(
			array(
				'prev_text' => '<span class="rx-post-nav__label">' . esc_html__( 'Previous Post', 'rx-theme' ) . '</span><span class="rx-post-nav__title">%title</span>',
				'next_text' => '<span class="rx-post-nav__label">' . esc_html__( 'Next Post', 'rx-theme' ) . '</span><span class="rx-post-nav__title">%title</span>',
				'class'     => 'rx-single-post-navigation',
			)
		);
	}
}

/**
 * Pagination for comments.
 *
 * Usage in comments.php:
 * rx_comment_pagination();
 *
 * @return void
 */
if ( ! function_exists( 'rx_comment_pagination' ) ) {
	function rx_comment_pagination() {
		if ( ! get_comment_pages_count() || get_comment_pages_count() <= 1 ) {
			return;
		}

		?>
		<nav class="rx-comment-pagination" aria-label="<?php esc_attr_e( 'Comments pagination', 'rx-theme' ); ?>">
			<?php
			paginate_comments_links(
				array(
					'prev_text' => esc_html__( 'Previous Comments', 'rx-theme' ),
					'next_text' => esc_html__( 'Next Comments', 'rx-theme' ),
					'type'      => 'list',
				)
			);
			?>
		</nav>
		<?php
	}
}

/**
 * Load more button.
 *
 * This is only the button markup.
 * AJAX JavaScript and PHP handler can be added separately.
 *
 * Usage:
 * rx_load_more_button();
 *
 * @param array $args Button args.
 * @return void
 */
if ( ! function_exists( 'rx_load_more_button' ) ) {
	function rx_load_more_button( $args = array() ) {
		global $wp_query;

		$defaults = array(
			'query'      => $wp_query,
			'current'    => rx_get_current_page_number(),
			'total'      => null,
			'button_text'=> esc_html__( 'Load More', 'rx-theme' ),
			'class'      => 'rx-load-more-button',
			'container'  => '#rx-posts-wrapper',
			'action'     => 'rx_load_more_posts',
		);

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

		$total = $args['total'] ? absint( $args['total'] ) : rx_get_total_pages( $args['query'] );
		$current = max( 1, absint( $args['current'] ) );

		if ( $current >= $total ) {
			return;
		}

		?>
		<button
			type="button"
			class="<?php echo esc_attr( $args['class'] ); ?>"
			data-rx-load-more="true"
			data-rx-current="<?php echo esc_attr( $current ); ?>"
			data-rx-next="<?php echo esc_attr( $current + 1 ); ?>"
			data-rx-total="<?php echo esc_attr( $total ); ?>"
			data-rx-container="<?php echo esc_attr( $args['container'] ); ?>"
			data-rx-action="<?php echo esc_attr( $args['action'] ); ?>"
		>
			<?php echo esc_html( $args['button_text'] ); ?>
		</button>
		<?php
	}
}

/**
 * Pagination JSON data for JavaScript apps.
 *
 * Usage:
 * $data = rx_get_pagination_data();
 *
 * @param WP_Query|null $query Query object.
 * @return array
 */
if ( ! function_exists( 'rx_get_pagination_data' ) ) {
	function rx_get_pagination_data( $query = null ) {
		global $wp_query;

		if ( ! $query || ! $query instanceof WP_Query ) {
			$query = $wp_query;
		}

		$current = rx_get_current_page_number();
		$total   = rx_get_total_pages( $query );

		return array(
			'current'       => $current,
			'total'         => $total,
			'has_previous'  => $current > 1,
			'has_next'      => $current < $total,
			'previous_page' => $current > 1 ? $current - 1 : null,
			'next_page'     => $current < $total ? $current + 1 : null,
			'first_page'    => 1,
			'last_page'     => $total,
		);
	}
}

/**
 * Print pagination JSON.
 *
 * Usage:
 * rx_print_pagination_json();
 *
 * @param WP_Query|null $query Query object.
 * @return void
 */
if ( ! function_exists( 'rx_print_pagination_json' ) ) {
	function rx_print_pagination_json( $query = null ) {
		$data = rx_get_pagination_data( $query );

		echo '<script type="application/json" class="rx-pagination-json">';
		echo wp_json_encode( $data );
		echo '</script>';
	}
}

/**
 * Add rel prev/next links in <head>.
 *
 * Hooked automatically.
 *
 * @return void
 */
if ( ! function_exists( 'rx_pagination_head_links' ) ) {
	function rx_pagination_head_links() {
		if ( is_singular() ) {
			return;
		}

		global $wp_query;

		$current = rx_get_current_page_number();
		$total   = rx_get_total_pages( $wp_query );

		if ( $total <= 1 ) {
			return;
		}

		if ( $current > 1 ) {
			echo '<link rel="prev" href="' . esc_url( get_pagenum_link( $current - 1 ) ) . '">' . "\n";
		}

		if ( $current < $total ) {
			echo '<link rel="next" href="' . esc_url( get_pagenum_link( $current + 1 ) ) . '">' . "\n";
		}
	}
}
add_action( 'wp_head', 'rx_pagination_head_links', 5 );

/**
 * Optional body class for paginated pages.
 *
 * @param array $classes Body classes.
 * @return array
 */
if ( ! function_exists( 'rx_pagination_body_classes' ) ) {
	function rx_pagination_body_classes( $classes ) {
		$current = rx_get_current_page_number();

		if ( $current > 1 ) {
			$classes[] = 'rx-is-paginated';
			$classes[] = 'rx-page-' . absint( $current );
		}

		return $classes;
	}
}
add_filter( 'body_class', 'rx_pagination_body_classes' );

Add this CSS in style.css

/* RX Advanced Pagination */

.rx-pagination-wrapper {
	margin: 40px 0;
	text-align: center;
}

.rx-pagination__count {
	margin-bottom: 14px;
	font-size: 14px;
	opacity: 0.75;
}

.rx-pagination {
	display: flex;
	justify-content: center;
	width: 100%;
}

.rx-pagination__list {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 8px;
	margin: 0;
	padding: 0;
	list-style: none;
}

.rx-pagination__item {
	margin: 0;
	padding: 0;
}

.rx-pagination__link,
.rx-pagination__link:visited {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 42px;
	min-height: 42px;
	padding: 8px 14px;
	border: 1px solid currentColor;
	border-radius: 8px;
	text-decoration: none;
	font-weight: 600;
	line-height: 1.2;
	transition: transform 0.2s ease, opacity 0.2s ease;
}

.rx-pagination__link:hover,
.rx-pagination__link:focus {
	transform: translateY(-2px);
	text-decoration: none;
}

.rx-pagination__link--current,
.rx-pagination__item.is-current .rx-pagination__link {
	background: currentColor;
	color: #fff;
	pointer-events: none;
}

.rx-pagination__link--disabled,
.rx-pagination__item.is-disabled .rx-pagination__link {
	opacity: 0.4;
	pointer-events: none;
	cursor: not-allowed;
}

.rx-pagination__dots {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 36px;
	min-height: 36px;
	opacity: 0.7;
}

.rx-pagination__mobile {
	display: none;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 16px;
}

.rx-pagination__mobile-count {
	font-weight: 700;
}

.rx-pagination-fallback {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	margin: 40px 0;
}

.rx-load-more-button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 12px 22px;
	border: 1px solid currentColor;
	border-radius: 8px;
	background: transparent;
	cursor: pointer;
	font-weight: 700;
}

.rx-load-more-button:hover,
.rx-load-more-button:focus {
	transform: translateY(-2px);
}

.rx-single-post-navigation {
	margin: 40px 0;
}

.rx-post-nav__label {
	display: block;
	font-size: 13px;
	opacity: 0.7;
}

.rx-post-nav__title {
	display: block;
	font-weight: 700;
}

.rx-comment-pagination {
	margin: 30px 0;
}

@media (max-width: 600px) {
	.rx-pagination__mobile {
		display: flex;
	}

	.rx-pagination {
		display: none;
	}

	.rx-pagination__link,
	.rx-pagination__link:visited {
		min-width: 38px;
		min-height: 38px;
		padding: 7px 12px;
		font-size: 14px;
	}
}

Use in index.php, archive.php, category.php, search.php

Place this after the loop:

<?php rx_pagination(); ?>

Example:

<?php if ( have_posts() ) : ?>

	<div id="rx-posts-wrapper" class="rx-posts-wrapper">
		<?php
		while ( have_posts() ) :
			the_post();

			get_template_part( 'template-parts/content', get_post_type() );

		endwhile;
		?>
	</div>

	<?php rx_pagination(); ?>

<?php else : ?>

	<?php get_template_part( 'template-parts/content', 'none' ); ?>

<?php endif; ?>

Use with custom WP_Query

<?php
$paged = rx_get_current_page_number();

$rx_query = new WP_Query(
	array(
		'post_type'      => 'post',
		'posts_per_page' => 10,
		'paged'          => $paged,
	)
);

if ( $rx_query->have_posts() ) :
	while ( $rx_query->have_posts() ) :
		$rx_query->the_post();

		get_template_part( 'template-parts/content', get_post_type() );

	endwhile;

	rx_pagination(
		array(
			'query'   => $rx_query,
			'current' => $paged,
		)
	);

	wp_reset_postdata();
endif;
?>

Use in single.php

<?php rx_single_post_navigation(); ?>

Use in comments.php

<?php rx_comment_pagination(); ?>

This is a strong advanced base for your RX theme pagination system. It keeps the theme lightweight and avoids needing a pagination plugin.

Leave a Reply

Your email address will not be published. Required fields are marked *