Ajax load more posts from custom post types in Wordpress (using WP API)

Updated on August 6, 2020


Ajax load more posts from custom post types in WordPress

Last week I made my first WordPress ajax load more posts call to retrieve posts from a custom post type. I made the Ajax call simply because of the fact that every time I clicked ‘next’ or ‘previous’, the page would reload and jump back to the top. With ajax, my page would not reload, and therefore stays on its original position. By writing this post I hope to retrieve a better understanding on how this works myself, as I also hope of course it could help someone else who’s giving it’s first looks at ajax and wordpress.

Ajax wordpress custom post types

This post is pretty long as I gave a lot of extra examples to make it more clear. But the only 3 files we’ll need to make this work is a posts.php page that will display all our posts. The WordPress functions.php file to setup our WordPress API and a custom.js file to write down all of our ajax/js code.

Project example where we can use ajax

I’m going to give an example on how to use WordPress Ajax based on a past project. My task was to load more posts from a custom post type on scroll. The website was using Pjax to handle page transitions and increase page load times. The problem with this was that ‘load more ajax posts’ plugins were not triggered or even loaded when I navigated to the posts page. Because of this I needed to create my own load more ajax script and I found it the easiest way to do this by using the default WordPress API of the website.

The Setup

Every recent version of WordPress will come with the WordPress API built-in. To see the JSON file of your website, vitis yourwebsite.com/wp-json. Normally this should return you a JSON file without any errors. To see your WordPress posts or posts from a specific post type, we should be able to navigate to yourwebsite.com/wp-json/projects, as /projects should be the name of your custom post type. But… When we try this, the page should return a JSON error file telling us that there was “No route found matching the URL and request method”.

To specify a working URL, we need to initiate a REST API route in our functions.php file. It’s no big deal and the following code will make /wp-json/projects a working URL for our website.

