How to load comments via AJAX with REST and JS in WordPress

Contents

Overview

This tutorial shows how to load WordPress post comments via AJAX using the WordPress REST API and plain JavaScript. Two practical approaches are presented:

  • Approach A (recommended): Use the built-in WP REST API /wp/v2/comments endpoint to fetch comment JSON and render it client-side.
  • Approach B: Register a custom REST route that returns already-rendered HTML for comments (useful if you want server-side markup identical to your theme).

Both approaches include pagination (load more) and progressive enhancement: the first page of comments can still be output by PHP in the theme for SEO/initial view, and subsequent pages are requested via JS.

Prerequisites

  • WordPress 4.7 (REST API for comments is part of core).
  • Basic knowledge of editing theme files (functions.php, template files) and creating/enqueuing a JS file.
  • A theme that outputs the comments area (the tutorial shows how to progressively enhance that area).

The concept

We progressively enhance the comments area. The theme outputs the first N comments on the server. JavaScript takes over to request additional pages from the REST API and appends them to the comments container. You can either:

  1. Call the core REST endpoint: /wp-json/wp/v2/comments?post=POST_IDper_page=5page=2 — this returns JSON with content.rendered (HTML of the comment content).
  2. Or call your custom endpoint which returns fully rendered HTML (LI elements) so you can append without building markup in JS.

1) HTML structure you need in your theme

Place a comments container and a Load More button in your comments template (usually comments.php). The server should output the initial set of comments (page 1) as usual. The JavaScript will request later pages and append them.

ltul class=comments-list id=comments-list aria-live=politegt
  lt!-- server-rendered first page of comments: li.comment entries --gt
lt/ulgt

ltbutton id=load-more-comments data-post-id=123 data-per-page=5 data-current-page=1 aria-label=Load more commentsgt
  Load more comments
lt/buttongt

2) Approach A — Use the core WP REST comments endpoint (JSON)

Why use it

  • No additional PHP required: WP provides /wp/v2/comments out of the box.
  • JSON contains HTML for the comment content (content.rendered), plus author and date data.
  • Better separation: JS builds the list items, enabling client-side tweaks or templates.

Enqueue the script and localize data

Add this to your themes functions.php (or plugin). It registers and enqueues a JS file and passes REST base URL and a nonce (nonce is optional for reading, but is useful for uniformity and potential authenticated requests later).

// functions.php
function mytheme_enqueue_comments_ajax() {
    // Register script (place your JS in /js/comments-ajax.js)
    wp_register_script(
        mytheme-comments-ajax,
        get_template_directory_uri() . /js/comments-ajax.js,
        array(),
        1.0,
        true
    )

    // Localize data for JS: REST base and WP info
    wp_localize_script( mytheme-comments-ajax, MyComments, array(
        root => esc_url_raw( rest_url() ),
        nonce => wp_create_nonce( wp_rest ),
    ) )

    wp_enqueue_script( mytheme-comments-ajax )
}
add_action( wp_enqueue_scripts, mytheme_enqueue_comments_ajax )

Client-side JS: fetch comments and append

This example uses the Fetch API. It reads data attributes from the Load More button to know the current page and post ID, requests the next page, then appends new list items to the comments list. Error handling and a simple loading state are included.

