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
orrx-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">←</span> <span>' . esc_html( $args['prev_text'] ) . '</span>',
'next_text' => '<span>' . esc_html( $args['next_text'] ) . '</span> <span aria-hidden="true">→</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">←</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">←</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">→</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">→</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.