In this blog, we will learn about how to create a load more posts feature for a single post page in WordPress.
- Create a nonce and pass it to the JavaScript file, so it can be verified.
- We will create a function that renders the template with load more button.
- Create a function that verifies the nonce and then makes a WP_Query to get more posts
- 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. - 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.