HOMEBlogTutorialsWordPress ajax load more: Safely Implement It Without…

WordPress ajax load more: Safely Implement It Without Plugins (4 Steps)

WordPress ajax load more

Are you tired of users bouncing from your blog because they have to click through clunky pagination links? Traditional pagination interrupts the reading experience and forces unnecessary page reloads, significantly degrading your site’s User Experience (UX). The modern solution is to implement a WordPress ajax load more feature. By fetching and appending new posts dynamically, you keep visitors engaged on a single, continuous page. In this comprehensive technical guide, we will build a robust, secure, and performant load more button using modern Vanilla JavaScript (Fetch API) and native WordPress AJAX handlers, bypassing the heavy jQuery dependency entirely.

Before diving into the code for our WordPress ajax load more implementation, let’s ensure your environment is prepped and ready for custom development.

Prerequisites

  • PHP 8.0 or higher recommended for optimal performance.
  • WordPress 6.0+ installed and running.
  • Active Child Theme (do not edit parent theme files directly to prevent update overwrites).
  • Basic understanding of the WordPress WP_Query class.
  • A complete backup of your website files and database.
Backup Required
Always create a staging environment or run a full site backup before modifying your theme’s functions.php file. A single syntax error in PHP can trigger a critical error on your site.

Implementing a WordPress ajax load more button involves three distinct layers: the frontend HTML/JS, the bridge that connects JS to PHP, and the backend PHP processor. Let’s break this down into manageable, foolproof steps.

Step 1: Enqueueing Scripts and Localizing AJAX Data

Learn how to properly register and enqueue your JavaScript file while securely passing localized PHP variables using the WordPress localized script function.

The first crucial step in building a WordPress ajax load more system is loading our JavaScript file and handing it the necessary backend data. JavaScript inherently doesn’t know where the WordPress AJAX processing file (admin-ajax.php) lives. We must use wp_localize_script() to create a global JavaScript object containing our AJAX URL, a security nonce, and the initial query parameters.

WordPress ajax load more - AJAX Data Flow Diagram
AJAX Data Flow Diagram

Creating the JavaScript File

First, create a new file in your child theme’s directory. A good standard practice is to place this inside an assets/js/ folder. Create the file and name it pnet-loadmore.js. Leave it blank for now; we will write the logic in Step 4. This file will handle the asynchronous requests for our WordPress ajax load more functionality.

Registering in functions.php

Open your child theme’s functions.php file. We will write a function hooked to wp_enqueue_scripts. This script not only enqueues our newly created JS file but also serializes backend data into a frontend variable.

PHP
function pnet_enqueue_load_more_scripts() {
    // Only load on the blog/archive pages to save resources
    if ( ! is_home() && ! is_archive() ) {
        return;
    }

    // Register and enqueue the script
    wp_register_script(
        'pnet-ajax-load-more',
        get_stylesheet_directory_uri() . '/assets/js/pnet-loadmore.js',
        array(), // No dependencies, we are using Vanilla JS!
        '1.0.0',
        true // Load in footer
    );

    wp_enqueue_script( 'pnet-ajax-load-more' );

    // Define the global WP_Query to access current page details
    global $wp_query;

    // Localize the script with new data
    wp_localize_script(
        'pnet-ajax-load-more',
        'pnet_loadmore_params',
        array(
            'ajaxurl'      => admin_url( 'admin-ajax.php' ), // The WP AJAX endpoint
            'posts'        => json_encode( $wp_query->query_vars ), // Current query arguments
            'current_page' => get_query_var( 'paged' ) ? get_query_var('paged') : 1, // Current page number
            'max_page'     => $wp_query->max_num_pages, // Total number of pages
            'nonce'        => wp_create_nonce( 'pnet_loadmore_nonce' ) // Security token
        )
    );
}
add_action( 'wp_enqueue_scripts', 'pnet_enqueue_load_more_scripts' );
Security First
Notice the wp_create_nonce(). This generates a cryptographic token that our backend will verify before processing the WordPress ajax load more request, protecting your server from unauthorized malicious requests.

Step 2: Building the HTML Structure and Loop

Create the foundation by writing a custom WordPress WP_Query loop and adding the structural HTML required for the load more button.

For your WordPress ajax load more button to function, your frontend HTML must be structured to wrap the posts in a container. This container acts as the target where our JavaScript will inject the newly fetched HTML payloads.

