Back to blog

How to Display Comment Counts on a Custom CMS: An API-First Implementation Guide

Learn how to display comment counts on a custom CMS without wrecking page speed: one API call, a cache layer, a crawler fallback, and rules for what the number should actually mean. How to Display Comment Counts on a Custom CMS: An API-First Implementation Guide is an EchoThread guide for site owners evaluating privacy-first comments, moderation, migration, performance, and reader engagement. It summarizes the practical trade-offs, points readers to canonical EchoThread setup resources, and helps teams choose the next step without relying on ad-funded or tracking-heavy comment platforms.

To solve how to display comment counts on custom CMS templates, treat the count as a lightweight data read rather than a full widget render. Fetch the number directly from an API comment count endpoint keyed by your canonical CMS identifier, cache the integer in your application layer, and render it as plain server-side text inside your index and archive templates.

Most publishing platforms make this harder than it needs to be by forcing developers to embed full commenting scripts just to output a single integer. When an archive template renders 20 posts per page, running 20 widget scripts introduces severe Cumulative Layout Shift (CLS), blows past third-party script budgets, and confuses moderation queues. Whether you are an installer wiring up custom templates this week or a moderator managing thread queues across a busy publication, this architectural guide provides the exact server-side and client-side patterns required to display accurate comment metrics reliably.


The Short Answer: One Endpoint, One Cache, One Fallback

A comment count is pure metadata. It belongs in your templates alongside the publish date, author byline, and reading time. To display it efficiently, your architecture requires three distinct parts:

  1. A stable thread key: An immutable identifier (such as a database UUID, content slug, or canonical path) that links your CMS content record to the discussion thread.
  2. A batched API comment count endpoint: An HTTP endpoint that accepts multiple thread identifiers in a single request and returns key-value pairs of thread IDs and approved comment totals.
  3. An application-level cache: A key-value cache (Redis, Memcached, or an in-memory runtime store) with a time-to-live (TTL) between 60 and 300 seconds to protect both your page render speed and your API limits.

What you should avoid is loading a third-party JavaScript bundle on an index page solely to read a number from a DOM element. Similarly, querying an unindexed SQL comment table or making unbatched round-trips to an external API on every page view will bottleneck your custom CMS.

Comment numbers belong on index pages, category archives, author lists, related-post blocks, and internal search results. These are the touchpoints where readers decide whether an article has an active discussion worth joining. For the solo installer, the primary goal is zero layout shift and fast delivery. For the editorial moderator, the goal is metric integrity: ensuring the displayed count matches the real, visible comments rather than spam or unreviewed submissions waiting in a queue.


Pick a Thread Identifier Before You Write Any Code

A custom CMS comment count is only as reliable as the identifier underneath it. If your CMS derives thread keys dynamically at render time from mutable fields, your comment counts will eventually break or reset to zero.

The Problem With Dynamic Identifiers

Many teams make the mistake of using the raw window URL or request path as the discussion identifier. Over time, editorial workflows introduce subtle changes:

  • An editor updates a post slug from /news/product-update to /news/product-update-2026 to improve search positioning.
  • Marketing adds campaign parameters (?utm_source=newsletter) that split discussions across multiple variations of the same page.
  • Locale routing introduces language prefixes (/en/post-name vs /fr/post-name).
  • Trailing slash variations (/post-name/ vs /post-name) create duplicate threads.

In every one of these scenarios, an API keyed on the dynamic URL will treat the altered path as a brand-new discussion, returning zero comments and hiding the historical conversation.

Best Practice: Store the Thread Key in Your Content Model

The cleanest implementation assigns an immutable string identifier to the content record at publish time. Store this value in your CMS database alongside the article record:

// Example CMS content schema record
{
  "id": "art_948194a02c",
  "title": "Scaling API Count Endpoints",
  "slug": "scaling-api-count-endpoints",
  "status": "published",
  "published_at": "2026-10-03T09:00:00Z",
  "thread_key": "article-948194a02c"
}

