There are a lot of commenting system plugins out there but most of them come with a cost, and sometimes just building a simple WordPress commenting system on top of the native commenting system can be enough.

The main features of this system will be:

  • Comments will be only loaded if the user clicks a button
  • Comments will be submitted and fetched by JS
  • Comments will have paginations fetched by JS
  • Comments will have some JS interactivity
  • Comments will have the reply feature enabled
  • The comment system will have an AMP implementation
  • The comment system will have a no JS implementation
  • Comments will work the same on AMP with our own script

The first thing will have to do for our system is to make some custom REST API for the JS to fetch.

We can add the following code to functions.php:

<?php
//...

add_action('rest_api_init', 'change_rest_post' );
function change_rest_post(){

  register_rest_route( 'a309/v1', '/get-comments-no/post/(?P<post_id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'get_top_level_comments_number',
    'permission_callback' => '__return_true',
  ) );
    
   register_rest_route( 'a309/v1', '/get-comments/post/(?P<post_id>\d+)/page/(?P<page_no>\d+)', array(
    'methods' => 'GET',
    'callback' => 'get_comments_post',
    'permission_callback' => '__return_true',
  ) );
 
}

function get_top_level_comments_number( $data ) {

    global $wpdb;
    $data['post_id'] = esc_sql($data['post_id']);
    $noComments = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_parent = 0 AND comment_post_ID = '".$data['post_id']."'" );
    wp_send_json([ 'commentNumber' => $noComments ]);
}

function get_comments_post($data){

// setup a fake POST to trick wp_list_comments
global $post; 
$post = new stdClass();

$post->ID = $data['post_id'];
setup_postdata( $post );

$comment_args = array(
					'avatar_size' => 60,
                                        'reverse_top_level' => true,
                                        'reverse_children' => true,
					'style'       => 'ol',
					'short_ping'  => true,
                                        'max_depth' => 5,
                                        'per_page' => 5,
                                        'page' =>    $data['page_no'],
                                        'echo' => false,
                                       
				);

$template = wp_list_comments( $comment_args );
		 
 wp_reset_postdata();
 
      wp_send_json([ 'comments' => $template, 'post_id' => $data['post_id'], 'page_no' => $data['page_no'] ]);
}


// Ajax Comment

function change_comment_action_url( $defaults ) {
	$defaults['action'] = get_template_directory_uri().'/inc/comment-post.php';
	return $defaults;
}

add_filter( 'comment_form_defaults', 'change_comment_action_url');

//...

We added 2 REST routes and modified the default route for posting comments.

For the modified route of submitting comments will use the default WordPress with some simple modifications, one is that we will return the avatar image, and the second is that we will make sure that the file returns JSON instead of a redirect.

The custom posting comment file comment-post.php:

 <?php
/**
 * Handles Comment Post to WordPress and prevents duplicate comment posting.
 *
 * @package WordPress
 */