Setting up the initial query

Navigate to your theme’s index.php, home.php, or custom archive template. You need to wrap your standard WordPress loop inside a specific div. Let’s look at how a standard loop should be structured for compatibility.

PHP
<div class="pnet-post-wrapper" id="pnet-post-container">
    <?php if ( have_posts() ) : ?>
        <?php while ( have_posts() ) : the_post(); ?>
            
            <article id="post-<?php the_ID(); ?>" <?php post_class( 'pnet-article-card' ); ?>>
                <h2 class="entry-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
                <div class="entry-excerpt">
                    <?php the_excerpt(); ?>
                </div>
            </article>

        <?php endwhile; ?>
    <?php else : ?>
        <p><?php esc_html_e( 'No posts found.', 'textdomain' ); ?></p>
    <?php endif; ?>
</div>

Adding the Load More Button

Immediately after the closing </div> of your #pnet-post-container, we need to output the physical button. However, we should only display this button if there is more than one page of content available. This keeps the UI clean when your WordPress ajax load more feature isn’t needed.

PHP
<?php 
global $wp_query; // Ensure we have access to the main query object
if (  $wp_query->max_num_pages > 1 ) : 
?>
    <div class="pnet-loadmore-wrapper" style="text-align: center; margin-top: 2rem;">
        <button id="pnet-loadmore-btn" class="button pnet-btn-primary">
            <?php esc_html_e( 'Load More Posts', 'textdomain' ); ?>
        </button>
    </div>
<?php endif; ?>
WordPress ajax load more - Dark Theme Blog Mockup
Dark Theme Blog Mockup

Step 3: Developing the AJAX PHP Handler

Write the backend PHP logic hooked to admin-ajax to securely process the incoming request and return the rendered post HTML.

When the user clicks the button, a request is sent to admin-ajax.php. WordPress looks for an action hook matching the request to process it. This is the heart of the WordPress ajax load more system.

Creating the wp_ajax_ actions

In WordPress, AJAX handlers require two hooks: wp_ajax_{action} for logged-in users, and wp_ajax_nopriv_{action} for logged-out visitors. Let’s add this processor to your functions.php.

PHP
function pnet_ajax_load_more_handler() {
    // 1. Verify the security nonce immediately
    if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'pnet_loadmore_nonce' ) ) {
        wp_send_json_error( 'Invalid security token.', 403 );
        wp_die();
    }

    // 2. Decode the query arguments passed from JS
    $args = isset( $_POST['query'] ) ? json_decode( stripslashes( $_POST['query'] ), true ) : array();
    
    // 3. Increment the page number for the new query
    $args['paged'] = isset( $_POST['page'] ) ? absint( $_POST['page'] ) + 1 : 2;
    $args['post_status'] = 'publish'; // Ensure only published posts are fetched

    // 4. Run the WP_Query
    $query = new WP_Query( $args );

    // 5. Output the results
    if ( $query->have_posts() ) {
        ob_start(); // Start output buffering

        while ( $query->have_posts() ) : $query->the_post();
            // It is best practice to use get_template_part here if you have a separate file
            // For this example, we output the markup directly
            ?>
            <article id="post-<?php the_ID(); ?>" <?php post_class( 'pnet-article-card' ); ?>>
                <h2 class="entry-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
                <div class="entry-excerpt">
                    <?php the_excerpt(); ?>
                </div>
            </article>
            <?php
        endwhile;

        $html = ob_get_clean(); // Capture the buffer
        wp_send_json_success( $html ); // Send valid JSON success response with HTML
    } else {
        wp_send_json_error( 'No more posts.' ); // Send JSON error if no posts
    }

    wp_die(); // Always kill the process at the end of an AJAX handler
}

// Hook for logged-in users
add_action( 'wp_ajax_pnet_loadmore', 'pnet_ajax_load_more_handler' );
// Hook for logged-out/guest users
add_action( 'wp_ajax_nopriv_pnet_loadmore', 'pnet_ajax_load_more_handler' );
Developer Tip
Using ob_start() and ob_get_clean() (Output Buffering) is the cleanest way to render complex HTML blocks in PHP and pass them back flawlessly via a JSON response for your WordPress ajax load more handler.

Step 4: Writing the Vanilla JavaScript Fetch Logic

Implement modern, dependency-free vanilla JavaScript to listen for the button click, send the Fetch request, and seamlessly append the new posts.