add_action('rest_api_init', 'custom_api_get_projects');
function custom_api_get_projects(){
  register_rest_route( 'projects', '/all-posts', array(
    'methods' => 'GET',
    'callback' => 'custom_api_get_projects_callback'

In this example you’ll see we use the register_rest_route function to specify a route for our Projects (custom post type) and a URL ‘/all-posts’. If you added this to your project, you should be able to access yoursite.com/wp-json/project and yoursite.com/wp-json/projects/all-projects.

Page setup

Our PHP template that will display our projects actually doesn’t need an extra update for ajax loading to work. Unless you are writing your jQuery script in-page instead of in a separate .js file. Anyway, I’ll drop here an example of my WP_Query and loop:

$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$get_projects = new WP_Query(array( 
	'post_type'     => 'project', 
	'status'        => 'published', 
	'posts_per_page'=> 9,
	'orderby'	=> 'post_date',
	'order'         => 'DESC',
	'paged'         => $paged

Make sure to add pagination to the query! This will give you the option on how many posts you want to display and when our ajax needs to load the next batch of posts.

The loop for displaying the first page of our project (in my case, 9 projects will be shown):

<ul class="project-list"><?php while($get_projects->have_posts()) : $get_projects->the_post(); ?>
 	<li class="post"><strong>View Project</strong></li>
if($get_projects->post_count < 9){
 	<div class="load-more"&gta class="btn secondary-button">no more projects</a></div>
 	<div id="project-loader" class="loading-banner"><a class="btn" href="#!">Load more posts</a></div>
 	<p class="no-results">No results found for your filter. Please try again.</p>

Please note: I couldn’t add tags into my code, because it displayed special characters in my WordPress editor. Please excuse me for this, but this should be nothing new and looks like a regular WordPress loop. Also at the end, I’ll check if my query returns less than 9 posts. If so, we don’t need a load more button, because we already have all the posts.


Finally, here we go! Adding some jQuery fun to the project. But not so faaast… we still need to create 1 more thing in our functions.php file. Because our ajax call need to load more posts from our WordPress site, this obviously needs to be done in PHP. Add the following code also to your functions.php file:

function custom_api_get_projects_callback($request){
    $posts_data = array();
    $paged = $request->get_param('page');
    $paged = (isset($paged) || !(empty($paged))) ? $paged : 1;
    $posts = get_posts( array(
      'post_type'       => 'project',
      'status'          => 'published',
      'posts_per_page'  => 9,
      'orderby'         => 'post_date',
      'order'           => 'DESC',
      'paged'           => $paged
    foreach($posts as $post){
      $id = $post->ID;
      $post_thumbnail = (has_post_thumbnail($id)) ? get_the_post_thumbnail_url($id) : null;
      $post_cat = get_the_category($id);
      $featured = (get_field('project_featured', $id)) ? true : false;
      $posts_data[] = (object)array(
        'id' => $id,
        'slug' => $post->post_name,
        'type' => $post->post_type,
        'title' => $post->post_title,
        'featured_img_src' => $post_thumbnail,
        'featured' => $featured,
        'category' => $post_cat[0]->cat_name
    return $posts_data;

The get_posts array at the top, looks the same as the WP_Query loop we have on our page. So we’re loading the correct posts into the page. Before we run the get_posts(), we’re checking if our URL gave us a specific page to load. If we’re already on page 3 of our pagination, the script needs to query only the posts for page 4.

After our query we’ll create a new loop that will set up our new posts data as a JSON file. You can add as many data fields to this as you want. I’ve added a few extra’s to give an example. If you run print_r($post); exit; in the foreach, you’ll be able to see which fields you can use. I provided you with a hidden Custom Field example as well.

In my foreach loop, I’m declaring a variable called $featured. In my project I needed this to add an extra ‘featured’ label to my project. I’ve added this in the example in case you need something like this as well in your project.

Ajax script

Ok now for real, here is the ajax code that I’m using in my project. It’s a bit long, but I think it’s pretty basic. It has my full example, so you can remove whatever you don’t need, but I provided it anyway to show you what’s possible. The getSJON() function will call our WP-API url and adds a ?page parameter to pull the correct page. If our call has data, we’ll loop over this data and push each posts already formatted as a string to an array.

After our loop we’ll append the entire array with all of our new posts to the existing post list we already have on our page. I added an extra check to see if our query could get more than 9 posts or not. If not, we reached the end of our page and our ajax script won’t need to run again.

let pull_page = 1; let jsonFlag = true;
jsonFlag = false; pull_page++;
$.getJSON("/wp-json/projects/all-posts?page=" + pull_page, function(data){
		var items = [];
		$.each(data, function(key, val){
			const arr = $.map(val, function(el) { return el });
			const post_url = arr[1];
			const post_title = arr[3];
			const post_img = arr[4];
			const post_featured = arr[5];
			const post_cat = arr[6];
			const post_class = (class_counter == 2) ? 'post adjust' : 'post';
			let featured = "";
				featured = "featured";
			let item_string = '<ul><li class="item">' + post_title + '</li></ul>'; 
		if(data.length >= 9){ 
	if(data.length){ jsonFlag = true; } 

At the start of our JS file we declare a jsonFlag and a pull_page variable. The jsonFlag will prevent our json script to run multiple times when it’s already doing his job. if our script is running, the jsonFlag will be false, and our script can’t run again. If our script is done, and it returned new posts, we set our jsonFlag back on true, and we are able to call our script again. You can also use this script to set a ‘disabled’ state to your load-more button.

The jsonFlag is set to false if the script runs for the first time. ONLY if our script returns more than 9 posts (or whatever you have in your pagination) we can put the flag back on True. If it returns less than 9 posts, this means we won’t have any more posts to load.

Ajax load more posts on click

The function we made before is actually already good to go for an onClick event. Wrap the entire code into a .on(‘click’) and the button should return the new posts every time you click it.

Ajax load more posts on scroll

To wrap our previous script in an .on(‘scroll’) event, we’ll need to add some extra validation. On scroll will run on each page, instead of only the page we have our posts on.

$(document).on('scroll', function(){
	const fromBottom = $(window).scrollTop() + $(window).height();
	if($('#project-list').length && $('#project-loader').length){
		const loader_height = $('#project-loader').offset().top + $('#project-loader').height();
			/* our previous ajax script */
In our scroll function, we check if our project list is on the page or not. If not, we don't need to run our scroll script. If it is present on the page, we also check if our project-loader button is present on the page. Because we are hiding our button if there are no more posts. So if there are no more posts, the button is hidden and our script won't run again on scroll.

– Extra –

I’m adding some extra examples to this post because I had a few extra problems I needed to solve for my project. So maybe you are running into those as well.

Ajax load posts from specific category

To load custom post types with a specific category, we only need to update our WP_Query script, in both our posts.php page AND our functions.php page. The query to load custom post types with a category looks like this. Please note that I am getting the requested category by ID and that I add it to the page url as a $_GET[] parameter.

$get_cat = $_GET['cat']; // ONLY IN POSTS.PHP
$get_cat = $request->get_param('cat'); // ONLY IN FUNCTIONS.PHP
$get_projects = new WP_Query(array( 
	'post_type'     => 'project', 
	'status'        => 'published', 
	'posts_per_page'=> 9,
	'order'         => 'DESC',
	'category__and' => $get_cat,
	'paged'         => $paged

This needs to be updated in both our posts.php and functions.php file. I hope it’s a bit clear in my code-example, but in our posts.php page we are getting the category from the URL by checking $_GET[‘cat’]. BUT, in functions.php we need to use $request->param(‘cat’) instead of $_GET[‘cat’] to get the same result.

The easy thing (I think) with using the WP API for this kind of stuff, is that we can easily update our Ajax url if we want changes like this. In our query we look for a GET parameter with the name ‘cat’. So now we just need to update our jQuery and provide this parameter in our Ajax URL as well:

let cat_string = $('#le_current_cat').text();
$.getJSON("/wp-json/projects/all-posts?page=" + pull_page + "&cat=" + cat_string, function(data){
	/* the regular ajax script */

Because jQuery can’t read PHP and I have all my jQuery in a separated file, I’ll put the current category of the page in a hidden div with the ID#le_current_cat. In my jQuery file I can just read out on what category we are filtering and pass this to my URL

Ordering posts based on a Custom Field

This was also a struggle. I needed to stick all ‘featured’ projects to the top of the page, and also my ajax script needed to load all features project first, before loading all the rest. This can be done by ordering our posts on my custom field ‘featured’ instead of on date. To fix this we don’t need to touch our ajax script. Only our PHP Query on both the posts.php page and in our functions.php file.

$get_projects = new WP_Query(array( 
      'post_type'       => 'project',
      'status'          => 'published',
      'posts_per_page'  => 9,
      'meta_key'        => 'project_featured',
      'orderby'         => 'meta_value_num',
      'order'           => 'DESC',
      'category__and'   => $get_cat,
      'paged'           => $paged

To filter on custom posts types we’re using the ‘meta_key’ with the custom field we gave it. In my case this ACF field is called ‘project_featured’ and will just return true/false. So I’m ordering all the posts with TRUE first and all the posts that are not a featured project after that.

So this is how I made a custom ajax loader on scroll using the WordPress API. I know this post is huge, but I hope it helped you out! I gave a lot of examples and extra’s, but maybe I’ll split this post into 2 separate posts to make it more clear. Just the basic ajax loader and then all the other extra’s…

Anyways, I hope could understand it and that you found it helpful 🙂

Best Bob

Leave a Reply

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


  • lol says:

    very big PHP SINTAXIS ERROR !!!!!!!

    have_posts()) : $get_projects->the_post(); ?>
    View Project

    if($get_projects->post_count < 9){
    no more projects
    Load more posts
    No results found for your filter. Please try again.

    • Bob Weichler says:

      Hi ‘lol’, you didn’t start your while loop after the if($get_projects->have_posts()) { … }. You should also add a while($get_projects->have_posts()). Right now, you have an “endwhile;” but you never started the while loop. Hope it helps!

  • Subhash dhaka says:

    hey please provide some code for show up related post with load more on swipe

  • Purushottam says:


    I have followed the tutorial, it’s working great, but there is one problem, it’s displaying the same content on clicking load more, it’s not breaking from each function, can you please give me any suggestion regarding this?

    • Bob Weichler says:

      Hi Purushottam,
      Thank you for your comment. Can you check that clicking on load-more is actually updating your $paged variable? Also make sure that your ‘posts_per_page’ is equal to your WordPress settings under ‘Settings > Reading > Blog pages show at most’. Hope it helps!

  • Fort says:

    Many thanks to you.

  • Min Somai says:

    very helpful post. thank you

  • Jan-Klaas says:

    Very helpfull, Bob.

    The jsonFlag variable in your javascript checks if an ajax response is empty, so you disable anymore unnecessary ajax requests. But still, you have one ajax request too much this way.

    Instead, you could send the total pages with the response.

    function custom_api_get_projects_callback($request){
    $post_per_page = 1;
    $count_posts = wp_count_posts( ‘project’ )->publish;

    $posts_data[] = (object)array(
    ‘id’ => $id,
    ‘total_pages’ => ceil ( $count_posts / $post_per_page )

    let total_pages;
    if(! total_pages || pull_page < total_pages){
    $.getJSON('/wp-json/projects/all-posts?page=' + pull_page, function(data){
    total_pages = data[0].total_pages;


  • Aps says:

    Thank You, It is really Helpfull.

  • Ajeesh TP says:

    In my site i would like to add Ajax load more option instead of pagination.
    i am using ACF, using query posts can i create load more option? please help me.
    here is my query

    $paged = (get_query_var(‘paged’)) ? get_query_var(‘paged’) : 1;
    $gallery_arg = array(
    ‘paged’ => $paged,
    ‘post_type’ => ‘gallery’,
    ‘posts_per_page’ => 1,
    ‘order’ => ‘ASC’,
    ‘orderby’ => ‘menu_order’,
    $gallery_query = query_posts($gallery_arg);

  • justin says:

    love the bottle of Fat Tire in the background. 🙂

  • dikc says:

    Woow, thank you so much!,This is very useful.

  • omid says:

    tanks . you have best websites. good luck