Image showing a browser with cogs

AJAX load posts on WordPress revamped

Home > AJAX load posts on WordPress revamped
102 Comments

It turns out back in 2016 people were crazy about loading posts on their pages using AJAX. For over four years I looked at this post and thought to myself: I should update it. A long time passed since then. I am wiser and know that I should do certain things in a different way. So I decided to revamp this article.

The reason I decided to update the article was actually because of a tweet by Maddy Osman

It looks like I’m up to something here. Well yes and no.

On one hand, this post is my most visited post (477 pageviews in the last 28 days on the 12th of September 2020). But, the information, while technically correct, is a bit outdated. The original post was the result of the StackOverflow question. I answered it using TwentyFifteen theme as an example. And the post used TewentySixteen. So, naturally, the new post should use TwentyTwenty as an example 😄.

I actually have a draft of a post called ‘Deep dive into AJAX in WordPress’, but I never got around to finish it. The article was supposed to be a continuation of this one. Explaining how to use AJAX in the admin pages. I even made a small companion plugin to go along with it. But, as with most things. I didn’t have the time (what a lousy excuse).

So let’s start over.

What is AJAX?

One of the things that users on the web hate is waiting. This is where asynchronicity comes into play. Asynchronicity is the basis of modern web apps. Whether it’s using technologies like AJAX or Fetch API, reloading of any kind is avoided at all costs. When it comes to WordPress, we are usually interested in loading posts without reloading the entire page. Although we can fetch any kind of data this way.

First things first – what actually is AJAX?

AJAX stands for Asynchronous JavaScript And XML. It is a way of using many technologies together. HTML, DOM, JavaScript, XMLHttpRequest(XHR) object among others, to make incremental updates to the user interface, without reloading the browser page. In layman terms: it’s sending and retrieving data from a server without interfering with the behavior of your web page. In our case, we click on a button or scroll down a posts page and fetch new posts from a server.

We can use AJAX for fetching any kind of data from a remote resource and use it to display that data on our web page or single page application.

What we are actually doing is making a JavaScript XMLHttpRequest and sending some instructions to our server. Fetching some data or posting some data to it, and then waiting for the response, which can be successful or not. You should learn how it’s done in pure JavaScript. It’s a good exercise.

Another, more recent, technology used for getting our data asynchronously, is using the Fetch API. The difference between the two technologies (fetch and XMLHttpRequest) is that fetch is a promise based operation. Which is a good way to get rid of callback hell.

The underlining benefit of both methods is that they are asynchronous. This means that the browser isn’t locked while an action is happening. The action of getting data is happening ‘under the hood’. All the while the user is free to browse the UI and interact with it without any interruptions.

In the article, we’ll first be using jQuery’s $.ajax function, which is a wrapper around the XHR object. We use it for several reasons:

  • It’s already an existing method provided in jQuery, so we don’t have to invent our own pure JavaScript methods
  • It’s simple to use and understand
  • jQuery is already bundled in the WordPress core, so why not use it?
  • jQuery’s $.ajax also handles promises

Before we begin, I am aware many people hate jQuery. I don’t see any reason why. It’s in WordPress, it does the job, so what’s the fuss? And no matter how you try to avoid it in WordPress, you can’t. It’ll be a long time until people rewrite the core in vanilla JS. And even then, there is a huge probability that some plugin will enqueue the jQuery from WordPress. So you cannot escape it.

PHP part – preparation

I’ve said that we’ll showcase our AJAX loading on the TwentyTwenty theme. But we don’t want to directly edit the theme. That would mean that we’d loose all our modifications on the theme update. For the purpose of this demo, we’ll create a child theme called Twenty Twenty Ajax. First, create a twentytwenty-ajax folder in your themes folder. Then add style.css and functions.php files. The contents of style.css file are

/* Theme Name: Twenty Twenty Ajax Theme URI: https://example.com/twenty-twenty-ajax/ Description: Twenty Twenty Ajax Child Theme Author: Denis Žoljom Author URI: https://madebydenis.com Template: twentytwenty Version: 1.0.0 License: GNU General Public License v2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Tags: blog, one-column, custom-background, custom-colors, custom-logo, custom-menu, editor-style, featured-images, footer-widgets, full-width-template, rtl-language-support, sticky-post, theme-options, threaded-comments, translation-ready, block-styles, wide-blocks, accessibility-ready Text Domain: twentytwentyajax */
Code language: CSS (css)

We’ll have to add small style changes later on. Then in your functions.php file place