// js/comments-ajax.js
(function () {
  use strict

  // Helper to create an LI for a comment JSON object from WP REST API
  function buildCommentListItem(comment) {
    // comment.content.rendered contains the HTML of the comment content
    var li = document.createElement(li)
    li.className = comment
    li.id = comment-   comment.id

    var author = comment.author_name  Anonymous
    var date = (new Date(comment.date)).toLocaleString()

    li.innerHTML = 
      ltdiv class=comment-metagt
        ltspan class=comment-authorgt   escapeHtml(author)   lt/spangt
        ltspan class=comment-dategt   escapeHtml(date)   lt/spangt
      lt/divgt
      ltdiv class=comment-contentgt   comment.content.rendered   lt/divgt
    
    return li
  }

  // Minimal HTML escape for author/date text nodes
  function escapeHtml(text) {
    return String(text)
      .replace(//g, amp)
      .replace(//g, gt)
      .replace(//g, quot)
      .replace(//g, #039)
  }

  function enableLoadMore(button) {
    var postId = button.getAttribute(data-post-id)
    var perPage = parseInt(button.getAttribute(data-per-page), 10)  5
    var currentPage = parseInt(button.getAttribute(data-current-page), 10)  1
    var list = document.getElementById(comments-list)

    if (!postId  !list) {
      return
    }

    button.addEventListener(click, function () {
      button.disabled = true
      var loadingText = button.getAttribute(data-loading-text)  Loading...
      var originalText = button.textContent
      button.textContent = loadingText

      var nextPage = currentPage   1
      // Build the REST endpoint URL: /wp/v2/comments?post=POST_IDper_page=PER_PAGEpage=NEXT
      var url = MyComments.root   wp/v2/comments?post=   encodeURIComponent(postId)
                per_page=   perPage   page=   nextPage   _=   Date.now()

      fetch(url, {
        credentials: same-origin,
        headers: {
          X-WP-Nonce: MyComments.nonce
        }
      })
      .then(function (response) {
        if (!response.ok) {
          // If 400/404: probably no more pages
          throw new Error(Network response was not ok:    response.status)
        }
        // JSON array of comment objects
        return response.json()
      })
      .then(function (comments) {
        if (!comments  comments.length === 0) {
          // No more comments: hide button
          button.style.display = none
          return
        }

        comments.forEach(function (comment) {
          var li = buildCommentListItem(comment)
          list.appendChild(li)
        })

        // Update current page state
        currentPage = nextPage
        button.setAttribute(data-current-page, currentPage)

        // If fewer comments returned than perPage, likely last page -> hide
        if (comments.length lt perPage) {
          button.style.display = none
        } else {
          button.disabled = false
          button.textContent = originalText
        }
      })
      .catch(function (error) {
        // Error handling: show original text and re-enable
        console.error(Error fetching comments:, error)
        button.disabled = false
        button.textContent = originalText
      })
    })
  }

  // Initialize
  document.addEventListener(DOMContentLoaded, function () {
    var btn = document.getElementById(load-more-comments)
    if (btn) {
      enableLoadMore(btn)
    }
  })
})()

Notes for Approach A

  • The core comments endpoint returns objects with content.rendered (comment content HTML), author_name, author_url, date. Use these fields to build display markup.
  • Pagination: WP REST API uses per_page and page. Per_page has a maximum (default 100). If you request a page beyond the last, youll get HTTP 400 or an empty array (depending on WP version) handle both.
  • Threaded comments: the core comments endpoint returns parent ID to render threaded comments in the correct structure you must either request all comments and assemble a tree client-side, or fetch per parent. For small sites, fetching all comments can be practical otherwise consider a server-side endpoint that renders nested markup and handles your themes walker.

3) Approach B — Custom REST route that returns server-rendered HTML

Why use it

  • You can reuse your themes comment markup exactly (class names, structure, microdata).
  • Client-side code only needs to append raw HTML to the list, no templating required.

Register a custom route and return HTML

Add this to functions.php or a plugin. The callback generates a string of ltligt elements representing comments. This simple example returns one level of comments — adapt it if you need nested threading rendered exactly like your theme.

// functions.php (or plugin)
add_action( rest_api_init, function () {
    register_rest_route( mytheme/v1, /comments, array(
        methods  => GET,
        callback => mytheme_rest_get_comments_html,
        permission_callback => __return_true,
    ) )
} )

function mytheme_rest_get_comments_html( WP_REST_Request request ) {
    post_id = (int) request->get_param( post )
    page    = max( 1, (int) request->get_param( page ) )
    per_page = max( 1, min( 100, (int) request->get_param( per_page ) ) )

    if ( ! post_id ) {
        return new WP_Error( no_post, Post ID is required, array( status => 400 ) )
    }

    args = array(
        post_id => post_id,
        status  => approve,
        number  => per_page,
        offset  => (page - 1)  per_page,
        orderby => comment_date_gmt,
        order   => ASC,
    )

    comments = get_comments( args )

    // Build HTML. Keep markup consistent with your theme. Minimal example:
    html = 
    foreach ( comments as comment ) {
        // Use functions like get_comment_author(), get_avatar(), comment_date() if you prefer.
        author = esc_html( get_comment_author( comment ) )
        date   = esc_html( get_comment_date( , comment ) )
        content = apply_filters( the_content, comment->comment_content )
        content = str_replace( ]]>, ]]gt, content )

        html .= 
  • comment_ID ) . class=comment> html .=
    . author . html .= . date .
    html .=
    . content .
    html .=
  • } // Return as simple JSON object with html and count return rest_ensure_response( array( html => html, count => count( comments ), ) ) }

    Client-side JS for custom endpoint

    This JS requests the custom route and appends the returned HTML to the comments list.

    // js/comments-ajax-custom.js
    (function () {
      use strict
    
      function enableLoadMoreCustom(button) {
        var postId = button.getAttribute(data-post-id)
        var perPage = parseInt(button.getAttribute(data-per-page), 10)  5
        var currentPage = parseInt(button.getAttribute(data-current-page), 10)  1
        var list = document.getElementById(comments-list)
    
        button.addEventListener(click, function () {
          button.disabled = true
          var original = button.textContent
          button.textContent = Loading...
    
          var nextPage = currentPage   1
          var url = MyComments.root   mytheme/v1/comments?post=   encodeURIComponent(postId)
                      per_page=   perPage   page=   nextPage
    
          fetch(url, {
            credentials: same-origin,
            headers: {
              X-WP-Nonce: MyComments.nonce
            }
          })
          .then(function (r) {
            if (!r.ok) throw new Error(Network response was not ok:    r.status)
            return r.json()
          })
          .then(function (data) {
            if (!data  !data.html) {
              button.style.display = none
              return
            }
            // Insert returned HTML (be mindful of sanitization and trust)
            list.insertAdjacentHTML(beforeend, data.html)
    
            currentPage = nextPage
            button.setAttribute(data-current-page, currentPage)
    
            if (data.count lt perPage) {
              button.style.display = none
            } else {
              button.disabled = false
              button.textContent = original
            }
          })
          .catch(function (err) {
            console.error(err)
            button.disabled = false
            button.textContent = original
          })
        })
      }
    
      document.addEventListener(DOMContentLoaded, function () {
        var btn = document.getElementById(load-more-comments)
        if (btn) enableLoadMoreCustom(btn)
      })
    })()
    

    4) Security and performance considerations

    • Sanitize output: Approach B returns HTML from server. Escape any values you generate and pass content through safe filters (apply_filters(the_content, …)) so shortcodes and formatting work. Consider running HTML through wp_kses if needed.
    • Permissions: Reading comments is typically public. The REST route permission_callback can be __return_true. If you restrict comments by capability or want logged-in-only access, adjust permission_callback accordingly.
    • Caching: REST request for comments may be cached by external caches. If you want the newest comments immediately available, use cache-busting query params or appropriate caching rules.
    • Rate limiting: Add throttling or rely on existing server protections to avoid abuse from rapid client requests.
    • Pagination headers: The core REST API includes pagination headers (X-WP-Total, X-WP-TotalPages). You can inspect response.headers.get(X-WP-TotalPages) to decide whether to hide the Load More button.

    5) Handling nested (threaded) comments

    If you use threaded comments (Replies), there are two main ways to display them:

    1. Server-side: Have your custom endpoint render nested ltulgtltligt comment structure exactly like your theme. This is easiest for themes that use a Walker to generate threaded HTML.
    2. Client-side assembly: Fetch all comments for the post (or enough comments to display the thread) and build a parent->children tree in JS, then render it into nested lists. This requires more client logic but avoids creating a custom server route.

    6) UX, accessibility and progressive enhancement

    • Make sure the Load More button has aria-label and is keyboard accessible (a native ltbuttongt is best).
    • Use aria-live on the comment list so screen readers announce new comments when appended.
    • Provide graceful fallback: If JS is disabled, the server should render pagination links or full comment list so users can still view comments.
    • Show loading indicators and clear error messages on failure.

    7) Example CSS (optional)

    / minimal styles for comments and load more button /
    .comments-list { list-style: none margin: 0 padding: 0 }
    .comment { border-bottom: 1px solid #e5e5e5 padding: 1rem 0 }
    .comment-meta { font-size: 0.9rem color: #666 margin-bottom: .5rem }
    #load-more-comments { display: inline-block margin-top: 1rem padding: .5rem 1rem }
    

    Troubleshooting

    • If fetch returns 400 when requesting a page: you likely requested a page number beyond the last. Detect this and hide the Load More button.
    • If content.rendered appears escaped or incomplete: ensure the REST response is coming from WP and not filtered by a plugin. If you use a custom endpoint, ensure you applied the_content filters.
    • If comments are missing avatars or author URLs: the REST comment object contains author fields but does not include avatar markup by default you can build avatar markup server-side or request additional fields with your custom endpoint.

    Summary

    Loading comments via AJAX with the REST API is straightforward. Use the core /wp/v2/comments endpoint for a simple JSON approach, or register a custom REST route if you need server-rendered markup. Enqueue a small JavaScript file that fetches pages, appends HTML (or builds it from JSON), handles pagination and errors, and keeps accessibility in mind. Progressive enhancement ensures that users and search engines still get a usable comments experience if JavaScript is not available.

    Useful references



    Acepto donaciones de BAT's mediante el navegador Brave 🙂



    Leave a Reply

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