By passing thread_key to your comment provider when initializing comments and when requesting counts, you decouple the discussion from URL rewrites, category restructuring, or domain migrations.

Managing Historical Migrations

If you are migrating from legacy commenting software to a modern system, establish a mapping table in version control before altering templates. Map your historical URLs or legacy IDs directly to your new stable CMS keys:

CMS Post ID Legacy Identifier (e.g., Disqus URL) New Stable Thread Key
1042 https://example.com/2023/old-post-title/ post-1042
1043 https://example.com/blog/another-article post-1043
1044 https://example.com/news/archive-item/ post-1044

Maintaining an explicit lookup table prevents missing comment counts during platform upgrades and ensures legacy discussions remain intact.


How to Display Comment Counts on Custom CMS Templates: The Server-Side Path

Server-side rendering (SSR) is the standard approach for custom CMS architectures. Rendering the count server-side ensures the number is present in the initial HTML payload sent to the client browser, eliminating layout shifts and blank placeholder badges.

If you run a decoupled front end, our guide on comment system integration for headless CMS walks through similar pipeline principles. Here is how to implement the server-side pipeline step-by-step:

Step 1: Batch Thread Requests

Avoid querying an API count endpoint inside a loop for each individual card on an index page. If an archive page displays 25 articles, firing sequential HTTP requests inside template loops introduces network latency that delays server response times.

Instead, collect all thread identifiers from your content collection and query the API in a single batched operation:

// Node.js / Custom CMS Controller Example
async function getArchivePageData(articles) {
  // 1. Extract thread keys from the collection
  const threadKeys = articles.map(article => article.thread_key);

  // 2. Fetch batched counts from cache or remote API
  const counts = await getCachedCommentCounts(threadKeys);

  // 3. Attach counts to the article view models
  return articles.map(article => ({
    ...article,
    commentCount: counts[article.thread_key] ?? 0
  }));
}

Step 2: Implement Multi-Tiered Caching

A resilient custom CMS uses an application cache (such as Redis or Memcached) to store counts. Querying an external service on every page render creates an unnecessary external dependency. Store thread counts with a reasonable TTL (e.g., 180 seconds):

// Example Redis retrieval and fallback pattern
async function getCachedCommentCounts(threadKeys) {
  const cacheKeys = threadKeys.map(k => `counts:${k}`);
  const cachedValues = await redis.mget(cacheKeys);
  
  const results = {};
  const missingKeys = [];

  threadKeys.forEach((key, index) => {
    if (cachedValues[index] !== null) {
      results[key] = parseInt(cachedValues[index], 10);
    } else {
      missingKeys.push(key);
    }
  });

  // If all keys hit cache, return immediately
  if (missingKeys.length === 0) return results;

  // Fetch only missing keys from the commenting API
  try {
    const response = await fetch(`https://api.echothread.io/v1/counts?threads=${missingKeys.join(',')}`, {
      headers: { 'Authorization': `Bearer ${process.env.COMMENT_API_KEY}` },
      timeout: 800 // Hard timeout in milliseconds
    });
    
    if (response.ok) {
      const data = await response.json();
      for (const [key, count] of Object.entries(data)) {
        results[key] = count;
        // Cache individual key with a 3-minute TTL
        await redis.setex(`counts:${key}`, 180, count);
      }
    }
  } catch (error) {
    console.error('Comment count fetch error, serving zero fallbacks:', error);
    // Fill remaining keys with 0 so the template render does not fail
    missingKeys.forEach(k => { results[k] = results[k] ?? 0; });
  }

  return results;
}

Step 3: Render Defensive Template Markup

Render the value directly into your HTML markup, including clear fallbacks. If the count cannot be retrieved, your template should render a graceful fallback rather than an empty string or broken HTML token:

<!-- Example Custom CMS Template (Liquid / Jinja / Blade) -->
<div class="article-card">
  <h3>{{ article.title }}</h3>
  <p class="meta">
    <span class="date">{{ article.published_at | date: "%B %d, %Y" }}</span>
    <span class="comment-count" aria-label="{{ article.commentCount }} comments">
      {{ article.commentCount }} {{ article.commentCount | pluralize: 'comment', 'comments' }}
    </span>
  </p>