if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
	$protocol = $_SERVER['SERVER_PROTOCOL'];
	if ( ! in_array( $protocol, array( 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0' ), true ) ) {
		$protocol = 'HTTP/1.0';
	}

	header( 'Allow: POST' );
	header( "$protocol 405 Method Not Allowed" );
	header( 'Content-Type: text/plain' );
	exit;
}

/** Sets up the WordPress Environment. */
require dirname(__DIR__, 4) . '/wp-load.php';

nocache_headers();

$comment = wp_handle_comment_submission( wp_unslash( $_POST ) );
if ( is_wp_error( $comment ) ) {
	$data = (int) $comment->get_error_data();
	if ( ! empty( $data ) ) {
		/*wp_die(
			'<p>' . $comment->get_error_message() . '</p>',
			__( 'Comment Submission Failure' ),
			array(
				'response'  => $data,
				'back_link' => true,
			)
		);*/
              
            wp_send_json([ 'error' => true, 'msg' => $comment->get_error_message() ]);
            
            
	} else {
		wp_send_json([ 'error' => true ]);
	}
}

$user            = wp_get_current_user();
$cookies_consent = ( isset( $_POST['wp-comment-cookies-consent'] ) );

/**
 * Perform other actions when comment cookies are set.
 *
 * @since 3.4.0
 * @since 4.9.6 The `$cookies_consent` parameter was added.
 *
 * @param WP_Comment $comment         Comment object.
 * @param WP_User    $user            Comment author's user object. The user may not exist.
 * @param bool       $cookies_consent Comment author's consent to store cookies.
 */
do_action( 'set_comment_cookies', $comment, $user, $cookies_consent );

$location = empty( $_POST['redirect_to'] ) ? get_comment_link( $comment ) : $_POST['redirect_to'] . '#comment-' . $comment->comment_ID;

// If user didn't consent to cookies, add specific query arguments to display the awaiting moderation message.
if ( ! $cookies_consent && 'unapproved' === wp_get_comment_status( $comment ) && ! empty( $comment->comment_author_email ) ) {
	$location = add_query_arg(
		array(
			'unapproved'      => $comment->comment_ID,
			'moderation-hash' => wp_hash( $comment->comment_date_gmt ),
		),
		$location
	);
}

/**
 * Filters the location URI to send the commenter after posting.
 *
 * @since 2.0.5
 *
 * @param string     $location The 'redirect_to' URI sent via $_POST.
 * @param WP_Comment $comment  Comment object.
 */
/*
$location = apply_filters( 'comment_post_redirect', $location, $comment );

wp_safe_redirect( $location );
exit;

 */
$comment->comment_avatar = get_avatar( $comment->comment_author_email, 64);

wp_send_json([ 'error' => false, 'comment' => $comment ]);

The original WordPress code is commented.

Then wee need to make sure we load are JS for the regular version and for the AMP version.

So in functions.php we need to have something like this:

//...


function add_adidtional_css_js() {
 if(!a309_is_amp()) {
    // APP CSS
    if( is_singular() ){
    wp_enqueue_style( 'a309-single', get_template_directory_uri() . '/css/single.css',false, null,'all');
    }
    
    // App JS
    wp_enqueue_script( 'a309-app', get_theme_file_uri('/js/app.js'), [], null, true );
    
    //Singular JS
    if( is_singular() ){
        wp_enqueue_script( 'a309-comments', get_theme_file_uri('/js/app_comments.js'), ['a309-app'], null, true );
    }
    
    //Index JS
    if( is_home() )
    {
        wp_enqueue_script( 'a309-index', get_theme_file_uri('/js/app_index.js'), ['a309-app'], null, true );
    }
 }else{
    // APP CSS
    if( is_singular() ){
    if(!a309_is_amp()) wp_enqueue_style( 'a309-single', get_template_directory_uri() . '/css/single.css',false, null,'all');
    else wp_enqueue_style( 'a309-single-amp', get_template_directory_uri() . '/css/single-amp.css',false, null,'all');
    }
 }
}

add_action( 'wp_enqueue_scripts', 'add_adidtional_css_js' );


function a309_enqueue_comment_reply() {
    if ( get_option( 'thread_comments' ) && !a309_is_amp() ) { 
        wp_enqueue_script( 'comment-reply' ); 
    }
}
// Hook into comment_form_before
add_action( 'comment_form_before', 'a309_enqueue_comment_reply' );
 

//...

As you see on the non-AMP mode we will make use of the default comment-reply.js and on AMP we will implement that functionality in app_comments.js.

Note that comment-reply.js does not need JQuery, also I would recommend altogether dumping JQuery on the frontEnd if you use WP 5.7 or later since is not needed by WP-core only by some plugins and there are many plugins that do not need JQuery.

After that, we should modify the comments.php file which is used for rendering the comments on the page, here is the full content of comments.php:

<?php
if (post_password_required()) {
    return;
}

$a309_no_comments = get_comments_number();

?>
<?php if (a309_is_amp()): ?>
    <amp-script id="comments-script" layout="container" src="<?php echo get_stylesheet_directory_uri() ?>/js/AMP/amp_comments.js" sandbox="allow-forms">

<?php endif; ?>

    <div id="comments" data-post-id="<?php echo $post->ID; ?>" data-no-comments="<?php echo $a309_no_comments; ?>" data-post-slug="<?php echo $post->post_name; ?>" class="comments-area default-max-width <?php echo get_option('show_avatars') ? 'show-avatars' : ''; ?>">
    <?php if (a309_is_amp()): ?>
            <div id="amp-respond"> <?php
endif;

$aria_req = ($req) ? " aria-required='true'" : '';
$comments_args = array(
    'comment_field' => '<p class="comment-form-comment"><textarea id="comment" name="comment" placeholder="Your Comment* " aria-required="true"></textarea></p>',
    'comment_notes_before' => '',
    'fields' =>
    [
        'author' =>
        '<div class="comment-name-email-block"><p class="comment-form-author">' .
        '<i class="icon-user-solid-square"></i><input id="author" class="blog-form-input" placeholder="Name* " name="author" type="text" value="' . esc_attr($commenter['comment_author']) .
        '" size="30"' . $aria_req . ' /></p>',
        'email' =>
        '<p class="comment-form-email">' .
        '<i class="icon-alternate_email"></i><input
   id="email" class="blog-form-input" placeholder="Email Address* " name="email" type="text" value="' . esc_attr($commenter['comment_author_email']) .
        '" size="30"' . $aria_req . ' /></p></div>',
        'url' => '',
    ],
    'logged_in_as' => null,
    'title_reply' => esc_html__('Leave a comment', 'a309'),
    'title_reply_before' => '<h2 id="reply-title" class="comment-reply-title">',
    'title_reply_after' => '</h2>',
);
if (a309_is_amp()) {
    $comments_args['cancel_reply_before'] = '';
    $comments_args['cancel_reply_after'] = '';
    $comments_args['cancel_reply_link'] = '';
    $comments_args['submit_button'] = '<noscript data-ampdevmode><input name="%1$s" type="submit" id="%2$s" class="%3$s" value="%4$s" /></noscript>';
}
comment_form($comments_args);
if (a309_is_amp()):
        ?>

                <p class="form-submit">
                    <button id="submit-amp" class="submit fade-in">Post Comment</button>
                </p>
            </div>
    <?php
endif;

if (have_comments()) :
    ?>
            <h2 class="comments-title"><?php
            if ('1' === $a309_no_comments) :
                esc_html_e('1 comment', 'a309');
            else :
                printf(
                        /* translators: %s: comment count number. */
                        esc_html(_nx('%s comment', '%s comments', $a309_no_comments, 'Comments title', 'a309')),
                        esc_html(number_format_i18n($a309_no_comments))
                );

            endif;
            ?></h2><!-- .comments-title -->


            <button  id="comments-show-btn" >
                Show Comments
            </button>
            <!-- TODO: NO-JS page with comments later        
           <noscript data-ampdevmode><a  id="comments-page" >
            Go to comments page
           </a>
           </noscript>
            -->


    <?php if (!comments_open()) : ?>
                <p class="no-comments"><?php esc_html_e('Comments are closed.', 'a309'); ?></p>
            <?php endif; ?>
        <?php endif; ?>
    </div><!-- #comments -->
        <?php if (a309_is_amp()): ?>
    </amp-script>

        <?php
     endif;

Mostly for the AMP version we need to include the amp-script element that will encompass the comments section, if we use the official amp-script plugin we don’t need to include the dependency for amp-script because it’s automatically included.

In the above code to bypass the automatic XHRequest that is automatically made by the amp-form, we simply insert the post button outside the from, but in case there is no JavaScript will still display the normal button so comments could be summited without JavaScript, though in that case, we should also modify comment-post.php, to detect if the request is made for JavaScript or not, in other words, we need to write some more code to fully support a non-JavaScript system.

Also as a side node to hide elements in amp when JS is not enabled can be done with:

html:not([amp-version]) .amp-selector-you-want-to-hide{
display: none;
}

This works because AMP will automatically append the amp-version attribute, and if no JS is available the attribute will not be there.

Also in the comments.php, we should have a way to include our amp-script that handles the comments, in the above code is done with src="/js/AMP/amp_comments.js".

I won’t put the CSS here but I’ll provide the link to the repo with all the code.

So to finalize our system we should put our two JS scripts one for AMP(which will run in the worker-dom) and one for NON-AMP.

JS for NON-AMP:

/* global A309TH */

// Document Ready
document.addEventListener("DOMContentLoaded", function () {

    const commentsEl = document.getElementById('comments');
    const commentFormEl = document.getElementById('commentform');

    const showCommentsBtn = document.getElementById('comments-show-btn');
    const postId = document.querySelector('article').dataset.id;

    let commentsList = null;
    let commentsShowMoreBtn = null;
    let page = null;

//Called from index
    const eventsOnComments = () => {
// Add event on comments show
        if (showCommentsBtn)
            showCommentsBtn.addEventListener('click', showCommentsFn);
        commentFormEl.addEventListener('submit', sumbitComment);
    };

    const commentsRemoveShowMoreBtn = () => {
        if (commentsShowMoreBtn) {
            commentsShowMoreBtn.removeEventListener('click', showMoreCommentsFn);
            commentsEl.removeChild(commentsShowMoreBtn);
            commentsShowMoreBt = null;
        }

    };

    const commentsAddShowMoreBtn = () => {
        const showMoreBtn = document.createElement('button');
        showMoreBtn.id = 'comments-show-more-btn';
        showMoreBtn.innerHTML = 'Show More Comments';
        commentsEl.appendChild(showMoreBtn);
        showMoreBtn.addEventListener('click', showMoreCommentsFn)

        return showMoreBtn;
    };


    const fetchComments = async () => {

        // fetch Comments 
        const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments/post/${postId}/page/${page}`;

        const response = await fetch(fetchUrl, {
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const data = await response.json(); // parses JSON response into native JavaScript objects

        page -= 1;
        return data;
    };


    const fetchCommentsNo = async () => {

        const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments-no/post/${postId}`;

        const response = await fetch(fetchUrl, {
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const data = await response.json();
        page = Math.ceil(data.commentNumber / 5);

    };

    const addCommentToDOM = (comment) => {

        const slug = commentsEl.getAttribute('data-post-slug');
        const date = new Date(comment.comment_date);
        const options = {year: 'numeric', month: 'long', day: 'numeric'};

        const newComHTML = `<li id="comment-${comment.comment_ID}" class="comment byuser comment-author-${comment.comment_author} even thread-even comment-added fade-in">
			<article id="div-comment-${comment.comment_ID}" class="comment-body">
				<footer class="comment-meta">
					<div class="comment-author vcard">
						 ${comment.comment_avatar}
                      <b class="fn"><a href="${comment.comment_author_url}" rel="external nofollow ugc" class="url">${comment.comment_author}</a></b> 
                       <span class="says">says:</span>					</div><!-- .comment-author -->

					<div class="comment-metadata">
						<a href="${window.location.origin}/${slug}/#comment-${comment.comment_ID}">
                                        <time datetime="${comment.comment_date}">${date.toLocaleDateString('en-US', options)} at ${date.toLocaleTimeString('en-US')}</time></a>					</div><!-- .comment-metadata -->

									</footer><!-- .comment-meta -->

				<div class="comment-content">
					<p>${comment.comment_content}</p>
				</div><!-- .comment-content -->
                                </article><!-- .comment-body -->
		</li>`;
        const divParent = document.createElement('div');
        divParent.innerHTML = newComHTML;
        const newCom = divParent.firstChild;                            
        if (Number(comment.comment_parent) === 0) {
            const commentList = document.getElementById('comment-list');
            commentList.insertBefore(newCom, commentList.firstChild);
        } else {
            const cancelLink = document.getElementById('cancel-comment-reply-link');
            cancelLink.click();
            let commentParent = document.getElementById(`comment-${comment.comment_parent}`);
            let children = commentParent.querySelector('ol.children');
            if (!children) {
                children = document.createElement('ol');
                children.classList.add('children');
                commentParent.appendChild(children);
            }
            commentList.insertBefore(newCom, commentList.firstChild);
            newCom.scrollIntoView();
        }
        return newCom;
    };

    const showCommentsFn = async () => {
        showCommentsBtn.innerHTML = `
     Loading
     <div class="loadingspinner"></div>
   `;
        showCommentsBtn.disabled = true;

        await fetchCommentsNo();

        const data = await fetchComments();

        commentsList = document.createElement('ol');
        commentsList.id = 'comment-list';
        commentsList.classList.add('comment-list');
        commentsList.insertAdjacentHTML('beforeend', data.comments);

        commentsEl.appendChild(commentsList);


        if (page) {
            await showMoreCommentsFn();
        }

        commentsEl.removeChild(showCommentsBtn);

    };

    const showMoreCommentsFn = async () => {

        commentsRemoveShowMoreBtn();
        const spinner = document.createElement('div');
        spinner.classList.add('loadingspinner');
        commentsEl.appendChild(spinner);


        const data = await fetchComments();
        commentsList.insertAdjacentHTML('beforeend', data.comments);

        //console.log(page);
        //console.log(commentsEl.dataset.noComments - (5 * (page)));

        if (page) {
            commentsShowMoreBtn = commentsAddShowMoreBtn();
        }

        commentsEl.removeChild(spinner);
    };


    const sumbitComment = async (e) => {

        e.preventDefault();
        delAlertBox();
        const respondEl = document.getElementById('respond');
        const spinner = addSimpleSpinner(respondEl, false);

        const commentform = commentFormEl;
        const formdata = new URLSearchParams(new FormData(commentform));


        var formurl = commentform.getAttribute('action');
        //Post Form with data

        const response = await fetch(formurl, {
            method: 'POST', // *GET, POST, PUT, DELETE, etc.
            cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
            credentials: 'same-origin', // include, *same-origin, omit
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: formdata // body data type must match "Content-Type" header
        });
        let alert = null;
        if (response.ok) {
            const data = await response.json();

            if (data.error) {
                alert = alertBox('error', `&#x26A0; ${data.msg}`);
                respondEl.appendChild(alert);
            } else {
                const commentList = document.getElementById('comment-list');
                const approved = parseInt(data.comment.comment_approved) === 1;
                const notApprovedText = 'Comment was posted but was not approved it will be live after approval.';
                if (commentList) {
                    const com = addCommentToDOM(data.comment);
                    if(!approved){
                        alert = alertBox('info', notApprovedText);
                         com.appendChild(alert);
                    }
                } else {
                     if (approved) {
                        alert = alertBox('success', 'Comment was posted');
                    } else {
                        alert = alertBox('info', notApprovedText);
                    }
                    respondEl.appendChild(alert);
                }
            }

        } else {
            alert = alertBox('error', '&#x26A0; HTTP fetch error, API down!');
            respondEl.appendChild(alert);
        }

        delSimpleSpinner(spinner);

    };
 
    eventsOnComments();

});

JS for AMP:

(async function ampComments() {
 
    const addSiSpinner = function (element, prepend = false) {
        const spinner = document.createElement('div');
        spinner.classList.add('loadingspinner');
        if (prepend) {
            element.insertBefore(element, element.firstElementChild);
        } else {
            element.appendChild(spinner);
        }
        return spinner;
    };

    const delSiSpinner = (spinner) => {
        if (spinner)
            spinner.parentNode.removeChild(spinner);
    };

    const delAlertBox = ( ) => {
        const oldAlertBox = document.getElementById('a309-alert-box');
        if (oldAlertBox) {
            oldAlertBox.parentNode.removeChild(oldAlertBox);
        }
    };

    const alertBox = (alertClass = 'error', alertMsg = '', delAlertBox = '') => {

        if (delAlertBox)
            delAlertBox();

        switch (alertClass) {
            case 'error':
                alertClass = 'alert-error';
                break;
            case 'info':
                alertClass = 'alert-info';
                break;
            case 'success':
                alertClass = 'alert-success';
                break;
            default:
                alertClass = 'alert-error';
                break;
        }

        const alertBox = document.createElement('div');
        alertBox.id = 'a309-alert-box';
        alertBox.classList.add('alert');
        alertBox.classList.add(alertClass);
        alertBox.classList.add('fade-in');
        alertBox.innerHTML = alertMsg;
        return alertBox;
    };


    const commentsEl = document.getElementById('comments');
    const commentFromEl = document.getElementById('commentform');

    commentFromEl.addEventListener('submit', (e) => {
        e.target.preventDefault();
    });

    const actionUrl = document.getElementById('commentform').getAttribute('action-xhr');
    const postId = document.getElementById('comments').getAttribute('data-post-id');


    let storeRespEl = null;
    let replyEl = null;
    let hiddenRepLink = null;



    let commentsList = null;
    let commentsShowMoreBtn = null;


    const showCommentsBtn = document.getElementById('comments-show-btn');

    const postCommentBtn = document.getElementById('submit-amp');

    let page = null;


    const HTMLtoEL = (html) => {
        const t = document.createElement('div');
        t.innerHTML = html;
        return t.children;
    };

    const DelShowMoreCBtn = () => {
        if (commentsShowMoreBtn) {
            commentsShowMoreBtn.parentNode.removeChild(commentsShowMoreBtn);
            commentsShowMoreBtn = null;
        }
    };

    const AddShowMoreCBtn = () => {
        const showMoreBtn = document.createElement('button');
        showMoreBtn.id = 'comments-show-more-btn';
        showMoreBtn.innerHTML = 'Show More Comments';
        showMoreBtn.addEventListener('click', showMoreCommentsFn);
        commentsEl.appendChild(showMoreBtn);
        return showMoreBtn;
    };


    const fetchCommentsNo = async () => {

        const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments-no/post/${postId}`;

        const response = await fetch(fetchUrl, {
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const data = await response.json();
        page = Math.ceil(data.commentNumber / 5);
    };


    const fetchComments = async () => {

        // fetch Comments 
        const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments/post/${postId}/page/${page}`;

        const response = await fetch(fetchUrl, {
            headers: {
                'Content-Type': 'application/json',
            }
        });
        const data = await response.json(); // parses JSON response into native JavaScript objects

        page -= 1;
        return data;
    };

    const repImgAMP = (...args) => {
        const src = args[1].replace(/#038;/gms, '');
        return `<amp-img
             alt
             src="${src}"
             width="60"
             height="60"
             layout="fixed"
           >
           </amp-img>`;
    };

    const AMPifyComments = (comments) => {
        return HTMLtoEL(comments
                .replace(/<img.*?src=['"]{1}(.*?)['"]{1}.*?>/gms, repImgAMP)
                .trim()
                .replace(/<!--.*?-->/gms, '')
                .replace(/\t/gm, '')
                .replace(/\n/gm, ''));
    };

    const AddCommentsToList = (ListEl, comments) => {
        for (const comment of comments) {
            ListEl.appendChild(comment);
        }
    };

    const showCommentsFn = async () => {

        showCommentsBtn.innerHTML = `
     Loading
     <div class="loadingspinner"></div>
   `;
        showCommentsBtn.disabled = true;

        await fetchCommentsNo();

        const data = await fetchComments();


        commentsList = document.createElement('ol');
        commentsList.id = 'comment-list';
        commentsList.classList.add('comment-list');

        const comments = AMPifyComments(data.comments);
        addReplyEvent(comments);

        AddCommentsToList(commentsList, comments);

        commentsEl.appendChild(commentsList);

        commentsEl.removeChild(showCommentsBtn);

        if (page) {
            await showMoreCommentsFn();
        }

    };

    const showMoreCommentsFn = async () => {

        DelShowMoreCBtn();
        const spinner = addSiSpinner(commentsEl);

        const data = await fetchComments();
        const commentsList = document.getElementById('comment-list');
        const comments = AMPifyComments(data.comments);


        addReplyEvent(comments);
        AddCommentsToList(commentsList, comments);


        if (page) {
            commentsShowMoreBtn = AddShowMoreCBtn();
        }

        delSiSpinner(spinner);
    };


    const sumbitComment = async (e) => {

        delAlertBox();
        const respondEl = document.getElementById('amp-respond');
        const spinner = addSiSpinner(respondEl);
        const submitBtn = document.getElementById('submit-amp');
        submitBtn.disabled = true;
        //serialize and store form data in a variable

        //console.log(actionUrl);


        const bodyData = {comment: document.getElementById('comment') ? document.getElementById('comment').value : '',
            author: document.getElementById('author') ? document.getElementById('author').value : '',
            email: document.getElementById('email') ? document.getElementById('email').value : '',
            comment_post_ID: document.getElementById('comment_post_ID') ? document.getElementById('comment_post_ID').value : '',
            comment_parent: document.getElementById('comment_parent') ? document.getElementById('comment_parent').value : '',
            'wp-comment-cookies-consent': document.getElementById('wp-comment-cookies-consent') ? document.getElementById('wp-comment-cookies-consent').value : '',
            akismet_comment_nonce: document.getElementById('akismet_comment_nonce') ? document.getElementById('akismet_comment_nonce').value : '',
            'ak_js': document.getElementById('ak_js') ? document.getElementById('ak_js').value : '',
            'ak_hp_textarea': document.getElementById('ak_hp_textarea') ? document.getElementById('ak_hp_textarea').value : '',
            'redirect_to': document.getElementById('redirect_to') ? document.getElementById('redirect_to').value : ''
        };

        const searchParams = Object.keys(bodyData).map((key) => {
            return `${encodeURIComponent(key)}${ bodyData[key] ? '=' + encodeURIComponent(bodyData[key]) : ''}`;
        }).join('&');

        const response = await fetch(actionUrl, {
            method: 'POST', // *GET, POST, PUT, DELETE, etc.
            cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
            credentials: 'same-origin', // include, *same-origin, omit
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: searchParams // body data type must match "Content-Type" header
        });
        let alert = null;
        if (response.ok) {
            const data = await response.json();

            if (data.error) {
                alert = alertBox('error', `&#x26A0; ${data.msg}`, delAlertBox);
                respondEl.appendChild(alert);
            } else {
                const approved = parseInt(data.comment.comment_approved) === 1;
                if (approved) {
                    const noComments = document.getElementById('comments').getAttribute('data-no-comments');
                    if (noComments) {
                        const nCom = parseInt(noComments) + 1;
                        document.getElementById('comments').setAttribute('data-no-comments', nCom);
                        const comText = commentsEl.querySelector('.comments-title');
                        if (comText) {
                            comText.textContent = [nCom, comText.textContent.split(' ')[1]].join(' ');
                        }
                    }
                }
                const notApprovedText = 'Comment was posted but was not approved it will be live after approval.';
                const commentList = document.getElementById('comment-list');
                if (commentList) {
                    const com = addCommentToDOM(data.comment);
                    if (!approved) {
                        alert = alertBox('info', notApprovedText, delAlertBox);
                        com.appendChild(alert);
                    }
                } else {
                    if (approved) {
                        alert = alertBox('success', 'Comment was posted', delAlertBox);
                    } else {
                        alert = alertBox('info', notApprovedText, delAlertBox);
                    }

                    respondEl.appendChild(alert);
                }
            }

        } else {
            alert = alertBox('error', '&#x26A0; HTTP fetch error, API down!', delAlertBox);
            respondEl.appendChild(alert);
        }

        submitBtn.disabled = false;
        delSiSpinner(spinner);

    };


    const addCommentToDOM = (comment) => {

        const slug = commentsEl.getAttribute('data-post-slug');
        const date = new Date(comment.comment_date);
        const options = {year: 'numeric', month: 'long', day: 'numeric'};

        const newCom = `<li id="comment-${comment.comment_ID}" class="comment byuser comment-author-${comment.comment_author} even thread-even comment-added fade-in">
			<article id="div-comment-${comment.comment_ID}" class="comment-body">
				<footer class="comment-meta">
					<div class="comment-author vcard">
						 ${comment.comment_avatar}
                      <b class="fn"><a href="${comment.comment_author_url}" rel="external nofollow ugc" class="url">${comment.comment_author}</a></b> 
                       <span class="says">says:</span>					</div><!-- .comment-author -->

					<div class="comment-metadata">
						<a href="${window.location.origin}/${slug}/#comment-${comment.comment_ID}">
                        <time datetime="${comment.comment_date}">${date.toLocaleDateString('en-US', options)} at ${date.toLocaleTimeString('en-US')}
                                </time></a></div><!-- .comment-metadata -->

									</footer><!-- .comment-meta -->

				<div class="comment-content">
					<p>${comment.comment_content}</p>
				</div><!-- .comment-content -->
                            </article><!-- .comment-body -->
		</li>`;
        const ampComment = AMPifyComments(newCom)[0];
        const commentList = document.getElementById('comment-list');
        if (Number(comment.comment_parent) === 0) {
            commentList.insertBefore(ampComment, commentList.firstChild);
        } else {
            const cancelLink = document.getElementById('cancel-comment-reply-link');
            cancelLink.click();
            let commentParent = document.getElementById(`comment-${comment.comment_parent}`);
            let children = commentParent.querySelector('.children');
            if (!children) {
                children = document.createElement('ol');
                children.classList.add('children');
                commentParent.appendChild(children);
            }
            children.insertBefore(ampComment, children.firstChild);
        }
       return ampComment;
    };

    const replyMoveForm = (e) => {
        e.preventDefault();
        if (hiddenRepLink)
            hiddenRepLink.removeAttribute('hidden');
        const commentId = e.target.getAttribute('data-commentid');
        const divComment = document.getElementById(`div-comment-${commentId}`);
        hiddenRepLink = divComment.querySelector('.comment-reply-link');
        hiddenRepLink.setAttribute('hidden');
        if (storeRespEl === null) {
            const oldCancelLink = document.getElementById('cancel-comment-reply-link');
            if (oldCancelLink)
                oldCancelLink.parentNode.removeChild(oldCancelLink);
            const respondEl = document.getElementById('amp-respond');
            const form = respondEl.querySelector('form');
            if (form) {
                form.removeAttribute('amp-novalidate');
                form.removeAttribute('class');
            }
            const noScript = respondEl.getElementsByTagName('noscript');
            if (noScript[0])
                noScript[0].parentNode.removeChild(noScript[0]);
            replyEl = respondEl.cloneNode(true);
            storeRespEl = respondEl;
            const resp = replyEl.querySelector('#respond');
            const fSubmit = resp.querySelector('#form-submit');
            const cancelLink = document.createElement('a');
            cancelLink.id = 'cancel-comment-reply-link';
            cancelLink.setAttribute('rel', 'nofollow');
            cancelLink.setAttribute('href', '#comments');
            cancelLink.textContent = `Cancel Reply`;
            resp.insertBefore(cancelLink, fSubmit);
            cancelLink.addEventListener('click', replyFormCancel);
            replyEl.querySelector('#submit-amp').addEventListener('click', sumbitComment);
        }
        document.getElementById('amp-respond').parentNode.removeChild(storeRespEl);

        let replyName = divComment.querySelector('.fn');
        replyName = replyName ? replyName.textContent : '';
        const replyTitle = replyEl.querySelector('#reply-title');
        replyTitle.textContent = `Reply to ${replyName}`;
        const resp = replyEl.querySelector('#respond');
        const inParent = resp.querySelector('#comment_parent');
        inParent.setAttribute('value', commentId);
        document.getElementById(`comment-${commentId}`).insertBefore(replyEl, divComment.nextSibling);

    };

    const replyFormCancel = () => {
        delAlertBox();
        const respondEl = document.getElementById('amp-respond');
        respondEl.parentNode.removeChild(respondEl);
        if (hiddenRepLink) {
            hiddenRepLink.removeAttribute('hidden');
            hiddenRepLink = null;
        }
        commentsEl.insertBefore(storeRespEl, commentsEl.firstChild);
    };

    const addReplyEvent = (comments) => {
        for (const comment of comments) {
            const links = comment.querySelectorAll('.comment-reply-link');
            for (const link of links) {
                //link.removeAttribute('href');
                link.addEventListener('click', replyMoveForm);
            }
        }
    };


    postCommentBtn.addEventListener('click', sumbitComment);
    if(showCommentsBtn) showCommentsBtn.addEventListener('click', showCommentsFn);
 
})();

And that’s should do it, here’s the repo link: https://gitlab.flashsoft.eu/andrei0x309/tailwindcss-wordpress-theme.

And also here is a demo GIF of the WordPress commenting system:

Wordpress commenting system