Load more posts with ajax in wordpress, without page reload — Weichie
Menu

Updated on April 20, 2022

·

Load more posts with ajax in wordpress, without page reload.

Loading more posts with ajax is a very popular UI button in today’s websites. On WordPress, a lot of people use plugins, page builders, or free/purchased themes that come shipped with this functionality. But writing it custom is not that hard either! So let’s try to create one today. As a bonus, we also cover load-more with pagination and the horror of the WordPress ‘offset’ function when trying to paginate.

load more posts ajax

I’ve covered this topic a few years ago when I was young and inexperienced. When I look back an the way I loaded more posts with ajax back then, I get the feeling every programmer has looking back at his/her code from a few years back. But… the post received a decent amount of traffic so I kept it online to help people create their own ajax load more function. But if you want a laugh, please visit my older way of loading more posts on WordPress with ajax 🙂

Now, a few years later, the time has passed and I use the admin-ajax function from WordPress way more instead of creating my own API calls to receive my posts. So this is what I’ll cover in this post:

  1. Setting up basic WordPress query
  2. Creating the ajax function to load more posts
  3. Creating the WordPress load more query
  4. Combining ajax and WordPress
  5. Hiding load more button (when no more posts)
  6. WordPress pagination with offset

The WordPress query to load posts from a (custom) post type

Let’s start off with the basic query for our posts. The query will display the first 6 posts from our post-type ‘publications’. As you can see, I also added the ‘paged’ parameter but hardcoded it to 1. This is because we’ll be loading more posts using javascript and are not listening to a query parameter. Visiting website.com/page/2 will NOT display the second page in this example as we’re not listening to the query var.

The following code example should go in your template file where you want to display your post-list. I’m using get but you can hardcode your card template in this loop as well. Having them as a template part makes it clearer later when we’re also loading those cards using javascript.

<?php 
$publications = new WP_Query([
  'post_type' => 'publications',
  'posts_per_page' => 6,
  'orderby' => 'date',
  'order' => 'DESC',
  'paged' => 1,
]);
?>

<?php if($publications->have_posts()): ?>
  <ul class="publication-list">
    <?php 
      while ($publications->have_posts()): $publications->the_post();
        get_template_part('parts/card', 'publication');
      endwhile;
    ?>
  </ul>
<?php endif; ?>
<?php wp_reset_postdata(); ?>

<div class="btn__wrapper">
  <a href="#!" class="btn btn__primary" id="load-more">Load more</a>
</div>

The code above can be split into 3 main sections. The first one is the query to actually get the first 6 posts from ‘publications’. The second part is our loop over the results of the $publications query. Don’t forget to call the wp_reset_postdata() function, in the end, to make sure the rest of our page continues to work correctly. Finally, we also add the “load more” button under our list to be able to load more posts.

Now that we have our base setup ready, it’s time to link some javascript and Ajax to that load-more button!

The load more ajax function

Now it’s time to add a click event to our load more button. I will be using jQuery in this tutorial, as it comes shipped by default with WordPress. And because this post is specifically for loading more posts with Ajax in WordPress, I will be doing just that.

For the JS part, head over to your javascript folder and add a click event to the button. You can use arrow functions if you’re feeling fancy, but because I did not cover a babel-compiler setup I’m going for the more stable old-school way:

