“Load More” Button on Multiple Loops

“Load More” Button on Multiple Loops

More and more blogs today are eschewing pagination in favor of loading additional posts on the same page via ajax. WordPress has no shortage of plugins that add such functionality, but most tend to be too general or too specific. Jetpack’s Infinite Scroll is one of the most popular and easy implementations, being the recommended method for VIP-hosted sites, but even it has a serious limitation in that it can only operate on the main loop. When nothing else fits, sometimes the best course of action is to create your own solution.

For this tutorial, we’re going to create our own implementation of a “Load More” button, one capable of handling multiple loops on one page. My approach has a few steps:

  • A template tag will render the button, specific to each loop
  • Clicking the button will fire some javascript that will request the loop’s archive page via ajax
  • The loaded page will be parsed and the posts will be pulled out and appended to my existing posts, along with the new Load More button for loading the next subsequent page

I’ve created a site using the Twenty Fifteen theme that comes with WordPress and filled it with some dummy posts to get the ball rolling. To help my screenshots display a little bit more content than the theme allows out of the box, I’ve also restyled the main loop slightly to display the posts as tiles and set the site to display four posts at a time.

"Load More" blog

Here’s what the relevant area of my index.php looks like at the moment:

<div id="primary" class="content-area">
   <main id="main" class="site-main" role="main">

   <?php if ( have_posts() ) : ?>

      <h2>Posts</h2>

      <?php
      while ( have_posts() ) : the_post();
         get_template_part( 'content', get_post_format() );
      endwhile;

      ?>
      <div class="clear"></div>
      <?php

      // The theme's default pagination
      the_posts_pagination( array(
         'prev_text'          => __( 'Previous page', 'twentyfifteen' ),
         'next_text'          => __( 'Next page', 'twentyfifteen' ),
         'before_page_number' => '<span class="meta-nav screen-reader-text">' . __( 'Page', 'twentyfifteen' ) . ' </span>',
      ) );

   else :
      get_template_part( 'content', 'none' );
   endif;
   ?>

   </main><!-- .site-main -->
</div><!-- .content-area -->

This would normally be the perfect situation in which to implement Jetpack’s Infinite Scroll. With just a tiny bit of setup, the navigation at the bottom can be replaced with a Load More button that will load in subsequent posts underneath the existing ones without reloading the entire page. However, my homepage is not going to stay so simple for long because I want to display a second post loop on the same page.

I’ve gone ahead and created my second set of posts, assigning them to a category to differentiate them from my regular posts.

"Load More" post list

To display the second set of posts, I’m going to have to create a second Loop and a new WP_Query to feed it. It’s good practice to separate out logic and presentation so this should go outside of my template file. I’m going to add a template tag that returns my new query in the theme’s functions.php file.

The following code will return a new WP_Query object with just the posts in my new category, while excluding those same posts from my main query.

/* Get custom posts query */
function voce_get_custom_posts() {
   return new WP_Query( array(
      'cat' => 2,
   ) );
}

/* Don't display custom posts in main query on the homepage */
add_action( 'pre_get_posts', function( $query ) {
   if ( $query->is_home() && $query->is_main_query() ) {
      $query->set( 'category__not_in', '2' );
   }
} );

Now I can loop over my new query and output just my custom posts, while those same posts will be excluding from the main loop.

<div id="primary" class="content-area">
   <main id="main" class="site-main" role="main">

   <?php
   // Get custom posts query via my new template tag
   $custom_post = voce_get_custom_posts();
   if ( $custom_post-> have_posts() ) :

      ?>
      <h2>Custom Posts</h2>
      <?php

      // Loop over custom posts
      while ( $custom_post->have_posts() ) : $custom_post->the_post();
         get_template_part( 'content', get_post_format() );
      endwhile;

      ?>
      <div class="clear"></div>
      <?php

   else :
      get_template_part( 'content', 'none' );
   endif;

   // Reset post data since I'm done with my custom query
   wp_reset_postdata();
   ?>

   <?php if ( have_posts() ) : ?>

...

My new loop is now rendering above the main loop, with each loop displaying the correct posts.

"Load More" custom posts

Now that I have my posts, it’s time to add my Load More buttons. I’m going to create a template tag to render the button. As before, the logic portion of the code should not go into the template itself but rather into functions.php.

/* Load More */
function voce_load_more( $loop_query = null ) {
   // Get the main query if one isn't specified
   if ( ! $loop_query ) {
      global $wp_query;

      $loop_query = $wp_query;
   }

   // Get the total number of pages
   $max_page = ( empty( $loop_query->max_num_pages ) ? 1 : $loop_query->max_num_pages );

   // Get the current page, and use that to get the next page
   $paged = ( empty( $loop_query->query_vars['paged'] ) ? 1 : $loop_query->query_vars['paged'] );
   $next_page = intval( $paged ) + 1;

   // If there are no more pages, we don't need the "Load More" button anymore
   if ( $next_page > $max_page ) {
      return '';
   }

   // Allow filtering on the URL, since we'll need to point each query to its respective archive URL
   $url = apply_filters( 'voce_load_more_url', next_posts( $max_page, false ), $loop_query, $next_page );

   // Render the Load More button
   ?>
   <div class="load-more">
      <span class="load-more-indicator">Loading...</span>
      <a class="load-more-link" href="<?php echo esc_url( $url ); ?>">Load More</a>
   </div>
   <?php
}

I also need to build out the URL that each loop should use to access subsequent posts. This is the URL to the archive for just that loop’s query and needs to be set for each loop.

