<div class="container pt-5">

    <h1>WordPress posts archive with REST API - A practical example</h1>
    <a href="https://templateartist.com/2020/05/12/wordpress-custom-posts-archive-with-rest-api-and-ajax/" >Tutorial for WordPress posts archive with REST API and ajax</a>

    <hr>

    <div class="api-grid my-5">

        <aside class="post-categories">
            <div class="categories-container">
                <h3>Categories</h3>
                <ul id="categories">
                    <div class="text-center">
                        <div class="spinner-border m-3" role="status"></div>
                        <p>Loading...</p>
                    </div>
                </ul>
            </div>
        </aside>

        <section class="posts-wrap">
            <h3>Articles</h3>

            <div id="all-posts" class="all-posts" data-per-page="4">
                <div class="text-center">
                    <div class="spinner-border m-3" role="status"></div>
                    <p>Loading...</p>
                </div>
            </div>
            <button id="load-more" class="load-more btn btn-primary end-page">Load More</button>
        </section>

    </div>
</div>
.api-grid {
    display: flex;
    flex-direction: column;
}

.post-categories, 
.posts-wrap {
    margin-top: 30px;
}

.post-categories ul{
    padding: 0;
    margin: 0;
}
.post-categories ul li{
    list-style: none;
    margin-bottom: 10px;
}
.post-categories ul li a{
    display: block;
    padding: 10px 20px;
    background-color: #dddddd;
    color: #333333;
    transition: all ease 0.3s;
}
.post-categories ul li a:hover,
.post-categories ul li a.current
{
    background-color: #333333;
    color: #dddddd;
    text-decoration: none;
}

.load-more{
    margin-top: 30px;
}

.load-more.end-page{
    opacity: 0.3;
    pointer-events: none;
}

.new-post{
    opacity: 0;
}
.new-post.fadein{
    opacity: 1;
    transition: all ease 0.3s;
}

@media (min-width:600px){
    .api-grid {
        flex-direction: row;
    }
    .post-categories{
        width: 30%;
        position: relative;
    }
    .categories-container{
        position: sticky;
        top: 50px;
        padding-right: 40px;
    }
    .posts-wrap{
        width: 70%;
    }
    
}

.all-posts article{
    background-color: #f8f9fa;
    padding: 15px;
    margin-bottom: 30px;
}

@media (min-width:800px){
    .all-posts article{
        display: flex;
    }
    .all-posts article .post-title-excerpt{
        width: 100%;
        padding: 15px;
    }
}