$('#load-more').on('click', function() {
  // We will do our magic here soon!
}

Before we continue, there are a few things we need to keep in mind. We want to load ‘more posts’, not just any posts, but more specifically only the next page of posts. So we need to keep track of the current page we’re on, and the page we want to load next.

The benefit of using Ajax to load more posts is that our page will not refresh. This means that we can remember things in plain javascript, without the need of storing data in localStorage or using some sort of $store as we have in React or Vue. A simple variable in javascript will do just fine.

We will edit our javascript snippet with a variable to keep track of the current page, and we add the $ajax call to our ajax function that we will write in the next chapter.

let currentPage = 1;
$('#load-more').on('click', function() {
  currentPage++; // Do currentPage + 1, because we want to load the next page

  $.ajax({
    type: 'POST',
    url: '/wp-admin/admin-ajax.php',
    dataType: 'html',
    data: {
      action: 'weichie_load_more',
      paged: currentPage,
    },
    success: function (res) {
      $('.publication-list').append(res);
    }
  });
});

If you want to test this snippet to make sure it’s working I need to disappoint you. This will not work just yet. We are now making a call to a WordPress function that we did not write yet! So let’s do that first before testing or complaining that it is not working.

But we can still test it to make sure we’re going in the right direction so far. When you click the load more button, nothing should happen visually on the page. But if you open your chrome dev tools and navigate to the Network tab, you should be able to see that we made an ajax request to admin-ajax.php.

This admin-ajax.php request will only become visible when you click our button. Clicking it again will make a second call (and then our payload should show ‘paged: 3’). We can see two things in our payload: The ‘action’ and ‘paged’. These are the two things we passed in our javascript $ajax call under data {}. It should return an error for you, as it cannot find our function ‘weichie_load_more’.

The payload is what we’re sending from our frontend to our backend. Because of this payload, we have access to the page we want to load in our PHP function where we will be doing the query. So next up: Creating our weichie_load_more function that will actually load more posts for us.

I don’t know how familiar you are with WordPress Queries and custom plugins? Some developers prefer creating plugins for their custom code, but I prefer to add our functionalities straight into the functions.php file in our WordPress root.

You may add the following snippet to your functions.php:

function weichie_load_more() {
  $ajaxposts = new WP_Query([
    'post_type' => 'publications',
    'posts_per_page' => 6,
    'orderby' => 'date',
    'order' => 'DESC',
    'paged' => $_POST['paged'],
  ]);

  $response = '';

  if($ajaxposts->have_posts()) {
    while($ajaxposts->have_posts()) : $ajaxposts->the_post();
      $response .= get_template_part('parts/card', 'publication');
    endwhile;
  } else {
    $response = '';
  }

  echo $response;
  exit;
}
add_action('wp_ajax_weichie_load_more', 'weichie_load_more');
add_action('wp_ajax_nopriv_weichie_load_more', 'weichie_load_more');

As you can see, our query part is actually the exact same thing as we have in our page template. We’re performing the exact same query, but the only difference here is that we’re pulling paged: 2 (or 3 or 4) instead of the first page.

We have our $ajaxposts which is our WordPress query we’ll be looping over (exactly like we do in our page template). We also declare a variable $response that will store our results. We then loop over our ajaxposts and in each iteration, we append our template_part to our response using the .= notation. And finally, we echo our $response and this is what we’ll be sending back to our javascript.

This is because of the pagination. We don’t want to pull the same posts when clicking on ‘load more’, but give us the same amount of posts, from the next page. Now that we have this, we can test if clicking on the button still returns an error or not. But normally, it should return us the HTML blocks of the 6 posts from page 2. You can check this out in our network tab again, but instead of going to Payload (what we’re sending to the backend) we can check under Response (what the backend is sending back to the frontend)

Combining load more with Ajax and WordPress

Cool, now that we have a response that is sending back our new list items for page 2, it’s time to make them appear on the website. The ‘Response’ we’re seeing in our network tab is automatically returned to our $ajax request.

If you’re lucky, our current code could be already working for you. When you take a look at our current JS part, we already have a success: function in our ajax call:

// Nothing changed in our JS. This is just to show which part I'm talking about.
$.ajax({
  ...
  dataType: 'html',
  ...
  success: function (res) {
    $('.publication-list').append(res);
  }
});

The success function takes a parameter ‘res’ that holds our ajax response. Because we passed a dataType in our call, it knows we’re expecting HTML for the response. We simply target our $(‘.publication-list’) and .append the result (which is HTML). The append function is default jQuery.

Debugging: If the code is not working for you, we need to debug a little more. Try to log your res to the console and check what type of data is returning for you. You can’t write any js inside our ajax object, but because the success: function() { ... } – obviously – is a function, we can write console.log(res)inside the success function for debugging.

Now that our code is working and actually loads more posts, you’ll probably discover another problem soon, if the programmer in you hasn’t already. What if there are no more posts? Example: We have 4 pages and we reached page 4, then clicking on our load more button will load page 5 -> which is empty. We need to hide our load more button when we reached the last page, or add an inactive class or whatever you would like to do at the end.

They also check if we’re on the last page, we will change our ajax call to not return plain HTML, but to actually receive a JSON response. In this response, we will both put our HTML response (as we already have it right now) and the max_pages. This is needed to check if our current page index is bigger or equal to the max_pages. If so, we can hide our button because we reached the max amount of pages.

First things first, update our ajax call to work with JSON instead of HTML. Please note that making this change will break our current functionality for now, but we will fix it shortly!

In your JS file, change the ajax call to the following:

$.ajax({
  type: 'POST',
  url: '/wp-admin/admin-ajax.php',
  dataType: 'json', // <-- Change dataType from 'html' to 'json'
  data: {
    action: 'weichie_load_more',
    paged,
  },
  success: function (res) {
    $('.publication-list').append(res);
  }
});

We only changed the dataType for now. We also need to change our response in a minute, but first, we’ll be updating our PHP Query in the functions.php file to the following:

function weichie_load_more() {
  $ajaxposts = new WP_Query([
    'post_type' => 'publications',
    'posts_per_page' => 6,
    'orderby' => 'date',
    'order' => 'DESC',
    'paged' => $_POST['paged'],
  ]);

  $response = '';
  $max_pages = $ajaxposts->max_num_pages;

  if($ajaxposts->have_posts()) {
    ob_start();
    while($ajaxposts->have_posts()) : $ajaxposts->the_post();
        $response .= get_template_part('parts/card', 'publication');
    endwhile;
    $output = ob_get_contents();
    ob_end_clean();
  } else {
    $response = '';
  }

  $result = [
    'max' => $max_pages,
    'html' => $output,
  ];

  echo json_encode($result);
  exit;
}
add_action('wp_ajax_weichie_load_more', 'weichie_load_more');
add_action('wp_ajax_nopriv_weichie_load_more', 'weichie_load_more');

We changed a few more things in our PHP query than we had in our JS. But let’s go over them in more detail. The query itself remains untouched. We also still have our empty string $response = '';. We then use a default WordPress functionality to catch the max_num_pages from a query. This will return the maximum amount of pages we have for this query. So our load more button can max load “max_num_pages”-amount of pages.

Then while we’re looping over our query and appending each found post to our $results string, we actually wrap it this time inside a PHP buffer, called output buffer. While output buffering is active, no output is sent from the script and the output is stored in an internal buffer instead. before we end our output buffer, we store the result in another variable named $output.

In the end, right before we will return our results back to our javascript file, let’s create our return array:

// Nothing changed, just zooming in on our final result array
$result = [
  'max' => $max_pages,
  'html' => $output,
];
echo json_encode($result);

We create an array that takes the $max_pages, to return the max amount of pages for this query, and our HTML. The $output we’ve created from within our post-loop. To return an array back to our json file, we need to encode the array for JSON and return it like that.

Finally, to work with our new output, we also need to tweak our javascript. Because we’re not getting HTML as a response, but JSON, we need to go one level deeper when printing the response to the page. Change your javascript success function to the following:

$.ajax({
  ...
  success: function (res) {
    if(paged >= res.max) {
      $('#load-more').hide();
    }
    $('.publication-list').append(res.html);
  }
});

Our ‘res’ from our function(res) will now hold a json object with 2 key-value pairs. A key ‘max’ that holds our max_num_pages and a key ‘html’ which holds the HTML of our response.

To display our HTML list again on our page, we’ll need to use res.html. We also check if pages >= res.max to see if we need to hide our button, or if we did not reach the maximum amount of pages yet.

Using WordPress pagination with Offset

Using ajax in WordPress to load more posts or to filter posts in WordPress, it’s all fun and games as long as it actually works! If you can’t get it to work, there is no fun in making those things custom. And I had this when I tried to use this load-more script we just wrote, together with an offset.

The WordPress pagination simply does not work when you use an offset. The pagination query is completely broken and I don’t know why. But the entire script above is ready for the trash. I Googled around and there is literally no solution to make pagination work together with an offset.

How I ended up fixing this, is to create the offset myself as well… So in this example, I am skipping the 3 most recent posts, and want to paginate after that. To paginate the query without the latest 3 posts. Normally, I can simply use offset: 3 but with pagination, no way josé!

This is what I ended up doing instead:

$recent_publications = new WP_Query([
  'post_type' => 'publications',
  'posts_per_page' => 3,
  'orderby' => 'date',
  'order' => 'DESC',
  'fields' => 'ids',
]);

$ajaxposts = new WP_Query([
  'post_type' => 'publications',
  'posts_per_page' => 6,
  'orderby' => 'date',
  'order' => 'DESC',
  'paged' => $_POST['paged'],
  'post__not_in' => $recent_publications->get_posts()
]);

I first create a query that pulls the most recent 3 posts. Afterward, I create our actual WP Query, and instead of using offset, I exclude the posts from our first query. In other words: Skip the most recent 3 posts, in the query where we want to use pagination. It’s the same as offset, but with additional steps.

If you found an easier solution to make pagination work with offset, please leave your solution in the comments!

Common mistakes with an ajax query in WordPress

The pagination or page count is not correct

If the pagination or the amount of posts returning from the query is not correct, try adding 'post_status' => 'publish' to your query. It might be that WordPress messes up the count because it’s also counting private or draft posts. Those are included in the count of the query but are not printed on the page.

Conclusion

I hope my article was clear and helpful for you to create your own ajax load more button. You can play with this to update our code that instead of clicking on the load more button, you will trigger our load-more function when we reach the bottom of the page. This is a way to create a simple infinite-scroll without any plugins.

Make sure to also check my other post on how to filter posts with ajax in WordPress. It’s using the same login as in this post. It can also help if you’re trying to make ajax filters together with ajax load more.

Leave a Reply

Your email address will not be published.

9 Comments

  • Hans Dampf says:

    I am getting “paged is not defined” when using your code to hide the load more button. Any idea? The first part works without problems.

    • Bob Weichler says:

      Hi Hans, thank you for your reply. I am not sure how you’ve implemented my code example, but indeed ‘page’ will not be defined as my code-snippet only gives an example (to show you the logic how I do it and you can tweak it however you need it to be to work with your setup). I have set it as it’s own function, that takes paged as a parameter, like this:

      function loadMore(paged) {
        $.ajax({
          type: 'POST',
          url: '/wp-admin/admin-ajax.php',
          dataType: 'json',
          data: {
            action: 'weichie_load_more',
            paged,
          },
          success: function (res) {
            if(paged >= res.total) {
              $('#load-more').hide();
            }
            $('#jobs-grid-main').append(res.html);
          }
        });
      }

      And then I pass page to my function somewhere else and add page + 1:

      let newPage = 1;
      $('#load-more').on('click', function(){
        loadMore(newPage + 1);
        newPage++;
      });
  • Alessandro Amato says:

    Hey there thanks for the article, it works perfectly! However I wanted to go one step further and combine this with and Ajax filter. So I already have a post filter that pulls in posts depending on the category, but if I also add a load more button at the end it would load the next page of posts regardless of the category of my previous Ajax call – did you find a way to handle a similar case?

    Thank you!

    • Bob Weichler says:

      Hi Alessandro, thank you for your message. To combine the load more with filters, you need to pass your current filters into the load-more function as well. I will try to add a section to this post later this week. But for your JS it would look like this:

      $.ajax({
        type: 'POST',
        url: '/wp-admin/admin-ajax.php',
        dataType: 'json',
        data: {
          action: 'weichie_load_more',
          category: cat_id,
          paged,
        },
        success: function (res) { ... }
      });

      And then in your functions.php you can include the category in your query:

      $ajaxposts = new WP_Query([
          'post_type' => 'publications',
          'posts_per_page' => 6,
          'orderby' => 'date',
          'order' => 'DESC',
          'cat' => $_POST['category'],
          'paged' => $_POST['paged'],
        ]);
      • Alessandro Amato says:

        Hi Bob,
        thanks for your answer. I’m trying this out but I don’t really know how to pass the cat_id to the ajax call – how do you define that?

        I am trying to pass it the way you did in the filter post (awesome as well btw!), but it doesn’t get reset/updated when I use the filter function and the content changes in the dom. Here’s how I structured it

        // load more posts
        let currentPage = 1;
        $("#load-more").on("click", function () {
          currentPage++;
        
          $.ajax({
            type: "POST",
            url: "/wp-admin/admin-ajax.php",
            dataType: "json",
            data: {
              action: "load_more_posts",
              category: $(this).data("slug"),
              paged: currentPage,
            },
            success: function (res) {
              if (currentPage >= res.max) {
                $("#load-more").addClass("hidden");
              }
              $(".showreel").append(res.html);
            },
          });
        });

        and then this in functions.php

        function load_more_posts() {
          $catSlug = $_POST['category'];
          $ajaxposts = new WP_Query([
            'post_type' => 'post',
            'posts_per_page' => 4,
            'orderby' => 'date',
            'order' => 'ASC',
            'category_name' => $catSlug,
            'paged' => $_POST['paged'],
        ]);

        this works at first, but as soon as you reach the end of the results, the load more button is hidden (as it should be) and is never reset; so when I switch the filter I can’t load more posts anymore. Can’t figure out what I’m doing wrong.

        • Bob Weichler says:

          Hi Alessandro, You add the class ‘hidden’ when there are no more posts. But you never remove that class afterwards. You should add an else-statement that removes the hidden-class if there are posts again. (because with ajax, the page never reloads to reset the initial state)

          if (currentPage >= res.max) {
            $('#load-more').addClass('hidden');
          } else {
            $('#load-more').removeClass('hidden');
          }

          Hope it helps!

          • Alessandro Amato says:

            Hey Bob – thanks for the assistance. Unfortunately no, I’m still stuck on trying to make the load_more function work together with the filter. Did you maybe give it a try yourself to update this post? I don’t really know how to pass the category_name to the load_more_posts function once you switch the filter (so the category has changed, e.g. from ‘film’ to ‘photography’ ).

          • Bob Weichler says:

            Hi Alessandro, I’m sorry it’s not working for you. My previous example with the load_more_posts() in our response is a working example of how to combine the pagination & category filters. Another way of doing this is adding a hidden field to your page that stores the current filtered category. Once you click load-more you can read the value from that hidden input field so you’re always loading more posts from that category.
            Hope it helps!

          • Alessandro Amato says:

            Update: I made it work!
            It’s a small thing and also maybe a good addition to your post. You need to reset currentPage to 1 in the filter function, otherwise it will keep trying to load posts from page 2/3/… 🙂

            Thanks for everything!