Back to blog

How to add comments to an Eleventy (11ty) site

Eleventy has no comment system and does not want one; it builds HTML. This guide adds a hosted comment section with a single include file in Nunjucks, Liquid or Handlebars, explains which page variable to use as the thread identifier, and shows how to turn comments off per post.

Eleventy turns templates into HTML and stops there, which is exactly why people choose it. It also means comments have to come from somewhere else: a hosted service that loads in the reader's browser and keeps the discussion attached to each page. This guide adds that with one include file and no Eleventy plugin, in whichever template language you already use.

The widget used here is EchoThread's, which is under 60 KB gzipped, loads asynchronously after your content, and needs no build step. Readers can comment as guests or sign in; every comment is scored by a machine-learning spam filter before it appears. There is a longer version of this tutorial with screenshots in the Eleventy integration guide.

Prerequisites

  • An Eleventy site, any version.
  • An EchoThread account (create one free), a site added in the dashboard with the domain you publish to, and that site's API key.

Step 1: create the include

Create _includes/echothread.njk (Nunjucks is Eleventy's default):

<div id="echothread"
     data-api-key="YOUR_API_KEY"
     data-page-url="{{ page.url | url }}"
     data-identifier="{{ page.fileSlug }}"
     data-page-title="{{ title }}"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>

Replace YOUR_API_KEY with the key from the dashboard. Everything else is filled in by Eleventy at build time.

Step 2: add it to your post layout

Open the layout your posts use, for example _includes/layouts/post.njk, and include the file after the content:

---
layout: layouts/base.njk
---
<article>
  <h1>{{ title }}</h1>
  {{ content | safe }}

  <!-- Comments -->
  {% include "echothread.njk" %}
</article>

Build, deploy, and every page using that layout has a comment section. That is the whole install.

Choosing the thread identifier

data-identifier is what keeps a discussion attached to a page even if its URL changes later. Eleventy gives you three candidates:

  • page.fileSlug: the filename without its extension, for example my-first-post. Stable, readable, and unaffected by permalink changes. Use this one.
  • page.url: the full path, for example /blog/my-first-post/. Fine until you restructure your URLs, at which point every thread detaches.
  • page.inputPath: the source path, for example ./src/blog/my-first-post.md. Stable but leaks your directory layout into the identifier.

If you later move a post between directories, fileSlug still matches as long as the filename does. That is the property you want.

Liquid and Handlebars

The HTML is identical; only the include syntax changes.

Liquid, in _includes/echothread.liquid:

<div id="echothread"
     data-api-key="YOUR_API_KEY"
     data-page-url="{{ page.url }}"
     data-identifier="{{ page.fileSlug }}"
     data-page-title="{{ title }}"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>

Include it with {% include 'echothread.liquid' %} or {% render 'echothread.liquid' %}.

Handlebars, in _includes/echothread.hbs, uses the same markup with {{ page.url }}, {{ page.fileSlug }} and {{ title }}, and is included with {{> echothread}}. Eleventy processes an include with the same engine as the parent template, so keep the extension matching the layout's language.

Theming

Two attributes cover most sites:

<!-- Dark mode -->
data-theme="dark"

<!-- Match a custom background -->
data-theme="#f5f0eb"

<!-- Accent colour for buttons and links -->
data-accent-color="#0ea5e9"

Because Eleventy has a data cascade, you can put those values in a directory data file such as blog/blog.json and reference them in the include as {{ echothread_theme }}, so one JSON edit restyles comments across a whole collection.

Turning comments off for one page

Wrap the include in a conditional:

{% if comments != false %}
  {% include "echothread.njk" %}
{% endif %}

Then set comments: false in the front matter of any post that should not have a comment section. The same trick works per collection through a directory data file.

Troubleshooting

  • Comments do not appear. Check the API key in the dashboard, check that the domain you registered matches the one the site is served from (add localhost while developing), and look for errors in the browser console.
  • The variables render as literal text. The include's extension does not match the template language of the layout that includes it. Nunjucks includes need .njk, Liquid .liquid, and so on.
  • Threads detached after a redesign. The identifier changed. Switch back to page.fileSlug, or, if you have to change identifiers, export the threads and re-import them against the new values.

What it costs

EchoThread's Hobby plan is free for your first site: unlimited comments, no ads, no reader tracking, and 10,000 page views a month on sites created from 1 October 2026 (a site created before that date keeps unmetered page views). Paid plans start at $9 a month for three sites and 100,000 page views; see pricing for the full ladder.

If you are choosing between hosted widgets and GitHub-backed options like giscus or utterances, the static-site comment systems roundup compares them for exactly this kind of site, and the giscus alternative post covers the one gap those tools share: your readers need a GitHub account.

Discussion

Comments

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

Ready to try EchoThread?

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

Create free account