@media (min-width: 992px){
    .container{
        max-width: 940px;
    }
}
@media (min-width: 992px){
    .container{
        max-width: 960px;
    }
}
(function($){

    //always use strict mode
    "use strict"

    let initializePosts = () => {

        /**
         * All variables
         */

        //posts container for append or replace data
        let container = $('#all-posts')

        //categories container
        let catContainer = $('#categories')

        //load more button
        let loadMore = document.querySelector('#load-more')

        //set initial current page
        let currentPage = 1;

        //set initial category ID to blank
        let categoryID = ''

        //set initial total pages to blank
        let totalPages = ''

        //get posts per page from the data attribute
        let perPage = container.data('per-page')

        //site url
        let siteurl = encodeURI('https://demo.wp-api.org/wp-json')

        //add our posts and categories for initial page load
        $.when(
            $.getJSON( siteurl + '/wp/v2/categories' ),
            $.getJSON( siteurl + '/wp/v2/posts?per_page=' + perPage )
        ).then( pageCallback, pageFailure )

        /**
         * @callback pageCallback
         * @param categories for category terms
         * @param posts for posts
         */
        function pageCallback( categories, posts ){

            //fetch and return our post categories
            if( categories[0].length ){
                console.log(categories[0])
                catContainer.html(
                    categories[0].map( cat => {

                        if( cat.count != 0 ){
                            return `<li><a class="post-category" href="#" data-term-id="${cat.id}">${cat.name}</a></li>`
                        }
                        
                    }).join('')
                )

                //store our category links for click event function
                let allCategories = $('.post-category')
                let i;

                //initialize category links click event if condition is a match
                for( i = 0; i < allCategories.length; i++ ) {
                    allCategories[i].addEventListener('click', getPostsByCategory )
                    //the callback `getPostsByCategory` is written later in the code
                }

            }

            //now, fetch and return the post data in our page
            if( posts[0].length ){
                console.log(posts)

                //store the initial total pages available for the query
                totalPages = parseInt(posts[2].getResponseHeader('X-WP-TotalPages'))

                container.html(
                    posts[0].map( post => {
                        return `
                        <article>
                            <div class="post-title-excerpt">
                                <h3><a href="${post.link}">${post.title.rendered}</a></h3>
                                ${post.excerpt.rendered}
                            </div>
                        </article>
                        `
                    }).join('')
                ).hide().fadeIn(400)

                //initialize load more posts click event
                //callback function `loadNextPage` is written later in the code 
                loadMore.addEventListener('click', loadNextPage)

                //if there are more than 1 page, 
                //remove the 'end-page' class from load more button
                if( currentPage <  totalPages){
                    loadMore.classList.remove('end-page')
                }

            }

        }//callback `pageCallback` ends here

        /**
         * @callback pageFailure
         * call this if our API url gets error
         */
        function pageFailure(){
            console.log("JSON Error for posts!")
        }//callback `pageFailure` ends here


        /**
         * @callback getPostsByCategory
         * load posts when category terms are clicked
         */
        function getPostsByCategory(e){

            e.preventDefault()

            //get the term id from the HTML data attribute
            let term_id = $(this).data('term-id')

            //this condition checks if the clicked link is already active or not
            //if clicked link not active, run this
            if( !$(this).hasClass('current') ){

                //API request
                $.getJSON( siteurl + '/wp/v2/posts?per_page=' + perPage + '&categories=' + term_id, ( posts, status, response ) => {
                    
                    //now setting the global category ID for the load more use
                    categoryID = term_id

                    //setting the global total pages for load more use
                    totalPages = response.getResponseHeader('X-WP-TotalPages')

                    //setting the current page to first page again
                    currentPage = 1

                    //checking if there are more than 1 pages
                    if( currentPage ==  totalPages){
                        //if first page is also the last page, 
                        //add 'end-page' class to load more button
                        loadMore.classList.add('end-page')
                    }else{
                        //if there are more than 1 page, 
                        //remove the 'end-page' class from load more button
                        loadMore.classList.remove('end-page')
                    }
                    
                    //getting data from the API request
                    if( posts.length ){
        
                        container.html(
                            posts.map( post => {
                                return `
                                <article>
                                    <div class="post-title-excerpt">
                                        <h3><a href="${post.link}">${post.title.rendered}</a></h3>
                                        ${post.excerpt.rendered}
                                    </div>
                                </article>
                                `
                            }).join('')
                        ).hide().fadeIn(400)
        
                    }
                    
                })

                //get all .current items
                let current = $(".current");

                //remove class .current from all items
                current.removeClass('current')

                //set .current class to the clicked item
                $(this).addClass('current')

            }
            
        }//callback `getPostsByCategory` ends here

        /**
         * @callback loadNextPage
         * load next page function
         */
        function loadNextPage(e){

            e.preventDefault()

            //run this if category links are clicked
            if( categoryID != '' && currentPage < totalPages ){
                
                //increment current page number
                currentPage = currentPage + 1;

                //API request
                $.getJSON( siteurl + '/wp/v2/posts?per_page=' + perPage + '&categories=' + parseInt(categoryID) + '&page=' + currentPage, data => {
                    
                    //getting data from the API request
                    if(data.length){
                        container.append(
                            data.map( post => {
                                return `
                                <article class="new-post">
                                    <div class="post-title-excerpt">
                                        <h3><a href="${post.link}">${post.title.rendered}</a></h3>
                                        ${post.excerpt.rendered}
                                    </div>
                                </article>
                                `
                            }).join('')
                        )
                    }

                    //adding a class(for fade in effect) to the newly added posts
                    window.setTimeout( () => $("#all-posts .new-post").addClass('fadein'), 100)
                })

                //adding 'end-page' class to the loadMore button if there is no more pages
                if( currentPage == totalPages ){
                    $(this).addClass('end-page')
                }
                
                console.log(currentPage)

            } else if( currentPage < totalPages ) {

                //run this to fetch all posts regardless of categories 
                //(after inital page load when no category link is clicked)

                currentPage = currentPage + 1;

                $.getJSON( siteurl + '/wp/v2/posts?per_page=' + perPage + '&page=' + currentPage , data => {
                    
                    //getting data from the API request
                    if(data.length){
                        container.append(
                            data.map( post => {
                                return `
                                <article class="new-post">
                                    <div class="post-title-excerpt">
                                        <h3><a href="${post.link}">${post.title.rendered}</a></h3>
                                        ${post.excerpt.rendered}
                                    </div>
                                </article>
                                `
                            }).join('')
                        )
                    }

                    //adding a class(for fade in effect) to the newly added posts
                    window.setTimeout( () => $("#all-posts .new-post").addClass('fadein'), 100)
                })

                //adding a class to the loadMore button if there is no more pages
                if( currentPage == totalPages ){
                    $(this).addClass('end-page')
                }
                
            }
            
        }//callback `loadNextPage` ends here

    }

    // Initialize on page load (front end).
    $(document).ready(function(){
        initializePosts( $(this) )
    })

})(jQuery)

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js