Skip to main content

Course Progress

Loading...

📄 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

💡
Key Concept
Pagination improves user experience by reducing page load times and making content more digestible. It's also crucial for SEO as it helps search engines crawl your content efficiently.

WordPress Pagination Methods

paginate_links()

Creates numbered pagination with customizable format

Most Flexible

the_posts_pagination()

Built-in numbered pagination (WordPress 4.1+)

Easiest

previous/next_posts_link()

Simple previous/next navigation

Basic

posts_nav_link()

Combined previous/next with separator

Simple

get_the_posts_navigation()

Returns navigation markup for customization

Customizable

Custom AJAX

Load more or infinite scroll

Advanced

Basic 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:

  1. Create initial posts display
  2. Add "Load More" button
  3. Set up AJAX handler in functions.php
  4. Create JavaScript for AJAX request
  5. 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

Pagination Best Practices

Performance Optimization

  • Use no_found_rows => true when 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
Issue: Custom query pagination not working
Solution: Use 'paged' parameter and wp_reset_postdata()
Issue: Static front page pagination broken
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

Create a complete blog archive page with:

  1. Numbered pagination using paginate_links()
  2. AJAX load more functionality as an alternative
  3. Custom styling matching your theme design
  4. Pagination for a custom post type
  5. Single post navigation with thumbnails
  6. SEO-friendly markup with proper meta tags
  7. Accessible navigation with ARIA labels
  8. Mobile-responsive pagination design

Additional Resources