</div>

Step 4: Invalidate on Write Events

Relying solely on a time-based TTL can cause minor delays when a thread receives frequent comments. To ensure counts update immediately after moderation events, register a webhook from your commenting platform that invalidates the cached key whenever a comment is approved or removed.

The operational tradeoff is straightforward: a 180-second TTL means a reader might see 14 comments on an index page when the article has 16. For virtually all publishing sites, this minor lag is an acceptable compromise to achieve sub-millisecond template rendering.


The Client-Side Path, and When It Is the Wrong Choice

Client-side fetching uses a browser script to request comment numbers via an API or DOM injector after the initial HTML renders. While this approach is simple to drop into static site setups, it is often the wrong choice for content-driven publications.

The Drawbacks of Client-Side Rendering

  • Layout Shift (CLS): If your template reserves zero pixels for the comment badge, the text pops in a fraction of a second after page load, shifting neighboring meta items and causing Cumulative Layout Shift.
  • Unnecessary HTTP Overhead: Every single reader visit causes an extra round-trip HTTP request to an API, increasing network contention on slower mobile connections.
  • Crawler Invisibility: Search engine crawlers and automated tools that do not run JavaScript will see empty tags or placeholder text rather than actual engagement data.

When Client-Side Rendering Makes Sense

Client-side fetching is appropriate when comment badges are purely decorative or secondary—such as inside a lazy-loaded slide-out drawer, dynamic hover card, or personalization bar that depends on user authentication state.

Implementing Client-Side Counts Resiliently

If you must fetch counts in the browser, collect all thread identifiers across the DOM and fire a single batched fetch event after first paint:

<!-- Server-rendered container with reserved width -->
<span class="comment-badge" data-thread-key="article-1042">
  <span class="badge-placeholder">&mdash;</span>
</span>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const badges = document.querySelectorAll('.comment-badge[data-thread-key]');
  if (!badges.length) return;

  const threadKeys = Array.from(badges).map(el => el.getAttribute('data-thread-key'));
  const uniqueKeys = [...new Set(threadKeys)];

  fetch(`https://api.echothread.io/v1/counts?threads=${uniqueKeys.join(',')}`)
    .then(res => res.json())
    .then(data => {
      badges.forEach(badge => {
        const key = badge.getAttribute('data-thread-key');
        if (data[key] !== undefined) {
          badge.textContent = `${data[key]} ${data[key] === 1 ? 'comment' : 'comments'}`;
        }
      });
    })
    .catch(() => {
      // Retain neutral placeholder on failure
    });
});
</script>

From an accessibility standpoint, any element updated dynamically after document load must follow web standards. Review the W3C WCAG 2.2 Understanding Status Messages guidance and reference MDN Web Docs on ARIA live regions when designing dynamic badges so screen readers process updates without disorienting the user.


Caching, Rate Limits, and the Cost of a Count

Because index, category, and archive pages generate far more traffic than individual articles, a count endpoint is usually the highest-frequency endpoint in your entire commenting architecture.

Managing Request Budgets

This point is context dependent and should be treated as a cautious recommendation. If the page receives substantial traffic, that unbatched approach generates 20 outbound requests per visit. Batching those 20 post IDs into a single request reduces external calls down to one per page load, and introducing an in-memory or Redis cache reduces external calls further so the CMS serves repeated visits locally.

Circuit Breakers and Timeouts

External comment services should not block your CMS from rendering an article listing. Configure your HTTP client with a strict timeout (such as 300 to 800 milliseconds). If the endpoint does not respond within that timeframe, catch the exception and fall back to 0 or an empty badge so template rendering continues uninterrupted.

Review your cache hit rate in your logging infrastructure. For listing pages with steady traffic, most requests should be served directly from cache. If you notice frequent cache misses, check whether dynamic query parameters are fragmenting your cache keys or if your cache lifetime is set too low.