Historically, WordPress ajax load more tutorials relied heavily on jQuery. In modern web development, vanilla JavaScript’s Fetch API is much faster, lighter, and completely eliminates the need for external libraries. Let’s write the client-side logic in the pnet-loadmore.js file we created in Step 1.

Catching the button click

We need to grab the DOM elements and add an event listener. We also need to manage state variables so we know which page we are currently on.

Javascript
document.addEventListener('DOMContentLoaded', function() {
    const button = document.getElementById('pnet-loadmore-btn');
    const container = document.getElementById('pnet-post-container');

    if ( ! button || ! container ) return; // Exit if elements don't exist

    // Extract variables from the localized object
    let currentPage = parseInt( pnet_loadmore_params.current_page );
    const maxPages = parseInt( pnet_loadmore_params.max_page );

    button.addEventListener('click', function(e) {
        e.preventDefault();

        // Update button UI state
        const originalText = button.textContent;
        button.textContent = 'Loading...';
        button.disabled = true;

        // Prepare the POST data using URLSearchParams
        const params = new URLSearchParams();
        params.append('action', 'pnet_loadmore'); // Matches our wp_ajax_ hook
        params.append('query', pnet_loadmore_params.posts);
        params.append('page', currentPage);
        params.append('nonce', pnet_loadmore_params.nonce);

        // Execute the Fetch request
        fetch( pnet_loadmore_params.ajaxurl, {
            method: 'POST',
            body: params,
            headers: {
                // Required by WP to process standard form data correctly
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 
            }
        })
        .then( response => response.json() )
        .then( data => {
            if ( data.success ) {
                // Append the raw HTML to our container
                container.insertAdjacentHTML('beforeend', data.data);
                
                currentPage++; // Increment the page counter

                // Hide button if we reached the end
                if ( currentPage >= maxPages ) {
                    button.style.display = 'none';
                } else {
                    // Reset UI
                    button.textContent = originalText;
                    button.disabled = false;
                }
            } else {
                console.error('WordPress ajax load more error:', data);
                button.textContent = 'End of content';
            }
        })
        .catch( error => {
            console.error('Fetch error:', error);
            button.textContent = 'Error loading posts';
            button.disabled = false;
        });
    });
});
WordPress ajax load more - DevTools Network Request
DevTools Network Request

Common Errors & Troubleshooting

Even with copy-pasting, environmental configurations can cause hiccups. Here are the most frequent issues developers face when implementing a WordPress ajax load more system.

Why is my WordPress ajax load more returning a 400 Bad Request?

This usually happens when the action parameter in your JavaScript does not match the wp_ajax_ hook in your PHP file. Double-check that params.append('action', 'pnet_loadmore'); exactly matches add_action( 'wp_ajax_pnet_loadmore', ... ).

Why does the button load the same posts repeatedly?

This occurs if your JavaScript does not correctly increment the current page variable, or if your PHP handler is ignoring the $args['paged'] parameter. Ensure currentPage++ is executing successfully inside the .then() block of your fetch request.

Why is admin-ajax.php returning a 0 instead of my HTML?

If WordPress returns a 0, it means the AJAX request reached admin-ajax.php, but WordPress couldn’t find a matching action hook to process it. This is often caused by typos in the action name or failing to include both wp_ajax_ and wp_ajax_nopriv_ hooks. Also, ensure you have wp_die() at the very end of your PHP handler function.

Conclusion

Upgrading your site from traditional pagination to a smooth, asynchronous fetching architecture provides a massive boost to user engagement. By following this guide, you have successfully implemented a highly optimized WordPress ajax load more feature using native PHP processors and modern JavaScript. You have eliminated the need for heavy jQuery dependencies, utilized secure nonces for backend protection, and created a scalable architecture that can be adapted for custom post types, WooCommerce products, or portfolio items.

Remember that while AJAX creates a seamless experience, you should always ensure your site remains accessible and functional. Test your implementation across different devices and monitor your network tab to ensure performance remains blazing fast.

Abhik

🚀 Full Stack WP Dev | ☕ Coffee Enthusiast | 🏍️ Biker | 📈 Trader
Hi, I’m Abhik. I’ve been coding since 2007, a journey that began when I outgrew Blogger and migrated to a robust self-hosted stack. That transition introduced me to WordPress, and I’ve been building professional solutions ever since.

Leave a comment