📄 Pagination & Navigation
Master WordPress pagination techniques for better user experience
Learn numbered pagination, infinite scroll, and archive navigation
Learning Objectives
- Understand WordPress pagination fundamentals
- Implement numbered pagination with paginate_links()
- Create previous/next navigation
- Build custom pagination for WP_Query
- Implement AJAX load more functionality
- Create infinite scroll pagination
- Style pagination for different designs
- Handle pagination for custom post types and taxonomies
Understanding WordPress Pagination
Pagination is essential for breaking up large amounts of content into manageable chunks. WordPress provides several methods for implementing pagination, each with its own use cases.
Common Pagination Styles
- « Previous
- 1
- 2
- 3
- ...
- 10
- Next »
Key Concept
WordPress Pagination Methods
paginate_links()
Creates numbered pagination with customizable format
Most Flexiblethe_posts_pagination()
Built-in numbered pagination (WordPress 4.1+)
Easiestprevious/next_posts_link()
Simple previous/next navigation
Basicposts_nav_link()
Combined previous/next with separator
Simpleget_the_posts_navigation()
Returns navigation markup for customization
CustomizableCustom AJAX
Load more or infinite scroll
AdvancedBasic Previous/Next Navigation
Simple Navigation Links
<?php
// In archive.php or index.php
if ( have_posts() ) :
while ( have_posts() ) : the_post();
// Display posts
endwhile;
// Simple previous/next links
?>
<div class="navigation">
<div class="nav-previous">
<?php next_posts_link( '← Older posts' ); ?>
</div>
<div class="nav-next">
<?php previous_posts_link( 'Newer posts →' ); ?>
</div>
</div>
<?php
// Or use combined navigation
posts_nav_link( ' · ', 'Newer posts →', '← Older posts' );
endif;
?>
Note: next_posts_link() shows older posts (confusing naming!). The "next" refers to the next page of results, which contains older posts.
Numbered Pagination with paginate_links()
Complete Numbered Pagination
<?php
// Get current page
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
// Custom query
$args = array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) :
while ( $query->have_posts() ) : $query->the_post();
// Display posts
endwhile;
// Pagination
$big = 999999999; // need an unlikely integer
$pagination_args = array(
'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
'format' => '?paged=%#%',
'current' => max( 1, $paged ),
'total' => $query->max_num_pages,
'prev_text' => __('« Previous'),
'next_text' => __('Next »'),
'type' => 'list',
'end_size' => 3,
'mid_size' => 2
);
echo '<nav class="pagination">';
echo paginate_links( $pagination_args );
echo '</nav>';
wp_reset_postdata();
endif;
?>
paginate_links() Parameters
- base: URL structure for pagination links
- format: Format for page number in URL
- current: Current page number
- total: Total number of pages
- prev_text/next_text: Text for previous/next links
- type: 'plain', 'array', or 'list'
- end_size: How many numbers on the ends
- mid_size: How many numbers around current page
WordPress Built-in Pagination Functions
Using the_posts_pagination() (WordPress 4.1+)
<?php
// Simplest numbered pagination
the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( '← Previous', 'textdomain' ),
'next_text' => __( 'Next →', 'textdomain' ),
'screen_reader_text' => __( 'Posts navigation', 'textdomain' )
) );
// Or get the markup for customization
$pagination = get_the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( '← Previous', 'textdomain' ),
'next_text' => __( 'Next →', 'textdomain' ),
) );
// Modify and output
echo str_replace( 'navigation pagination', 'my-custom-pagination', $pagination );
?>
Using the_posts_navigation()
<?php
// Simple previous/next navigation with semantic markup
the_posts_navigation( array(
'prev_text' => __( 'Older posts', 'textdomain' ),
'next_text' => __( 'Newer posts', 'textdomain' ),
'screen_reader_text' => __( 'Posts navigation', 'textdomain' ),
'aria_label' => __( 'Posts', 'textdomain' ),
'class' => 'posts-navigation',
) );
?>
Single Post Navigation
🚀 AJAX Load More Pagination
Create dynamic pagination without page refreshes for better user experience.
Implementation Steps:
- Create initial posts display
- Add "Load More" button
- Set up AJAX handler in functions.php
- Create JavaScript for AJAX request
- Handle response and append posts
HTML Structure
<!-- In your template file -->
<div class="posts-container">
<?php
$paged = 1;
$args = array(
'post_type' => 'post',
'posts_per_page' => 6,
'paged' => $paged
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) :
echo '<div class="posts-grid" data-page="1">';
while ( $query->have_posts() ) : $query->the_post();
get_template_part( 'template-parts/content', 'grid' );
endwhile;
echo '</div>';
if ( $query->max_num_pages > 1 ) :
echo '<button class="load-more-btn" data-max="' . $query->max_num_pages . '">Load More</button>';
echo '<div class="loading-spinner" style="display:none;">Loading...</div>';
endif;
wp_reset_postdata();
endif;
?>
</div>
JavaScript AJAX Handler
jQuery(document).ready(function($) {
$('.load-more-btn').on('click', function() {
var button = $(this);
var container = $('.posts-grid');
var page = parseInt(container.attr('data-page'));
var maxPages = parseInt(button.attr('data-max'));
// Show loading spinner
button.hide();
$('.loading-spinner').show();
$.ajax({
url: ajax_params.ajax_url,
type: 'POST',
data: {
action: 'load_more_posts',
page: page + 1,
nonce: ajax_params.nonce
},
success: function(response) {
if (response.success) {
// Append new posts
container.append(response.data.html);
// Update page number
container.attr('data-page', page + 1);
// Hide/show button
if (page + 1 >= maxPages) {
button.remove();
} else {
button.show();
}
} else {
console.error('Error loading posts');
}
$('.loading-spinner').hide();
},
error: function() {
console.error('AJAX error');
$('.loading-spinner').hide();
button.show();
}
});
});
});
PHP AJAX Handler (functions.php)
<?php
// Enqueue scripts with localization
function enqueue_load_more_scripts() {
wp_enqueue_script(
'load-more',
get_template_directory_uri() . '/js/load-more.js',
array('jquery'),
'1.0.0',
true
);
wp_localize_script( 'load-more', 'ajax_params', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'load_more_nonce' )
));
}
add_action( 'wp_enqueue_scripts', 'enqueue_load_more_scripts' );
// AJAX handler
function load_more_posts_handler() {
// Verify nonce
if ( ! wp_verify_nonce( $_POST['nonce'], 'load_more_nonce' ) ) {
wp_die( 'Security check failed' );
}
$page = isset($_POST['page']) ? intval($_POST['page']) : 2;
$args = array(
'post_type' => 'post',
'posts_per_page' => 6,
'paged' => $page
);
$query = new WP_Query( $args );
ob_start();
if ( $query->have_posts() ) :
while ( $query->have_posts() ) : $query->the_post();
get_template_part( 'template-parts/content', 'grid' );
endwhile;
endif;
$html = ob_get_clean();
wp_send_json_success( array(
'html' => $html,
'max_pages' => $query->max_num_pages
));
wp_die();
}
add_action( 'wp_ajax_load_more_posts', 'load_more_posts_handler' );
add_action( 'wp_ajax_nopriv_load_more_posts', 'load_more_posts_handler' );
?>
Infinite Scroll Implementation
Infinite Scroll with Intersection Observer
// Modern infinite scroll using Intersection Observer
document.addEventListener('DOMContentLoaded', function() {
const postsContainer = document.querySelector('.posts-grid');
const loadingTrigger = document.querySelector('.infinite-scroll-trigger');
let isLoading = false;
let currentPage = 1;
const options = {
root: null,
rootMargin: '100px',
threshold: 0.1
};
const loadMorePosts = () => {
if (isLoading) return;
isLoading = true;
currentPage++;
fetch(ajax_params.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'load_more_posts',
page: currentPage,
nonce: ajax_params.nonce
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
postsContainer.insertAdjacentHTML('beforeend', data.data.html);
if (currentPage >= data.data.max_pages) {
observer.unobserve(loadingTrigger);
loadingTrigger.remove();
}
}
isLoading = false;
})
.catch(error => {
console.error('Error:', error);
isLoading = false;
});
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMorePosts();
}
});
}, options);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
});
Pagination Styling Examples
Classic Style
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
margin: 2rem 0;
}
.pagination a,
.pagination span {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
text-decoration: none;
color: #333;
}
.pagination .current {
background: #007cba;
color: white;
border-color: #007cba;
}
Modern Rounded
.pagination {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.pagination a,
.pagination span {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #4b5563;
text-decoration: none;
transition: all 0.3s;
}
.pagination a:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
.pagination .current {
background: #667eea;
color: white;
}
Minimal
.pagination {
display: flex;
gap: 2rem;
justify-content: center;
align-items: center;
}
.pagination a {
color: #6b7280;
text-decoration: none;
position: relative;
padding: 0.5rem 0;
}
.pagination a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: #667eea;
transition: width 0.3s;
}
.pagination a:hover::after {
width: 100%;
}
.pagination .current {
color: #667eea;
font-weight: bold;
}
Pagination Best Practices
Performance Optimization
- Use
no_found_rows => truewhen not displaying pagination - Cache pagination results when possible
- Limit the number of page links displayed
- Consider lazy loading for infinite scroll
User Experience
- Always show current page clearly
- Provide first/last page links for long lists
- Include page numbers in URLs for bookmarking
- Add keyboard navigation support
- Show total pages or items when relevant
SEO Considerations
- Use canonical URLs to avoid duplicate content
- Implement rel="next" and rel="prev" tags
- Ensure crawlable pagination links
- Avoid infinite scroll for important content
SEO-Friendly Pagination Head Tags
<?php
// In header.php
$paged = get_query_var('paged') ? get_query_var('paged') : 1;
if ( is_archive() || is_home() ) {
if ( $paged > 1 ) {
echo '<link rel="prev" href="' . get_pagenum_link( $paged - 1 ) . '" />';
}
global $wp_query;
if ( $paged < $wp_query->max_num_pages ) {
echo '<link rel="next" href="' . get_pagenum_link( $paged + 1 ) . '" />';
}
}
?>
Troubleshooting Common Issues
Issue: Pagination shows 404 on page 2+
Solution: Check permalink settings and ensure 'paged' parameter is correctly set
Solution: Check permalink settings and ensure 'paged' parameter is correctly set
Issue: Custom query pagination not working
Solution: Use 'paged' parameter and wp_reset_postdata()
Solution: Use 'paged' parameter and wp_reset_postdata()
Issue: Static front page pagination broken
Solution: Use 'page' instead of 'paged' parameter
Solution: Use 'page' instead of 'paged' parameter
Fix for Static Front Page
<?php
// For static front page, use 'page' instead of 'paged'
if ( get_query_var('paged') ) {
$paged = get_query_var('paged');
} elseif ( get_query_var('page') ) {
$paged = get_query_var('page');
} else {
$paged = 1;
}
$args = array(
'posts_per_page' => 10,
'paged' => $paged
);
?>
Practice Exercise
Pagination Challenge