Load More Post Or Infinite Scroll For A Single Post In WordPress

In this blog, we will learn about how to create a load more posts feature for a single post page in WordPress.

  1. Create a nonce and pass it to the JavaScript file, so it can be verified.
  2. We will create a function that renders the template with load more button.
  3. Create a function that verifies the nonce and then makes a WP_Query to get more posts
  4. We use the posts_where WordPress hook, to modify the queries where clause to ensure load the post ids less than the custom single post id, when load more is clicked.
  5. We add an event on the load more button, so that when it’s clicked, we trigger a load more AJAX request through JavaScript function, that calls the above PHP function to make a query and loop through posts and displays them.

Create None for Security and Enqueue Scripts

Let’s Create a nonce for AJAX request and pass the nonce to our JavaScript file which we will create later in the blog. Every time a request is made the nonce will be verified for security.

namespace MyApp;function asset_loader() {
   // Registers scripts.
   wp_register_script( 'app', 'url-path-to/loadmore.js' ), [ 'jquery' ], filemtime( get_stylesheet_directory() . '/file-path-to/loadmore.js' ), true );
   wp_enqueue_script( 'app' );   wp_localize_script( 'app', 'siteConfig', [
      'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
      'ajax_nonce' => wp_create_nonce( 'loadmore_post_nonce' ),
   ] );
}add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\asset_loader' );

Create a post Template

Let’s create a post template. `template-parts/post-card.php`. Please note that for demonstration purposes we are using tailwind CSS. You can add the CSS according to your own requirement.

<?php
/**
 * Post Card
 *
 * Note: Should be called with The Loop
 */
namespace MyApp;
$post_permalink = get_the_permalink();
?>
<section id="post-<?php the_ID(); ?>"
         class="mb-5 lg:mb-8 xl:mb-10 px-1 w-full overflow-hidden sm:w-1/2 md:w-1/3 lg:w-1/4">
   <header>
      <a href="<?php echo esc_url( $post_permalink ); ?>" class="block">
         <figure class="img-container relative w-full">
            <?php the_post_thumbnail( 'post-thumbnail', [ 'class' => 'absolute w-full h-full left-0 top-0 object-cover' ] ); ?>
         </figure>
      </a>
   </header>
   <div class="post-content">
         <p class="line-clamp-5 leading-6"><?php echo wp_strip_all_tags( get_the_content() ); ?></p>
   </div>
</section>

Now let’s add some functions in functions.php.

<?php
/**
 * Load More Posts Functions For Single Post.
 *
 * @package Aquila
 */

namespace MyApp;

use \WP_Query;

/**
 * Load more script call back
 *
 * @param bool $initial_request Initial Request( non-ajax request to load initial post ).
 *
 */
function ajax_script_single_post_load_more( bool $initial_request = false ) {

   if ( ! $initial_request && ! check_ajax_referer( 'loadmore_post_nonce', 'ajax_nonce', false ) ) {
      wp_send_json_error( __( 'Invalid security token sent.', 'aquila' ) );
      wp_die( '0', 400 );
   }

   // Check if it's an ajax call.
   $is_ajax_request = ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
                      strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest';
   /**
    * Page number.
    * If get_query_var( 'paged' ) is 2 or more, its a number pagination query.
    * If $_POST['page'] has a value which means its a loadmore request, which will take precedence.
    */
   $page_no = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
   $page_no = ! empty( $_POST['page'] ) ? filter_var( $_POST['page'], FILTER_VALIDATE_INT ) + 1 : $page_no;
   $single_post_id = ! empty( $_POST['single_post_id'] ) ? $_POST['single_post_id'] : 0;

   $query = get_single_load_more_query( $page_no, $single_post_id );

   if ( $query->have_posts() ):
      // Loop Posts.
      while ( $query->have_posts() ): $query->the_post();
         
        get_template_part( 'template-parts/post-card' );      endwhile;
   else:
      // Return response as zero, when no post found.
      wp_die( '0' );
   endif;

   wp_reset_postdata();

   /**
    * Check if its an ajax call, and not initial request
    *
    * @see https://wordpress.stackexchange.com/questions/116759/why-does-wordpress-add-0-zero-to-an-ajax-response
    */
   if ( $is_ajax_request && ! $initial_request ) {
      wp_die();
   }

}

/*
 * Load more script ajax hooks
 */
add_action( 'wp_ajax_nopriv_ajax_script_single_post_load_more', __NAMESPACE__ . '\\ajax_script_single_post_load_more' );
add_action( 'wp_ajax_ajax_script_single_post_load_more', __NAMESPACE__ . '\\ajax_script_single_post_load_more' );

/*
* Single post load more container.
*/
function single_post_load_more_container() {

   $single_post_id = get_the_ID();
   $loadmore_query = get_single_load_more_query( 1, $single_post_id );
   $has_next_page = !empty( $loadmore_query->posts );
   $total_pages = $loadmore_query->max_num_pages;

   // If the no next post is available, return null;
   if ( empty( $has_next_page ) ) {
      return null;
   }

   ?>
   <div class="single-post-loadmore-wrap entry-content">
      <div id="single-post-load-more-content" class="single-post-loadmore">
         <?php // This is where more posts will be added ?>
      </div>
      <div class="text-center mb-30px mt-10px">
         <button
               id="single-post-load-more-btn"
               data-page="0"
               data-single-post-id="<?php echo esc_attr( $single_post_id ); ?>"
               class="button-tertiary"
               data-max-pages="<?php echo esc_attr( $total_pages ); ?>"
         >
            <span><?php esc_html_e( 'Load More Stories', 'aquila' ); ?></span>
         </button>
         <span id="single-loading-text" class="mt-1 hidden text-sm"><?php esc_html_e( 'Loading...', 'aquila' ); ?></span>
      </div>
   </div>
   <?php
}