<?php add_action( 'wp_enqueue_scripts', 'twentytwenty_ajax_enqueue_styles' ); function twentytwenty_ajax_enqueue_styles() { $parenthandle = 'twentytwenty-style'; $theme = wp_get_theme(); wp_enqueue_style( $parenthandle, get_template_directory_uri() . '/style.css', array(), // if the parent theme code has a dependency, copy it to here $theme->parent()->get('Version') ); wp_enqueue_style( 'twentytwenty-ajax-style', get_stylesheet_uri(), array( $parenthandle ), $theme->get('Version') // this only works if you have Version in the style header ); }
Code language: PHP (php)

Because we want to load the parent styles (we want to add the functionality to load more posts, not change the style). You can activate the theme, and you’ll see that it looks the same as the parent theme. Now we need to change this a bit. We need to change the way the pagination works – we want to remove it. And we need to identify where we want to load our new posts. So we’ll copy the index.php from the parent theme and remove the pagination.

<?php /** * The main template file * * This is the most generic template file in a WordPress theme * and one of the two required files for a theme (the other being style.css). * It is used to display a page when nothing more specific matches a query. * E.g., it puts together the home page when no home.php file exists. * * @link https://developer.wordpress.org/themes/basics/template-hierarchy/ * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.0 */ get_header(); $cat_id = get_query_var( 'cat' ); ?> <main id="site-content" role="main" class="js-post-container" data-category="<?php echo esc_attr( $cat_id ); ?>" data-search="<?php echo esc_attr( $_GET['s'] ); ?>"> <?php $archive_title = ''; $archive_subtitle = ''; if ( is_search() ) { global $wp_query; $archive_title = sprintf( '%1$s %2$s', '<span class="color-accent">' . __( 'Search:', 'twentytwenty' ) . '</span>', '&ldquo;' . get_search_query() . '&rdquo;' ); if ( $wp_query->found_posts ) { $archive_subtitle = sprintf( /* translators: %s: Number of search results. */ _n( 'We found %s result for your search.', 'We found %s results for your search.', $wp_query->found_posts, 'twentytwenty' ), number_format_i18n( $wp_query->found_posts ) ); } else { $archive_subtitle = __( 'We could not find any results for your search. You can give it another try through the search form below.', 'twentytwenty' ); } } elseif ( is_archive() && ! have_posts() ) { $archive_title = __( 'Nothing Found', 'twentytwenty' ); } elseif ( ! is_home() ) { $archive_title = get_the_archive_title(); $archive_subtitle = get_the_archive_description(); } if ( $archive_title || $archive_subtitle ) { ?> <header class="archive-header has-text-align-center header-footer-group"> <div class="archive-header-inner section-inner medium"> <?php if ( $archive_title ) { ?> <h1 class="archive-title"><?php echo wp_kses_post( $archive_title ); ?></h1> <?php } ?> <?php if ( $archive_subtitle ) { ?> <div class="archive-subtitle section-inner thin max-percentage intro-text"><?php echo wp_kses_post( wpautop( $archive_subtitle ) ); ?></div> <?php } ?> </div><!-- .archive-header-inner --> </header><!-- .archive-header --> <?php } if ( have_posts() ) { $i = 0; while ( have_posts() ) { $i++; if ( $i > 1 ) { echo '<hr class="post-separator styled-separator is-style-wide section-inner" aria-hidden="true" />'; } the_post(); get_template_part( 'template-parts/content', get_post_type() ); } } elseif ( is_search() ) { ?> <div class="no-search-results-form section-inner thin"> <?php get_search_form( array( 'label' => __( 'search again', 'twentytwenty' ), ) ); ?> </div><!-- .no-search-results --> <?php } ?> </main><!-- #site-content --> <div class="load-more-wrapper"> <div class="load-more-wrapper--loader js-loader"></div> <button class="button button-primary aligncenter js-load-more"><?php esc_html_e('Load more posts', 'twentytwenty-ajax'); ?></button> </div><!-- .load-more-wrapper --> <?php wp_nonce_field( 'more_posts_nonce_action', 'more_posts_nonce' ); ?> <?php get_template_part( 'template-parts/footer-menus-widgets' ); ?> <?php get_footer();
Code language: HTML, XML (xml)

We’ve added the class js-post-container to the #site-content wrapper. It’s always a good idea to prefix the classes that you’ll use only with your JavaScrip with js- prefix. We won’t use them for styling. We’ve also added the button element instead of the pagination template. And the nonce field for security purposes (we’ll explain this later on).

Since we are editing the index.php, we’ve added the category field, so that we can query the category if we are in the category archive page. And the data-search attribute will account for the search functionality (forshadowing).

Using AJAX with WordPress

To use AJAX with WordPress we need to tap into WordPress’s own AJAX handler called `admin-ajax`. The AJAX is in the WordPress core since version 2.1.0. So it’s been there for quite a long time (since 2007). Its original use was to: handle post autosaves, on the fly post-editing (quick edit), or post comment approvals for instance. Which is why it has the admin prefix.

When making an AJAX call we need to specify the URL to the file which handles our AJAX requests. On the WordPress back end, we can use the JavaScript global variable ajaxurl. Since it is already built into the core. WordPress has this feature since version 2.8. Before that, you’d actually had to include your JavaScript in your PHP and hook it to the admin_footer hook. Which is something you should avoid – always separate your concerns.

To use admin-ajax.php on your front end, we need to localize it first. Localization is a way to expose certain data from the backend to the front end. That way you can use it in your JavaScript. If you’re implementing the AJAX method on the admin page, you can skip this part. WordPress loads it automatically on the admin side. Read more on Codex, or in plugin developer handbook. I’ll also add a way for your AJAX to work with the popular WPML plugin. In functions.php we’ll add our own JS file where we’ll handle ajax loading and localize our admin-ajax.php

add_action( 'wp_enqueue_scripts', 'twentytwenty_ajax_enqueue_scripts' ); function twentytwenty_ajax_enqueue_scripts() { $theme_version = wp_get_theme()->get( 'Version' ); $script_handle = 'twentytwenty-ajax'; wp_enqueue_script( $script_handle, get_stylesheet_directory_uri() . '/assets/js/index.js', array( 'jquery' ), $theme_version, false ); // Include WPML case. if( in_array( 'sitepress-multilingual-cms/sitepress.php', get_option( 'active_plugins' ) ) ){ $ajaxurl = admin_url( 'admin-ajax.php?lang=' . ICL_LANGUAGE_CODE ); } else{ $ajaxurl = admin_url( 'admin-ajax.php'); } wp_localize_script( $script_handle, 'twentyTwentyAjaxLocalization', array( 'ajaxurl' => $ajaxurl, 'action' => 'twentytwenty_ajax_more_post', 'noPosts' => esc_html__('No older posts found', 'twentytwenty-ajax'), ) ); }
Code language: PHP (php)

You see that I added the check if the WPML is present, and in that case, we’ve added the ?lang= attribute at the end of the ajax URL. This is necessary so that the AJAX call will work in different languages. In case you don’t have it, you only need to add the admin_url( 'admin-ajax.php' ) to your code. After that, we’ve localized our variables to the ‘twentytwenty-ajax’ script. The script where we’ll put the AJAX code – here it’s index.js file. We used the handle twentyTwentyAjaxLocalization. Localization will make your ajaxurl, action, and noPosts variables part of the twentyTwentyAjaxLocalization object. That way they will be available to use in our AJAX call.

Developer tools showing the twentyTwentyAjaxLocalization object.
Developer tools showing the twentyTwentyAjaxLocalization object.

Load post functionality

To actually load your posts, you’ll need to create a function you’ll call on AJAX that will render your posts. You can either create a separate file to include it in or put it in the functions.php file at the end. I’ll be doing the latter for the simplicity of it.

We’ll start simple and expand on it as we go

add_action( 'wp_ajax_nopriv_twentytwenty_ajax_more_post', 'twentytwenty_ajax_more_post_ajax' ); add_action( 'wp_ajax_twentytwenty_ajax_more_post', 'twentytwenty_ajax_more_post_ajax' ); function twentytwenty_ajax_more_post_ajax() { if ( ! isset( $_POST['more_posts_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['more_posts_nonce'] ), 'more_posts_nonce_action' ) ) { return wp_send_json_error( esc_html__( 'Number not only once is invalid', 'twentytwenty-ajax' ), 404 ); } wp_send_json_success( $_POST, 200 ); }
Code language: PHP (php)

Notice the wp_ajax_ action hook. When using ajax on the admin pages we only need the wp_ajax_{$action} and not wp_ajax_nopriv_{$action} action hook. This is because when we are on the admin pages we are already logged in. So we don’t need the nopriv action hook which is used for users that are not logged in.

First, we want to be sure that the AJAX call came from the screen that we wanted it to come, with proper authorization. That’s why we are using WordPress nonces. After the nonce check passed, we can do whatever we want in the callback method.

Nonces or number valid only once, are security tokens generated to help protect URLs and forms from misuse. Because we are dealing with sending a directive through JS towards our server, it’s a good practice to have extra security in place. It’s a form of cross-site request forgery (CSRF) prevention.

We can add a bunch of logic to this, but for now, let’s verify that this callback works. We’ll add some code to our index.js file.

jQuery(document).ready(function($) { 'use strict'; const $wrapper = $('.js-post-container'); const $button = $('.js-load-more'); const $loader = $('.js-loader'); const $nonce = $('#more_posts_nonce'); const postsPerPage = $wrapper.find('.js-post:not(.sticky)').length; const category = $wrapper.data('category'); const search = $wrapper.data('search'); $button.on('click', function(event) { loadAjaxPosts(event); }); function loadAjaxPosts(event) { event.preventDefault(); if (!($loader.hasClass('is-loading') || $loader.hasClass('no-posts'))) { const postNumber = $wrapper.find('.js-post:not(.sticky)').length; $.ajax({ 'type': 'POST', 'url': twentyTwentyAjaxLocalization.ajaxurl, 'data': { 'postsPerPage': postsPerPage, 'postOffset': postNumber, 'category': category, 'search': search, 'morePostsNonce': $nonce.val(), 'action': twentyTwentyAjaxLocalization.action, }, beforeSend: function () { $loader.addClass('is-loading'); } }) .done(function(response) { console.log(response); $loader.removeClass('is-loading'); }) .fail(function(error) { }); } } });
Code language: JavaScript (javascript)

First, we define our constants – things that will be set when our page loads and don’t change. These are usually containers, buttons, nonce field. We’ve also added a postsPerPage variable, that will define how many posts we’ll load. Finding them out this way ensures you can actually change the number in the Settings menu. We are also excluding sticky posts because we’ll exclude them from the query. They’ll always show at the beginning of the post list. Another way to define posts per page would be to read the setting’s value from the database. We could add this information to a data-postsPerPage attribute that we could then read in our JS.

But wait, what is this .js-post class? Where did that come from?

Remember how we mentioned you should separate your concern? For easier post manipulation, we’d like depend on something concrete. Sure, we could use .type-post class, but it’s better to use js prefixed classes. To be honest, in the beginning of rewriting this I used .post class, but it turns out, not every post has this class. So how do we add this custom class? With a filter. In our functions.php file add

add_filter( 'post_class', 'twentytwenty_ajax_add_js_post_class', 10, 3 ); function twentytwenty_ajax_add_js_post_class( $classes, $class, $post_id ) { $classes[] = 'js-post'; return $classes; }
Code language: PHP (php)

It was that simple. You can change this further, by checking the post type, etc. But this will work for our purposes.

We defined the postNumber variable inside our loadAjaxPosts callback. That way, whenever you click on a load more button, you’ll get the correct number to offset your posts in the query. You can also trigger the load posts by scrolling. In that case, you’ll need either waypoints.js or Intersection Observer API.

Let’s go back to our PHP part.

add_action( 'wp_ajax_nopriv_twentytwenty_ajax_more_post', 'twentytwenty_ajax_more_post_ajax' ); add_action( 'wp_ajax_twentytwenty_ajax_more_post', 'twentytwenty_ajax_more_post_ajax' ); function twentytwenty_ajax_more_post_ajax() { if ( ! isset( $_POST['morePostsNonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['morePostsNonce'] ), 'more_posts_nonce_action' ) ) { return wp_send_json_error( esc_html__( 'Number not only once is invalid', 'twentytwenty-ajax' ), 404 ); } $posts_per_page = ! empty( $_POST['postsPerPage'] ) ? (int) $_POST['postsPerPage'] : 1; $offset = ! empty( $_POST['postOffset'] ) ? (int) $_POST['postOffset'] : 0; $category = ! empty( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; $search = ! empty( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; $query_args = array( 'post_type' => 'post', 'post_status' => 'published', 'perm' => 'readable', 'posts_per_page' => $posts_per_page, 'offset' => $offset, 'post__not_in' => get_option( 'sticky_posts' ), ); if ( ! empty( $category ) ) { $query_args['cat'] = $category; } if ( ! empty( $search ) ) { $query_args['s'] = $search; } $posts_query = new WP_Query( $query_args ); $posts_out = ''; ob_start(); if ($posts_query->have_posts()) { while ($posts_query->have_posts()) { $posts_query->the_post(); echo '<hr class="post-separator styled-separator is-style-wide section-inner" aria-hidden="true" />'; get_template_part( 'template-parts/content', 'post' ); } } $posts_out = ob_get_clean(); wp_reset_postdata(); wp_send_json_success( $posts_out, 200 ); }
Code language: PHP (php)

After nonce checks, we check for other variables we can extract from the $_POST superglobal array. You can always error_log or debug this using Xdebug. Or you can check the dev tools when you click the load more posts button

Developer tools showing the contents of the ajax callback
Developer tools showing the contents of the ajax callback

We have our AJAX action callback ready. Let’s finish the JS part and add some CSS styles so that our loading looks a bit more user friendly.

When we click the button in the front end, we trigger the click event in the JS. That will take the data, and make an AJAX request towards our admin-ajax.php script. Because we sent the action parameter with it, that script will search all wp_ajax_{$hook} (and nopriv) hooks. When it finds the one matching our action it will execute it.
In our callback, we ran a new post query with the parameters, wrapped it in an output buffer. We used output buffering because we will output HTML. Using the wp_send_json_succes() function we will send a response to our AJAX function in JS. You can see the response in the developer tools. Just click the ‘Preview’ or the ‘Response’ tab.

That means we can use it. In our .done() part of the $.ajax method, we can now see what we have in the response (HTML) and append it to the container

.done(function(response) { const contents = response.data; $wrapper.append(contents); $loader.removeClass('is-loading'); })
Code language: JavaScript (javascript)

But we also don’t want to load posts if the query doesn’t return anything. That’s why we’ll add

.done(function(response) { const contents = response.data; if (contents.length) { $wrapper.append(contents); $loader.removeClass('is-loading'); } else { $button.html(twentyTwentyAjaxLocalization.noPosts); $loader.removeClass('is-loading'); $loader.addClass('no-posts'); } }) .fail(function(error) { console.error(error) });
Code language: PHP (php)

And that’s it. The .fail()method is there in case something bad happens to our response. In our case, only ‘bad’ response would be if the number not only once isn’t valid. You can attach a message in this case, or not. That’s up to you.

We’ll add some nice style to our load more loader

.load-more-wrapper { display: flex; flex-direction: column; } .load-more-wrapper--loader, .load-more-wrapper--loader:before, .load-more-wrapper--loader:after { border-radius: 50%; width: 2.5em; height: 2.5em; -webkit-animation-fill-mode: both; animation-fill-mode: both; -webkit-animation: load7 1.8s infinite ease-in-out; animation: load7 1.8s infinite ease-in-out; } .load-more-wrapper--loader { color: #cd2653; font-size: 10px; margin: 20px auto; display: none; position: relative; text-indent: -9999em; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } .load-more-wrapper--loader.is-loading { display: block; } .load-more-wrapper--loader:before, .load-more-wrapper--loader:after { content: ''; position: absolute; top: 0; left: -3.5em; } .load-more-wrapper--loader:before { -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } .load-more-wrapper--loader:after { left: 3.5em; } @-webkit-keyframes load7 { 0%, 80%, 100% { box-shadow: 0 2.5em 0 -1.3em; } 40% { box-shadow: 0 2.5em 0 0; } } @keyframes load7 { 0%, 80%, 100% { box-shadow: 0 2.5em 0 -1.3em; } 40% { box-shadow: 0 2.5em 0 0; } }
Code language: CSS (css)

Replacing jQuery $.ajax with Fetch API?

The last thing before I go is to show how you can fetch posts using Fetch API. We can use WordPress REST API to fetch the posts.

First, we’ll change the localization object. In functions.php add

wp_localize_script( $script_handle, 'twentyTwentyAjaxLocalization', array( 'ajaxurl' => $ajaxurl, 'action' => 'twentytwenty_ajax_more_post', 'root' => esc_url_raw( rest_url() ), 'noPosts' => esc_html__('No older posts found', 'twentytwenty-ajax'), ) );
Code language: PHP (php)

If we would use REST API the JavaScript part would look something like this

jQuery(document).ready(function ($) { 'use strict'; const $wrapper = $('.js-post-container'); const $button = $('.js-load-more'); const $loader = $('.js-loader'); const $nonce = $('#more_posts_nonce'); const postsPerPage = $wrapper.find('.js-post:not(.sticky)').length; const category = $wrapper.data('category'); const search = $wrapper.data('search'); $button.on('click', function (event) { loadAjaxPosts(event); }); function loadAjaxPosts(event) { event.preventDefault(); if (!($loader.hasClass('is-loading') || $loader.hasClass('no-posts'))) { const postNumber = $wrapper.find('.js-post:not(.sticky)').length; fetch(twentyTwentyAjaxLocalization.root + 'wp/v2/posts?per_page=' + postsPerPage + '&offset=' + postNumber) .then(response => response.json()) .then(data => { if (data.length) { data.map((post) => { $wrapper.append(post.content.rendered); }); $loader.removeClass('is-loading'); } else { $button.html(twentyTwentyAjaxLocalization.noPosts); $loader.removeClass('is-loading'); $loader.addClass('no-posts'); } }) .catch(error => { console.error('Error:', error); }); } } });
Code language: JavaScript (javascript)

The drawback of this approach is that you’d only get post content (blocks). Not the post wrapper, title wrapper, etc. You’d have to construct the article wrapper yourself. We could try to call the admin-ajax.php by manually providing the URL and data

jQuery(document).ready(function ($) { 'use strict'; const $wrapper = $('.js-post-container'); const $button = $('.js-load-more'); const $loader = $('.js-loader'); const $nonce = $('#more_posts_nonce'); const postsPerPage = $wrapper.find('.js-post:not(.sticky)').length; const category = $wrapper.data('category'); const search = $wrapper.data('search'); $button.on('click', function (event) { loadAjaxPosts(event); }); function loadAjaxPosts(event) { event.preventDefault(); if (!($loader.hasClass('is-loading') || $loader.hasClass('no-posts'))) { const postNumber = $wrapper.find('.js-post:not(.sticky)').length; const data = new FormData(); data.append('postsPerPage', postsPerPage); data.append('postOffset', postNumber); data.append('category', category); data.append('search', search); data.append('morePostsNonce', $nonce.val()); data.append('action', twentyTwentyAjaxLocalization.action); $loader.addClass('is-loading'); fetch(twentyTwentyAjaxLocalization.ajaxurl, { method: 'POST', body: data }) .then(response => response.json()) .then(response => { const contents = response.data; if (contents.length) { $wrapper.append(contents); $loader.removeClass('is-loading'); } else { $button.html(twentyTwentyAjaxLocalization.noPosts); $loader.removeClass('is-loading'); $loader.addClass('no-posts'); } }) .catch(error => { console.error('Error:', error); }); } } });
Code language: JavaScript (javascript)

Notice that you must send the data as a FormData() object in order for it to work.

Did we gain much by replacing the jQuery $.ajax with fetch()? I don’t think so. In this case, it looks like an overkill.

Conclusion

In the article, you’ve seen the correct way to load your posts using the jQuery AJAX function. It’s simple and gives your theme a bit of a flare. You can do a lot with this technology, especially if you couple it with WordPress REST API. In that case, I would opt in using fetch over jQuery, but those cases go beyond the simple WordPress theme.

You can download the code from this article from my GitHub repo (or fork it).

I hope you find this tutorial useful, leave a comment if you have any questions. Happy coding!

Help spreading the word

102 comments

  1. Pingback: Creating a menu page and saving it with AJAX – Made by Denis

  2. Pingback: My developer tips and tricks – Made by Denis

  3. Jean Candice Yu

    on

    Reply

    Hello! Thank you for sharing! I tried to implement it on the website I’m working on but I always get “ReferenceError: ajax_posts is not defined” even after adding the scripts to functions.php and my JS file. What do you think I missed?

    • Jean Candice Yu

      on

      Reply

      Here’s the script on functions.php:

      wp_register_script( ‘script_js’, get_template_directory_uri().’/js/script.js’ );
      wp_localize_script( ‘script_js’, ‘ajax_posts’, array(
      ‘ajaxurl’ => admin_url( ‘admin-ajax.php’ )
      ));
      wp_enqueue_script( ‘script_js’ );

      • Denis Žoljom

        on

        Reply

        Are you sure that your script is actually enqueued? In your console try typing `ajax_posts`, this should give you an object with the ajaxurl. If it’s not there, something has to be wrong with the enqueue function.

  4. agiorgini

    on

    Reply

    Hi, thanks for sharing. I’ve just tried copy/paste your code, just to see it working: in console I get ReferenceError: screenReaderText is not defined. I’m on a 4.9 with transcargo theme. Any help?

    Thanks

    • Denis Žoljom

      on

      Reply

      Hi! screenReaderText is name that is attached to the twentysixteen-script script handle that is specific for the Twentysixteen theme. You need to use the wp_localize_script() with the handle that responds to the main script in your theme, or the one you have placed your ajax code in :)
      So for instance you can enqueue your own custom.js script like:


      wp_register_script( 'some_handle', 'path/to/custom.js' );
      wp_localize_script( 'some_handle', 'object_name', array(
      'some_string' => __( 'Some string to translate', 'plugin-domain' ),
      'value' => '10'
      ) );
      wp_enqueue_script( 'some_handle' );

  5. Alen Å irola

    on

    Reply

    Hi, Dennis, thanks for sharing your code.
    I have implemented it, but I got bizarre behavior – instead of loading posts – ajax load entire page all over … It’s even stranger that it happens on default themes (twentyseveteen, twentysixteen), but not on my theme, or couple of wp.org themes I tried …
    Any thoughts on where to look …

    • Denis Žoljom

      on

      Reply

      Usually that means that the callback return is not ok. Could be something in the query. You could post your code on stackovreflow so that I can see it, and try to find the error…

      • Alen Å irola

        on

        Reply

        Hi, thanks for such a fast reply – it turned out that, after “reverse engineering” TwentySixteen (after testing more then dozen wp.org themes :) ) – it was a question of translatable js vars (wp_localize_scripts) – since you used the “screenReaderText” from enqueued “twentysixteen-script”, that messed up ajax query, seemingly.
        I changed all the instances of “screenReaderText”, and voila … it works. So I would suggest using unique variable name for $name in wp_localize_script( $handle, $name, $data ); .
        Oh, yeah, sorry, I made a separate functionality, didn’t change the twentysixteen … so, you shouldn’t change it in tut, I should have changed that …
        Inače, tek sam naknadno skužio da si iz HR … :Pozdrav iz Rijeke :)

        • Denis Žoljom

          on

          Reply

          Yeah, this article desperately needs an update. Usually inside the theme I attach the localization to the script I put the ajax call in. In this case I used the default javascript file from twentysixteen…

          Hehe pozdrav iz Zageba :D

  6. Kalyn Bradford

    on

    Reply

    How would you structure this is you wanted to load a custom post type with custom taxonomies?

    • Denis Žoljom

      on

      Reply

      Just provide the necessary info for custom post type and taxonomy in the `data` attribute so that you can ‘pick’ them up with javascript and pass them to your query when you call the callback function in your ajax.

  7. Azizul Haque

    on

    Reply

    Isn’t nonce important?

    • Denis Žoljom

      on

      Reply

      Yeah, nonce is very important, but in this case since technically you are only fetching posts back, you can omit it. In cases when you are sending data to the server, using some kind of forms, nonce is crucial.
      I’ve been planing on redoing this tutorial from scratch, cover OOP approach, but I really haven’t find time to do it…

  8. Justin Estrada

    on

    Reply

    Hellyes Thanks a bunch man! Got this working perfectly on a custom theme I wrote! You rock!

  9. michel lompret

    on

    Reply

    Hi Denis could you look at my question on stackoverflow ? http://stackoverflow.com/questions/40759697/ajax-load-more-and-query-in-wordpress

    Thanks

  10. wimhuiskes

    on

    Reply

    Thank you for this tutorial, but i cant seem to get it to work, for the load more posts part so to say.
    I was hoping you could help me by checking my project and point me in the right direction.

    http://www.restaurantbruisblaricum/nieuws

    is what i have so far.

    • Denis Žoljom

      on

      Reply

      Are you sure you have enough posts? The load more button seems disabled

      • wimhuiskes

        on

        Reply

        Yes, i am sure, there are 7 in total, in the category ‘nieuws’

      • wimhuiskes

        on

        Reply

        This my javascrip/ajax function:

        var ppp = 4; // Post per page
        var cat = 35;
        var pageNumber = 1;

        function load_posts(){
        pageNumber++;
        var str = ‘&cat=’ + cat + ‘&pageNumber=’ + pageNumber + ‘&ppp=’ + ppp + ‘&action=more_post_ajax’;
        $.ajax({
        type: “POST”,
        dataType: “html”,
        url: ajax_posts.ajaxurl,
        data: str,
        success: function(data){
        var $data = $(data);
        if($data.length){
        $(“#ajax-posts”).append($data);
        $(“#more_posts”).attr(“disabled”,false);
        } else{
        $(“#more_posts”).attr(“disabled”,true);
        }
        },
        error : function(jqXHR, textStatus, errorThrown) {
        $loader.html(jqXHR + ” :: ” + textStatus + ” :: ” + errorThrown);
        }

        });
        return false;
        }

        $(“#more_posts”).on(“click”,function(){ // When btn is pressed.
        $(“#more_posts”).attr(“disabled”,true); // Disable the button, temp.
        load_posts();
        });

        } );

  11. Kabolobari

    on

    Reply

    What I hoped to find was how this “load more” posts (whatever can be loaded) functionality could be built into a plugin which is portable and can be used on any site to affect any list page of one’s chosen. Is there such a tutorial somewhere? Thanks.

    • Denis Žoljom

      on

      Reply

      I just saw this comment, disqus didn’t notify me. Well, there are lots of plugins for post loading out there (iirc Jetpack has that built in), but it’s generally tricky to match all the things for this to work with every theme – post containers, post layouts that can be changed etc. I have planned to do a recap of this tutorial, go more in depth, but I am swamped with work atm.

  12. jin jin

    on

    Reply

    Thank you for the tutorial!
    I was able to load post titles and post thumbnails in my site, using your method.
    But I can not load “echo get_post_meta($post->ID, _aioseop_description, true)”. Actually, I don’t know how to rewrite it (in functions.php).
    Could you tell me how I should rewrite it, if you are fine?
    (I’m not a native English speaker. Sorry if my English is difficult to understand.)

  13. Hamdi PINAR

    on

    Reply

    Hello Denis, As I am not a coder, tried tonnes of plugins before to get this function. Your plugin is the only one which worked upon the activation. Many thanks. The only problem, whenever I click the “load more posts” the plugin removes the sidebar of my original theme an loads post snippets and thumbnails with a larger (full screen) view. Is there anything that I can do to fix it?

    • Denis Žoljom

      on

      Reply

      You must be careful to what you’re appending your post. The above code will append posts in the `.ajax_posts` container, and it shouldn’t remove the sidebar. It’s hard to tell what went wrong without seeing the code, but you can always post your question on SO and link it here so that I can take a look :)

      • Hamdi PINAR

        on

        Reply

        I think the problem is that the script doesn’t fetch my blog’s custom css. Anyway, I liked it in that way too. My loaded posts look very unique :)

  14. sami

    on

    Reply

    Hi Denis great tuto but i have a question.
    Why in the function.js you do that

    $loader.removeClass(‘post_loading_loader’);
    $newElements.animate({ opacity: 1 });
    $loader.removeClass(‘post_loading_loader’).html(screenReaderText.loadmore);
    I mean why you removeclass of post_loading_loader twice ?

  15. roberto solini

    on

    Reply

    Denis
    The modification of post_date by date doesn’t show anything neither but i change the function.js

    from :

    jQuery(document).ready(function($) {
    var ppp = 3; // Post per page
    var offset = $(‘#ajax-posts’).find(‘.posts’).length;
    var $content = $(‘#ajax_posts’);
    var $loader = $(‘#more_posts’);

    $loader.on( ‘click’, load_posts );

    function load_posts(){

    if (!($loader.hasClass(‘post_loading_loader’) || $loader.hasClass(‘post_no_more_posts’))) {

    $.ajax({
    type: “POST”,
    dataType: “html”,
    url: screenReaderText.ajaxurl,
    data: {
    ‘ppp’: ppp,
    ‘offset’: offset,
    ‘action’: ‘more_post_ajax’
    },

    beforeSend : function () {
    $loader.addClass(‘post_loading_loader’).html(”);
    },
    success: function (data) {
    var $data = $(data);
    if ($data.length) {
    var $newElements = $data.css({ opacity: 0 });
    $content.append($newElements);
    $loader.removeClass(‘post_loading_loader’);
    $newElements.animate({ opacity: 1 });
    $loader.removeClass(‘post_loading_loader’).html(screenReaderText.loadmore);
    } else {
    $loader.removeClass(‘post_loading_loader’).addClass(‘post_no_more_posts’).html(screenReaderText.noposts);
    }
    },
    error : function (jqXHR, textStatus, errorThrown) {
    $loader.html($.parseJSON(jqXHR.responseText) + ‘ :: ‘ + textStatus + ‘ :: ‘ + errorThrown);
    console.log(jqXHR);
    },
    });
    }

    return false;
    }

    });

    to

    jQuery(document).ready(function($) {
    var ppp = 3; // Post per page
    var offset = $(‘#ajax-posts’).find(‘.posts’).length;
    var $content = $(‘#ajax_posts’);
    var $loader = $(‘#more_posts’);

    function load_posts(){

    $.ajax({
    type: “POST”,
    dataType: “html”,
    url: screenReaderText.ajaxurl,
    data: {
    ‘ppp’: ppp,
    ‘offset’: offset,
    ‘action’: ‘more_post_ajax’
    },

    success: function(data){
    var $data = $(data);
    if ($data.length) {
    $(“#ajax-posts”).append($data);
    $(“#more_posts”).attr(“disabled”,false);
    } else{
    $(“#more_posts”).attr(“disabled”,true);
    }
    },
    error : function (jqXHR, textStatus, errorThrown) {
    $loader.html($.parseJSON(jqXHR.responseText) + ‘ :: ‘ + textStatus + ‘ :: ‘ + errorThrown);
    console.log(jqXHR);
    },
    });

    return false;
    }
    $(“#more_posts”).on(“click”,function(){ // When btn is pressed.
    $(“#more_posts”).attr(“disabled”,true); // Disable the button, temp.
    load_posts();
    });

    });

    I have no idea why the first one didn’t show anything

  16. roberto solini

    on

    Reply

    Denis I’m trying to get the latest posts but i’ve got some problem to show the posts. I’ve post a question on SO. http://stackoverflow.com/questions/39150087/ajax-load-posts-in-wordpress
    If you got the time to look at it
    Thanks

  17. Jorge Rivero

    on

    Reply

    Is there any way to hide the load more button when the list of posts don’t reach the ppp? For example: My ppp is 9 but I have 6 posts.

    Thanks in advance

  18. Pankaj Kumar

    on

    Reply

    What if i already has a post present .. ? The problem is lets take i have 5 post with title.. Posts 1, Post 2, Post 3, Post 4, Post 5 ,, then the problem is if Post 3 is already present and i pressed over the load more link then the same post is getting loaded again ?? how can i escape the existing posts and show the rest ?

  19. roberto solini

    on

    Reply

    Hi Denis great tuto
    when I read it I immediately want to add to the page builder I use.
    There ‘ s no problem with the php part but there’s some problem with the js part.
    As the page builder allow to have for example 5 more posts from sports category with load more button and 4 more posts from news category with load more button. I think i could store this info like you do with data-category in the load more button so we’ll have

    <div id="more_posts" data-category="” data-number=”x”>

    and then use jquery each function to loop through all data-category and data-number in order to use them in

    function load_ajax_posts(). What do you think of this method ?

    Do you see an easier way of doing that ?

    • Denis Žoljom

      on

      Reply

      Well multiple logo is possible, but you need to specify what you want to do with them. Load different categories on click, load different posts, or just load new posts by clicking on the same button? It’s all possible, but would require a small modification of the code depending on what you need…

      • roberto solini

        on

        Reply

        Denis I don’t understand the if ($data.length) in the function.js because it seems to me that there must be a condition like $data.length < something. What condition do you look when you do if ($data.length)

        • Denis Žoljom

          on

          Reply

          The condition if($data.length) is basically if($data.length>0) It checks if the $data (jQuery object) is empty or not (the data is returned html from the ajax call, and by wrapping it in $(data) we ‘convert’ it to jQuery object for easier manipulation).

  20. Pankaj Gupta

    on

    Reply

    Hi,
    I want to add load more button on my woocommerce archive (shop) page. Pls help me.

    • Denis Žoljom

      on

      Reply

      WooCommerce is a bit different, you’d need to disable the pagination, and modify the functions to load post type product, and you’ll need to find a hook to add the load more button. It’s not a trivial task and would probably warrant a tutorial of it’s own. Which is not a bad idea for future tutorial :D

  21. sonia maklouf

    on

    Reply

    Hi Denis, thanks for your tutorial.
    Can you take a look to http://stackoverflow.com/questions/38994626/ajax-load-more-posts-button ?

    Thanks

  22. Rumman Amin

    on

    Reply

    Hi Denis, fantastic post. Just what I was looking for! Have a couple of questions that i’d really appreciate if you could answer for me. The additional posts are loading fine, but for some reason they are being duplicated, any idea why that might be? I had a look in the Networks tab and it seems each time I press the button, admin-ajax.php runs twice which causes this duplication.

    Also, as per your tutorial, by default it loads up normal ‘posts’ but what about custom posts? Would I need to duplicate the entire thing to get it to work for a custom post type on a separate page as well as the normal post type?

    Thanks,
    Rumman

    • Denis Žoljom

      on

      Reply

      Looks like your script loads twice, you can try to use a trick to prevent double click. The script should work fine (I’m using it here on the front page). The script should work fine for custom posts but you’d need to modify the query to load them (and add the category query string to the script). I’m not on my computer so I cannot answer the post on so unfortunately.

  23. Justin

    on

    Reply

    Great work, Denis! I’m doing something wrong, it seems. I’ve attempted to do this on a fresh install of Twenty Sixteen to get it right but when I click the load more button it loads the entire index.php page again. Can’t figure where I erred so I thought I would ask… Thanks again!

  24. Nageena Akbar

    on

    Reply

    Hello
    I used your code but i have this error ReferenceError: ajax_posts is not defined
    pls tell me why i am facing this

  25. jose fano

    on

    Reply

    excelent tuto!! , where can I see a working demo of this?

    • Denis Žoljom

      on

      Reply

      Hmmm, see I haven’t really given this a thought :D I’ll try to set up a demo site and link to it in the future. Thanks for the suggestion :)

      • jose fano

        on

        Reply

        Yea but really this tuto is excellent, helped me a lot :)

        • jose fano

          on

          Reply

          Do you see and advantage on using an external handler for the Ajax calls? I’ve seen a lot of people using that method too claiming that makes it faster.
          I was able to do my own Ajax calls with the guidance of ur tutorial and couple of others I found

          • Denis Žoljom

            on

            External handler as in admin-ajax.php? It’s hardly external, it’s a part of WordPress AJAX API, so it’s the best option actually :)

          • jose fano

            on

            Yea some people claim that using a different Ajax handler (not admin-ajax) can improve speed, because it would not include the whole wordpress environment, so it would be a light weight version. Although I’ve used that method I don’t see that much speed difference in my case, very minimal

          • Denis Žoljom

            on

            Oh, you’re not including the whole environment. That’s the thing. Before people would include the whole wp-load.php when calling ajax (because they just needed admin-ajax.php) like:

            include "../../../wp-load.php";

            This is a bad pracitce, and should never be used.
            But that’s why you pull only admin-ajax.php with your wp_localize_script, and you can use only it. I didn’t look into performance gains by using some outside handler vs the one that comes with WP, but I don’t think there should be a big difference. I’m all for the approach: use what you’re given :)

    • Denis Žoljom

      on

      Reply

      Hi jose,

      I’ve set my front page to use the same code as above (with modifications to fit my posts style of course), so you can see how it’s working now :)

  26. Sven Parker

    on

    Reply

    Thanks for the help! One thing though, i had to declare $category_out=array(); in functions.php for my categories to work. Maybe add it into the code on the site or somebody might just see this comment and figure it out, anyway good luck!.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.