Understanding Pricing Models and Usage Limits

When selecting a platform, be clear on what your vendor actually meters. Some platforms bill per API request, while others bill strictly on loaded widget views.


What the Number Should Count: Approved, Pending, or Both

Most technical guides focus entirely on HTTP transport and ignore data classification. For an editorial team or community moderator, the decision of what gets counted is critical.

Approved vs. Pending vs. Spam

A raw database query might return all rows associated with a thread ID. However, an API comment count should only return publicly approved comments. Counting unapproved or flagged comments introduces serious operational issues:

  • Reader Frustration: A reader clicks an index card showing "5 comments," opens the article, and finds an empty thread because all five submissions are pending review.
  • Spam Amplification: If automated bot spam inflates your public count badge, bad actors receive visible confirmation that their payloads registered in your system.
  • Moderation Desynchronization: Editors looking at listing pages cannot tell whether a post has a genuine, active discussion or is simply being hit by automated comment spam.

Managing the Pre-Moderation Queue Lag

If your editorial team enforces strict pre-moderation, a popular article may have 40 comments waiting in review while the public card displays 3 comments. This behavior is correct: the public count must represent the live reading experience. As moderators approve submissions, webhook events should invalidate the cache and increment the number.

For community leads handling heavy daily discussion volumes, our tactical operational guide details how to triage high-volume comment queues effectively without leaving threads stranded in an unreviewed state.


Crawlers, Search, and AI Readers: Making the Count and the Thread Legible

A server-rendered count badge gives search engines an immediate signal about user engagement and discussion density directly in the initial HTML document.

Why Server-Rendered Badges Matter for Crawlers

Search bots budget their crawling resources carefully. According to official documentation from Google Search Central on JavaScript SEO basics, search engine crawlers process HTML instantly, whereas client-side rendered JavaScript content must enter an execution queue that may delay or omit indexing.

When your CMS serves the comment count directly in the server response, search crawlers capture and index your discussion metrics immediately. This social proof often surfaces directly in search snippets, enhancing organic click-through rates.

The Thread Itself: Crawling Full Discussions

The same logic applies to the discussion text itself. If your comments are only injected via client-side JavaScript, search crawlers and AI search agents cannot reliably index the user-generated content. Look for platforms that provide dedicated server-side discussion endpoints. A per-thread endpoint that publishers can render server-side lets search and AI crawlers that do not execute JavaScript read the discussion, while the widget injects an equivalent block for crawlers that do. For a deeper technical comparison on crawler compatibility across modern discussion systems, read our analysis on which comment systems can AI crawlers read.


Edge Cases That Break Comment Counts in Production

Custom CMS implementations frequently encounter edge cases that corrupt comment counts if unaccounted for in your data layer:

1. Unpublished, Draft, or Deleted Posts

If an article is drafted, deleted, or soft-deleted in your CMS, its associated thread key may still hold comments in your discussion provider. Ensure your CMS list queries filter by publication status (such as status = 'published') before passing thread keys to the count endpoint. Otherwise, your CMS may display comment badges for unlisted or restricted articles.

2. Content Mergers and Canonical Redirects

When consolidating two related articles into one, setting up an HTTP 301 redirect handles page navigation, but it leaves two distinct discussion threads in your comment database. To fix this, update your thread key mapping table so the consolidated URL points explicitly to the target thread key, or export and merge the historical comment records.

3. Scheduled Publishing

Avoid allowing front-end templates to query counts for future-dated or scheduled posts. If a staging environment or preview URL shares thread keys with your production site, internal testing can attach comments to an unreleased article, leaking discussion counts prior to launch.

4. Closed Threads