For my custom query, I can just check for the cat query_var, as that is all that defines that query. The archive permalink structure for my custom query would be the /category/TERM archive page.

/* Set archive URL for custom loop */
add_filter( 'voce_load_more_url', function( $url, $query, $next_page ) {
   if ( ! empty( $query->query_vars['cat'] ) && 2 == $query->query_vars['cat'] ) {
      $url = sprintf( '%s/category/custom-loop/page/%d', site_url(), $next_page );
   }

   return $url;
}, 10, 3 );

For my main query, things are a little bit more complicated. Even though this is the main query, I still need to specify a custom URL to use for its Load More buttons because I need the archive page of just that query; with the way the site is currently set up, the main query will just use index.php for its archives (which now contains two loops!). As such, I have to go with the /category/uncategorized archive page, which in this case singles out just the posts that would show up in my main query. This means I need two checks to determine which query I’m working with: check for the category__not_in query_var for the homepage, and check for the “Uncategorized” category archive for subsequent pages.

/* Set archive URL for main loop */
add_filter( 'voce_load_more_url', function( $url, $query, $next_page ) {
   if ( ! empty( $query->query_vars['category__not_in'] ) ||
      ( ! empty( $query->query_vars['cat'] ) && 1 == $query->query_vars['cat'] ) ) {
      $url = sprintf( '%s/category/uncategorized/page/%d', site_url(), $next_page );
   }

   return $url;
}, 10, 3 );

With my new template tag created, I can now use it to build out the Load More buttons in both my templates: index.php (which loads the first page of both query) and archive.php (which loads the subsequent pages of each query).

<div id="primary" class="content-area">
   <main id="main" class="site-main" role="main">

   <?php
   $custom_post = voce_get_custom_posts();
   if ( $custom_post-> have_posts() ) :

      ?>
      <h2>Custom Posts</h2>
      <div class="load-more-wrapper">
         <div class="load-more-container">

            <?php
            // Loop over custom posts
            while ( $custom_post->have_posts() ) : $custom_post->the_post();
               get_template_part( 'content', get_post_format() );
            endwhile;
            ?>

         </div>
         <div class="clear"></div>
         <?php
         // Load More for custom query
         voce_load_more( $custom_post );
         ?>
      </div>
      <?php

   else :
      get_template_part( 'content', 'none' );
   endif;

   // Reset post data since I'm done with my custom query
   wp_reset_postdata();
   ?>

   <?php if ( have_posts() ) : ?>

      <h2>Posts</h2>
      <div class="load-more-wrapper">
         <div class="load-more-container">

            <?php
            // Loop over main query
            while ( have_posts() ) : the_post();
               get_template_part( 'content', get_post_format() );
            endwhile;
            ?>

         </div>
         <div class="clear"></div>
         <?php
         // Load More for main query
         voce_load_more();
         ?>
      </div>
      <?php

   else :
      get_template_part( 'content', 'none' );
   endif;
   ?>

   </main><!-- .site-main -->
</div><!-- .content-area -->

The buttons are rendering on the homepage now.

"Load More" link

Next, I have to update the archive.php template.

<section id="primary" class="content-area">
   <main id="main" class="site-main" role="main">

   <?php if ( have_posts() ) : ?>

      <header class="page-header">
         <?php
            the_archive_title( '<h1 class="page-title">', '</h1>' );
            the_archive_description( '<div class="taxonomy-description">', '</div>' );
         ?>
      </header><!-- .page-header -->

      <?php // The part that matters! ?>
      <?php // -------------------------------- ?>
      <div class="load-more-wrapper">
         <div class="load-more-container">
            <?php // Start the Loop ?>
            <?php while ( have_posts() ) : the_post(); ?>

               <?php get_template_part( 'content', get_post_format() ); ?>

            <?php endwhile; ?>
         </div>
         <div class="clear"></div>
         <?php voce_load_more(); ?>
      </div>
      <?php // --------------------------------

 // If no content, include the "No posts found" template.
 else :
 get_template_part( 'content', 'none' );

 endif;
 ?>

 </main><!-- .site-main -->
</section><!-- .content-area -->

In all instances, the voce_load_more() tag is called inside the .load-more-wrapper div, right after the .load-more-container div which houses the actual posts. This allows the javascript to treat each .load-more-wrapper div individually as separate loops. Time to add the javascript that will do just that.

(function($){

   // Attach "Load More" functionality
   $('body').on('click', '.load-more a', function(e) {

      // Don't actually follow the href in the <a>
      e.preventDefault();

      var $el = $(e.target),      // the <a>
         $resp = $('<div>'),      // a <div> to store our new posts
         $parent = $el.parent();  // the <div> wrapper for the current loop

      // Hide the "Load More" button and show the "Loading..." indicator
      $parent.addClass('loading');

      // Get posts (and new Load More button) from archive page and append them to our existing posts 
      $resp.load($el.attr('href') + ' .load-more-wrapper', function() {

         $parent.removeClass('loading');

         var $container = $el.parents('.load-more-wrapper').find('.load-more-container'),
            $new_posts = $resp.find('.load-more-container').children(),
            $load_more = $resp.find('.load-more') || '';

         $container.append($new_posts);

         $el.parent().replaceWith($load_more);
      });
   });

})(jQuery);

That’s it! I now have two loops on my homepage, each with a corresponding Load More button. Further, the links on the buttons lead to the actual archive page for that loop. I’ve used this code on several sites now, and (after accounting for styling) the only real modifications necessary are to the filters that determine the URL for the buttons. Hopefully this helps someone out as much as it’s helped me.