I’ll build this as a strong standalone pagination chunk for RX Theme: classic links, AJAX loading, infinite scroll, load-more button, accessibility, history URL updates, caching, and safe WordPress-friendly behavior.
Dear friend, create this file:
assets/static-js/static-chunks-js/chunk-029-pagination.js
Then paste this full advanced pagination code.
/*!
* RX Theme - Chunk 029 Pagination
* File: assets/static-js/static-chunks-js/chunk-029-pagination.js
* Purpose:
* - AJAX pagination
* - Load More button
* - Infinite scroll
* - History URL update
* - Browser back/forward support
* - Accessibility focus management
* - Skeleton loading
* - Error handling
* - Small in-memory cache
* - WordPress archive/search/category/tag support
*/
(function () {
'use strict';
/**
* RX Pagination Namespace
*/
const RXPagination = {
version: '1.0.0',
config: {
contentSelector: '[data-rx-posts], .rx-posts, .site-main, main',
paginationSelector: '[data-rx-pagination], .rx-pagination, .navigation.pagination, nav.pagination',
nextSelector: 'a.next, .next.page-numbers, [rel="next"], [data-rx-next]',
prevSelector: 'a.prev, .prev.page-numbers, [rel="prev"], [data-rx-prev]',
pageLinkSelector: '.page-numbers a, a.page-numbers, [data-rx-page-link]',
loadMoreSelector: '[data-rx-load-more], .rx-load-more',
infiniteRootSelector: '[data-rx-infinite-pagination]',
ajaxContainerSelector: '[data-rx-ajax-container]',
itemSelector: '[data-rx-post-item], article, .post, .rx-card',
skeletonClass: 'rx-pagination-skeleton',
loadingClass: 'rx-pagination-is-loading',
disabledClass: 'rx-pagination-is-disabled',
activeClass: 'rx-pagination-is-active',
errorClass: 'rx-pagination-has-error',
cacheLimit: 20,
scrollOffset: 90,
infiniteScrollOffset: 550,
requestTimeout: 20000,
useHistory: true,
useCache: true,
usePrefetch: true,
useSkeleton: true,
enableAjaxLinks: true,
enableLoadMore: true,
enableInfiniteScroll: true,
debug: false
},
state: {
isLoading: false,
currentUrl: window.location.href,
lastUrl: '',
currentPage: 1,
nextUrl: '',
prevUrl: '',
observer: null,
controller: null,
cache: new Map(),
initialized: false
},
init() {
if (this.state.initialized) return;
this.readConfigFromBody();
this.detectCurrentPage();
this.updatePaginationState();
this.bindPaginationLinks();
this.bindLoadMore();
this.bindInfiniteScroll();
this.bindPopState();
this.prefetchNextPage();
this.state.initialized = true;
this.log('RX Pagination initialized');
},
/**
* Optional body config:
* <body data-rx-pagination-history="false" data-rx-pagination-infinite="true">
*/
readConfigFromBody() {
const body = document.body;
if (!body) return;
if (body.dataset.rxPaginationHistory === 'false') {
this.config.useHistory = false;
}
if (body.dataset.rxPaginationCache === 'false') {
this.config.useCache = false;
}
if (body.dataset.rxPaginationPrefetch === 'false') {
this.config.usePrefetch = false;
}
if (body.dataset.rxPaginationInfinite === 'false') {
this.config.enableInfiniteScroll = false;
}
if (body.dataset.rxPaginationAjaxLinks === 'false') {
this.config.enableAjaxLinks = false;
}
if (body.dataset.rxPaginationDebug === 'true') {
this.config.debug = true;
}
},
log(...args) {
if (this.config.debug) {
console.log('[RX Pagination]', ...args);
}
},
warn(...args) {
if (this.config.debug) {
console.warn('[RX Pagination]', ...args);
}
},
getContentContainer() {
return (
document.querySelector(this.config.ajaxContainerSelector) ||
document.querySelector(this.config.contentSelector)
);
},
getPaginationContainer() {
return document.querySelector(this.config.paginationSelector);
},
updatePaginationState() {
const pagination = this.getPaginationContainer();
if (!pagination) {
this.state.nextUrl = '';
this.state.prevUrl = '';
return;
}
const next = pagination.querySelector(this.config.nextSelector);
const prev = pagination.querySelector(this.config.prevSelector);
this.state.nextUrl = next ? next.href : '';
this.state.prevUrl = prev ? prev.href : '';
},
detectCurrentPage() {
const url = new URL(window.location.href);
let page = 1;
const pagedParam = url.searchParams.get('paged');
if (pagedParam && !Number.isNaN(parseInt(pagedParam, 10))) {
page = parseInt(pagedParam, 10);
}
const pathMatch = url.pathname.match(/\/page\/([0-9]+)\/?/i);
if (pathMatch && pathMatch[1]) {
page = parseInt(pathMatch[1], 10);
}
this.state.currentPage = page || 1;
},
bindPaginationLinks() {
if (!this.config.enableAjaxLinks) return;
document.addEventListener('click', (event) => {
const link = event.target.closest(this.config.pageLinkSelector);
if (!link) return;
if (!this.isValidPaginationLink(link)) return;
event.preventDefault();
this.loadPage(link.href, {
mode: 'replace',
pushState: true,
scroll: true,
focus: true
});
});
},
bindLoadMore() {
if (!this.config.enableLoadMore) return;
document.addEventListener('click', (event) => {
const button = event.target.closest(this.config.loadMoreSelector);
if (!button) return;
event.preventDefault();
const nextUrl = button.dataset.rxNext || this.state.nextUrl;
if (!nextUrl || this.state.isLoading) return;
this.loadPage(nextUrl, {
mode: 'append',
pushState: true,
scroll: false,
focus: false,
trigger: button
});
});
},
bindInfiniteScroll() {
if (!this.config.enableInfiniteScroll) return;
const infiniteRoot = document.querySelector(this.config.infiniteRootSelector);
const pagination = this.getPaginationContainer();
if (!infiniteRoot && !pagination) return;
const sentinel = document.createElement('div');
sentinel.className = 'rx-pagination-sentinel';
sentinel.setAttribute('aria-hidden', 'true');
const target = infiniteRoot || pagination;
target.insertAdjacentElement('afterend', sentinel);
this.state.observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry || !entry.isIntersecting) return;
if (this.state.isLoading) return;
if (!this.state.nextUrl) return;
const root = document.querySelector(this.config.infiniteRootSelector);
if (!root && !document.body.dataset.rxAutoInfinite) {
return;
}
this.loadPage(this.state.nextUrl, {
mode: 'append',
pushState: true,
scroll: false,
focus: false
});
},
{
root: null,
rootMargin: `${this.config.infiniteScrollOffset}px 0px`,
threshold: 0
}
);
this.state.observer.observe(sentinel);
},
bindPopState() {
window.addEventListener('popstate', () => {
this.loadPage(window.location.href, {
mode: 'replace',
pushState: false,
scroll: true,
focus: true
});
});
},
isValidPaginationLink(link) {
if (!link || !link.href) return false;
if (link.getAttribute('aria-disabled') === 'true') return false;
if (link.classList.contains('current')) return false;
if (link.classList.contains(this.config.disabledClass)) return false;
if (link.target && link.target !== '_self') return false;
const url = new URL(link.href, window.location.origin);
if (url.origin !== window.location.origin) return false;
return true;
},
async loadPage(url, options = {}) {
const settings = Object.assign(
{
mode: 'replace',
pushState: true,
scroll: true,
focus: false,
trigger: null
},
options
);
if (!url || this.state.isLoading) return;
const container = this.getContentContainer();
if (!container) {
this.warn('Content container not found.');
window.location.href = url;
return;
}
this.state.isLoading = true;
this.state.lastUrl = this.state.currentUrl;
this.setLoading(true, settings.trigger);
try {
let html;
if (this.config.useCache && this.state.cache.has(url)) {
html = this.state.cache.get(url);
this.log('Loaded from cache:', url);
} else {
html = await this.fetchPage(url);
this.addToCache(url, html);
}
const doc = this.parseHTML(html);
this.renderPage(doc, url, settings);
this.state.currentUrl = url;
this.detectCurrentPageFromUrl(url);
this.updatePaginationState();
if (settings.pushState && this.config.useHistory) {
history.pushState(
{
rxPagination: true,
url
},
'',
url
);
}
if (settings.scroll) {
this.scrollToContainer();
}
if (settings.focus) {
this.focusContainer();
}
this.prefetchNextPage();
this.dispatchEvent('rx:pagination:loaded', {
url,
mode: settings.mode,
page: this.state.currentPage
});
} catch (error) {
this.handleError(error, url);
} finally {
this.state.isLoading = false;
this.setLoading(false, settings.trigger);
}
},
fetchPage(url) {
if (this.state.controller) {
this.state.controller.abort();
}
this.state.controller = new AbortController();
const timeout = setTimeout(() => {
this.state.controller.abort();
}, this.config.requestTimeout);
return fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'text/html,application/xhtml+xml'
},
signal: this.state.controller.signal
})
.then((response) => {
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.text();
})
.catch((error) => {
clearTimeout(timeout);
throw error;
});
},
parseHTML(html) {
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
},
renderPage(doc, url, settings) {
const currentContainer = this.getContentContainer();
const newContainer =
doc.querySelector(this.config.ajaxContainerSelector) ||
doc.querySelector(this.config.contentSelector);
const currentPagination = this.getPaginationContainer();
const newPagination = doc.querySelector(this.config.paginationSelector);
if (!currentContainer || !newContainer) {
throw new Error('Pagination render container missing.');
}
if (settings.mode === 'append') {
this.appendItems(currentContainer, newContainer);
} else {
currentContainer.innerHTML = newContainer.innerHTML;
}
if (currentPagination && newPagination) {
currentPagination.innerHTML = newPagination.innerHTML;
} else if (currentPagination && !newPagination) {
currentPagination.remove();
} else if (!currentPagination && newPagination) {
currentContainer.insertAdjacentElement('afterend', newPagination.cloneNode(true));
}
this.updateDocumentMeta(doc);
this.markActivePagination(url);
this.lazyRefresh();
},
appendItems(currentContainer, newContainer) {
const newItems = newContainer.querySelectorAll(this.config.itemSelector);
if (!newItems.length) {
currentContainer.insertAdjacentHTML('beforeend', newContainer.innerHTML);
return;
}
const fragment = document.createDocumentFragment();
newItems.forEach((item) => {
const clone = item.cloneNode(true);
clone.classList.add('rx-pagination-new-item');
fragment.appendChild(clone);
});
currentContainer.appendChild(fragment);
requestAnimationFrame(() => {
currentContainer
.querySelectorAll('.rx-pagination-new-item')
.forEach((item) => {
item.classList.remove('rx-pagination-new-item');
});
});
},
updateDocumentMeta(doc) {
const newTitle = doc.querySelector('title');
if (newTitle && newTitle.textContent) {
document.title = newTitle.textContent;
}
this.replaceMetaTag(doc, 'meta[name="description"]');
this.replaceMetaTag(doc, 'link[rel="canonical"]');
this.replaceMetaTag(doc, 'link[rel="prev"]');
this.replaceMetaTag(doc, 'link[rel="next"]');
},
replaceMetaTag(doc, selector) {
const oldTag = document.head.querySelector(selector);
const newTag = doc.head.querySelector(selector);
if (oldTag && newTag) {
oldTag.replaceWith(newTag.cloneNode(true));
} else if (!oldTag && newTag) {
document.head.appendChild(newTag.cloneNode(true));
} else if (oldTag && !newTag) {
oldTag.remove();
}
},
markActivePagination(url) {
const pagination = this.getPaginationContainer();
if (!pagination) return;
pagination.querySelectorAll('a').forEach((link) => {
link.classList.remove(this.config.activeClass);
link.removeAttribute('aria-current');
if (link.href === url) {
link.classList.add(this.config.activeClass);
link.setAttribute('aria-current', 'page');
}
});
},
setLoading(isLoading, trigger) {
const body = document.body;
const container = this.getContentContainer();
const pagination = this.getPaginationContainer();
body.classList.toggle(this.config.loadingClass, isLoading);
if (container) {
container.classList.toggle(this.config.loadingClass, isLoading);
container.setAttribute('aria-busy', isLoading ? 'true' : 'false');
}
if (pagination) {
pagination.classList.toggle(this.config.loadingClass, isLoading);
}
if (trigger) {
trigger.classList.toggle(this.config.loadingClass, isLoading);
trigger.disabled = isLoading;
trigger.setAttribute('aria-busy', isLoading ? 'true' : 'false');
const loadingText = trigger.dataset.rxLoadingText;
const defaultText = trigger.dataset.rxDefaultText || trigger.textContent;
if (!trigger.dataset.rxDefaultText) {
trigger.dataset.rxDefaultText = defaultText;
}
if (isLoading && loadingText) {
trigger.textContent = loadingText;
}
if (!isLoading) {
trigger.textContent = trigger.dataset.rxDefaultText;
}
}
if (this.config.useSkeleton) {
this.toggleSkeleton(isLoading);
}
},
toggleSkeleton(show) {
const container = this.getContentContainer();
if (!container) return;
let skeleton = document.querySelector(`.${this.config.skeletonClass}`);
if (show) {
if (skeleton) return;
skeleton = document.createElement('div');
skeleton.className = this.config.skeletonClass;
skeleton.setAttribute('aria-hidden', 'true');
skeleton.innerHTML = `
<div class="rx-skeleton-card"></div>
<div class="rx-skeleton-card"></div>
<div class="rx-skeleton-card"></div>
`;
container.insertAdjacentElement('beforebegin', skeleton);
} else if (skeleton) {
skeleton.remove();
}
},
scrollToContainer() {
const container = this.getContentContainer();
if (!container) return;
const top =
container.getBoundingClientRect().top +
window.pageYOffset -
this.config.scrollOffset;
window.scrollTo({
top: Math.max(top, 0),
behavior: 'smooth'
});
},
focusContainer() {
const container = this.getContentContainer();
if (!container) return;
if (!container.hasAttribute('tabindex')) {
container.setAttribute('tabindex', '-1');
}
container.focus({
preventScroll: true
});
},
detectCurrentPageFromUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
let page = 1;
const param = parsed.searchParams.get('paged');
if (param) {
page = parseInt(param, 10);
}
const pathMatch = parsed.pathname.match(/\/page\/([0-9]+)\/?/i);
if (pathMatch && pathMatch[1]) {
page = parseInt(pathMatch[1], 10);
}
this.state.currentPage = page || 1;
} catch (error) {
this.state.currentPage = 1;
}
},
addToCache(url, html) {
if (!this.config.useCache) return;
if (this.state.cache.has(url)) {
this.state.cache.delete(url);
}
this.state.cache.set(url, html);
while (this.state.cache.size > this.config.cacheLimit) {
const firstKey = this.state.cache.keys().next().value;
this.state.cache.delete(firstKey);
}
},
prefetchNextPage() {
if (!this.config.usePrefetch) return;
if (!this.state.nextUrl) return;
if (this.state.cache.has(this.state.nextUrl)) return;
const nextUrl = this.state.nextUrl;
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
this.prefetchUrl(nextUrl);
});
} else {
setTimeout(() => {
this.prefetchUrl(nextUrl);
}, 1200);
}
},
async prefetchUrl(url) {
if (!url || this.state.cache.has(url)) return;
try {
const html = await fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'text/html'
}
}).then((response) => {
if (!response.ok) throw new Error('Prefetch failed');
return response.text();
});
this.addToCache(url, html);
this.log('Prefetched:', url);
} catch (error) {
this.warn('Prefetch error:', error);
}
},
lazyRefresh() {
/**
* Refresh native lazy loading, third-party lazy libraries,
* masonry/grid plugins, ads, analytics hooks if available.
*/
document.querySelectorAll('img[data-src]').forEach((img) => {
if (!img.getAttribute('src')) {
img.setAttribute('src', img.dataset.src);
}
});
if (window.lazySizes && typeof window.lazySizes.autoSizer === 'object') {
window.lazySizes.autoSizer.checkElems();
}
if (window.Masonry && document.querySelector('.rx-masonry')) {
document.querySelectorAll('.rx-masonry').forEach((grid) => {
try {
const masonry = Masonry.data(grid);
if (masonry) masonry.reloadItems();
} catch (error) {
this.warn('Masonry refresh failed:', error);
}
});
}
if (window.rxTheme && typeof window.rxTheme.refresh === 'function') {
window.rxTheme.refresh();
}
this.dispatchEvent('rx:pagination:refresh');
},
handleError(error, url) {
this.warn('Pagination error:', error);
const body = document.body;
const pagination = this.getPaginationContainer();
body.classList.add(this.config.errorClass);
if (pagination) {
pagination.classList.add(this.config.errorClass);
}
this.showErrorMessage(url);
this.dispatchEvent('rx:pagination:error', {
error,
url
});
},
showErrorMessage(url) {
const pagination = this.getPaginationContainer();
if (!pagination) return;
let errorBox = pagination.querySelector('.rx-pagination-error-message');
if (!errorBox) {
errorBox = document.createElement('div');
errorBox.className = 'rx-pagination-error-message';
errorBox.setAttribute('role', 'alert');
pagination.appendChild(errorBox);
}
errorBox.innerHTML = `
<p>Pagination loading failed. Please try again.</p>
<a href="${this.escapeAttr(url)}">Open this page normally</a>
`;
},
escapeAttr(value) {
return String(value)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
},
dispatchEvent(name, detail = {}) {
document.dispatchEvent(
new CustomEvent(name, {
bubbles: true,
detail
})
);
},
destroy() {
if (this.state.observer) {
this.state.observer.disconnect();
this.state.observer = null;
}
if (this.state.controller) {
this.state.controller.abort();
this.state.controller = null;
}
this.state.cache.clear();
this.state.initialized = false;
}
};
/**
* Public API
*/
window.RXThemePagination = RXPagination;
/**
* Auto init
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => RXPagination.init());
} else {
RXPagination.init();
}
})();
Add this small CSS in your theme CSS file, for example:
assets/css/components/pagination.css
.rx-pagination-is-loading {
pointer-events: none;
opacity: 0.75;
}
.rx-pagination-skeleton {
display: grid;
gap: 1rem;
margin-bottom: 1.5rem;
}
.rx-skeleton-card {
min-height: 160px;
border-radius: 14px;
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.06),
rgba(0, 0, 0, 0.11),
rgba(0, 0, 0, 0.06)
);
background-size: 200% 100%;
animation: rxSkeletonLoading 1.2s linear infinite;
}
.rx-pagination-new-item {
animation: rxPaginationFadeIn 0.35s ease both;
}
.rx-pagination-error-message {
margin-top: 1rem;
padding: 1rem;
border-radius: 12px;
background: #fff4f4;
border: 1px solid #ffd0d0;
}
.rx-pagination-error-message p {
margin: 0 0 0.5rem;
}
.rx-pagination-is-active,
.page-numbers.current {
font-weight: 700;
}
.rx-pagination-sentinel {
width: 100%;
height: 1px;
}
@keyframes rxSkeletonLoading {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
@keyframes rxPaginationFadeIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Use this HTML structure in archive/category/search templates:
<main id="primary" class="site-main rx-site-main">
<div class="rx-posts" data-rx-ajax-container data-rx-posts>
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<article <?php post_class('rx-card'); ?> data-rx-post-item>
<h2>
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h2>
<div class="rx-excerpt">
<?php the_excerpt(); ?>
</div>
</article>
<?php endwhile; ?>
<?php endif; ?>
</div>
<nav class="rx-pagination" data-rx-pagination aria-label="Posts pagination">
<?php
the_posts_pagination(array(
'mid_size' => 2,
'prev_text' => esc_html__('Previous', 'rx-theme'),
'next_text' => esc_html__('Next', 'rx-theme'),
));
?>
</nav>
<button
class="rx-load-more"
data-rx-load-more
data-rx-loading-text="Loading..."
type="button"
>
Load More
</button>
</main>
Enqueue it in functions.php or your enqueue file:
function rx_theme_enqueue_pagination_chunk() {
wp_enqueue_script(
'rx-chunk-029-pagination',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-029-pagination.js',
array(),
wp_get_theme()->get('Version'),
true
);
}
add_action('wp_enqueue_scripts', 'rx_theme_enqueue_pagination_chunk');
For automatic infinite scroll, add this attribute to body or wrapper:
<div data-rx-infinite-pagination>
or add this in body:
<body data-rx-auto-infinite="true">
Best practical setup for RX Theme:
<div class="rx-posts" data-rx-ajax-container data-rx-posts>
Each post/card:
<article data-rx-post-item>
Pagination wrapper:
<nav data-rx-pagination>
Load more button:
<button data-rx-load-more data-rx-loading-text="Loading...">Load More</button>