function get_single_load_more_query( $page_no, $single_post_id ) {
   // Default Argument.
   $args = [
      'post_status'      => 'publish',
      'posts_per_page'   => 1, // Number of posts per page - default
      'paged'            => $page_no,
      'starting_post_id' => intval( $single_post_id ),
   ];

   return new WP_Query( $args );
}

Modify the Query to start WordPress loop from after the current single post id

Place this in functions.php

Here we are checking the custom query param called `starting_post_id` from our query in `ajax_script_single_post_load_more()` function. If this parameter exists in the query, we modify the where clause to ensure we get all the post ids that are less than the starting_post_id (current single post id).

function posts_where( $where, $query ) {

   global $wpdb;

   $start = $query->get( 'starting_post_id' );

   // If the query does not have our custom param 'starting_post_id' return default where clause.
   if ( empty( $start ) ) {
      return $where;
   }

   $where .= " AND {$wpdb->posts}.ID < $start";

   return $where;
}

add_filter( 'posts_where', __NAMESPACE__ . '\\posts_where', 10, 2 );

Single Post — single.php

<?php
/**
 * Single Post template file
 *
 * @package Aquila
 */

namespace MyApp;

get_header();
?>
   <main id="primary" class="site-main">

      <?php
      if ( have_posts() ) :

         /* Start the Loop */
         while ( have_posts() ) :
            the_post();

            get_template_part( 'template-parts/post-card' );

         endwhile;

      else :

         get_template_part( 'template-parts/content', 'none' );

      endif;

      ?>
      <?php single_post_load_more_container(); ?>

   </main><!-- #main -->
   <?php
get_footer();

Add JavaScript

JavaScript. Create a file called `loadmore.js`. We will use the Intersection Observer API for tracking our load more button and firing an AJAX request at the click of the load more button.

( function( $ ) {
   class SingleLoadMore {
      constructor() {
         this.ajaxUrl = siteConfig?.ajaxUrl ?? '';
         this.ajaxNonce = siteConfig?.ajax_nonce ?? '';
         this.loadMoreBtn = $( '#single-post-load-more-btn' );
         this.loadingTextEl = $( '#single-loading-text' );
         this.isRequestProcessing = false;
         
         this.init();
         
      }
      
      init() {
         
         if ( ! this.loadMoreBtn.length ) {
            return;
         }
         
         this.totalPagesCount = this.loadMoreBtn.data( 'max-pages' );
         
         this.loadMoreBtn.on( 'click', () => {
            this.handleLoadMorePosts();
         } );
         
      }
      
      
      /**
       * Load more posts.
       *
       * 1.Make an ajax request, by incrementing the page no. by one on each request.
       * 2.Append new/more posts to the existing content.
       * 3.If it's the last page, remove the load-more button from DOM.
       *
       * @return null
       */
      handleLoadMorePosts() {
         
         // Get page no from data attribute of load-more button.
         const page = this.loadMoreBtn.data( 'page' );
         const singlePostId = this.loadMoreBtn.data( 'single-post-id' );
         if ( undefined === page || this.isRequestProcessing ) {
            return null;
         }
         
         const nextPage = parseInt( page ) + 1; // Increment page count by one.
         
         this.toggleLoading( true );
         
         $.ajax( {
            url: this.ajaxUrl,
            type: 'post',
            data: {
               page: page,
               single_post_id: singlePostId,
               action: 'ajax_script_single_post_load_more',
               ajax_nonce: this.ajaxNonce,
            },
            success: ( response ) => {
               
               this.loadMoreBtn.data( 'page', nextPage );
               $( '#single-post-load-more-content' ).append( response );
               this.removeLoadMoreIfOnLastPage( nextPage );
               
               this.toggleLoading( false );
            },
            error: ( response ) => {
               console.log( response );
               this.toggleLoading( false );
            },
         } );
      }
      
      /**
       * Remove Load more Button If on last page.
       *
       * @param {int} nextPage New Page.
       */
      removeLoadMoreIfOnLastPage = ( nextPage ) => {
         if ( nextPage + 1 > this.totalPagesCount ) {
            this.loadMoreBtn.remove();
         }
      }
      
      /**
       * Toggle Loading
       *
       * Show or hide the loading text.
       *
       * @param isLoading
       */
      toggleLoading = ( isLoading ) => {
         this.isRequestProcessing = isLoading;
         
         if ( isLoading ) {
            this.loadingTextEl.addClass( 'block' );
            this.loadingTextEl.removeClass( 'hidden' );
         } else {
            this.loadingTextEl.addClass( 'hidden' );
            this.loadingTextEl.removeClass( 'block' );
         }
      }
   }
   
   new SingleLoadMore();
   
} )( jQuery );

This creates a load more feature on a single post. For infinite Scroll, you can modify the JavaScript by taking reference from this blog post

That’s all folks. Thank you.

Leave a Reply