Closing a thread to new submissions should not remove the historical count. In mature discussion tools, closing stops new submissions while keeping existing comments visible and readable. EchoThread can close a thread to new comments 30, 60, 90, 180, or 365 days after that thread was created, or leave threads open indefinitely. Existing comments stay visible and readable, and the widget renders a closed thread read-only with a plain explanation shown to signed-out readers as well as signed-in ones. The state is derived at request time rather than written onto threads, so changing or clearing the setting reopens them, and a thread an owner manually re-opens stays exempt from the schedule. This thread-closing feature is included across every tier, including the free Hobby plan. Your CMS template should continue fetching and displaying the comment count normally on closed articles, accompanied by a subtle lock icon or "Closed" label in the metadata.

5. Multi-Language and Multi-Regional Content

Determine whether multi-language versions of an article should share a single unified discussion or maintain separate threads per language. If an international media site publishes translations under /en/post-slug and /es/post-slug, passing a localized key (post-1042-en vs post-1042-es) isolates comments by region, whereas using a shared parent key (post-1042) unifies discussion numbers across all languages.


A 30-Minute Implementation Checklist

Follow this checklist to deploy comment counts on your custom CMS architecture safely:

  • [ ] Define Content Key: Ensure an immutable thread_key is assigned to every content item in your CMS database at creation time.
  • [ ] Batch Queries: Collect all thread keys on the active archive or index template and fetch them in a single batch API call.
  • [ ] Add Caching: Implement a key-value store (Redis or local memory cache) with a TTL between 60 and 300 seconds.
  • [ ] Configure Network Timeouts: Set a strict timeout (300–800 ms) on the API call with a default fallback of 0.
  • [ ] Defensive Markup: Reserve container dimensions in CSS to avoid Cumulative Layout Shift if any data hydrates asynchronously.
  • [ ] Filter Moderation Status: Verify with your comment provider that the API count endpoint strictly tallies approved, published comments.
  • [ ] Register Webhooks: Configure an invalidation webhook to bust specific cached keys when new comments are approved or removed.
  • [ ] Isolate Staging: Verify that local and staging environments use separate API keys or sandbox thread identifiers so testing does not pollute production counts.

Frequently Asked Questions

How do I get a comment count from a comment API without slowing down my page?

Fetch counts in a batched HTTP request for the articles on the page rather than making individual calls per post. Cache the returned values in your application layer (such as Redis or memory) with a short expiration window, and configure a strict upstream timeout so an unresponsive count API does not delay your page response.

Should comment counts include pending and spam comments?

No. Comment counts should only reflect approved, publicly readable comments. Including pending submissions or spam entries inflates engagement metrics, confuses visitors who click through to find an empty thread, and signals to spammers that their submissions registered in your database.

Why does my comment count show zero on some posts?

This usually happens when the thread identifier changes dynamically after publication. If your CMS derives keys from mutable values like the full page URL, changing a slug, adding tracking parameters (like ?utm_source), or altering trailing slashes generates a new thread key with zero comments. Storing an immutable thread ID on the article record resolves this issue.

Can search engines and AI crawlers see my comment counts?

Search crawlers can easily see comment counts if they are rendered into the server-side HTML response prior to delivery. If counts are injected purely through client-side JavaScript after page load, search engines that defer or skip JavaScript execution will fail to index the counts and any associated discussion text.

Do I need to render the whole comment widget just to show a count?

No, and you should avoid doing so. Full commenting widgets include substantial JavaScript, stylesheets, and authentication logic intended for interactive discussion threads. On archive and index pages, query a dedicated, lightweight REST or GraphQL count endpoint that returns raw integers without loading UI script bundles.


If you need a discussion platform that offers dedicated count endpoints, batched reads, and server-rendered discussion blocks, EchoThread installs as a single script tag with vanilla JavaScript and zero dependencies. EchoThread offers a free Hobby plan with usage limits (1 site, unlimited comments, and 10,000 page views a month on sites created on or after 1 October 2026; sites created before that date keep unmetered page views) alongside paid Starter, Pro, and Business tiers; it is not unconditionally free forever.

Discussion

Comments

This thread runs on EchoThread — the same widget you would add to your own site.

No comments yet.

Ready to try EchoThread?

Free for your first site. Set up in under a minute.

Create free account