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:
- 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).
- 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 .=
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:
- 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.
- 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 🙂 |