Client-side comments with Mastodon on a static Jekyll website

7 minute read

WARNING: As with all my technical posts, this is NOT a simple copy/paste description. If you want to use this proposed solution, I expect you to know how Jekyll works and how my changes work as described after reading this. Maybe (not very likely) I will turn this into an official plugin/addon for Jekyll, but for now it’s a rough rundown of what I did to make it work. YMMV (Your Mileage May Vary)

For many, many years this blog was a complicated, outdated and slow Wordpress instance. It became a constant bad thought: “You really need to update/fix/speed up that thing”. A while ago I did just that. I exported all blog entries to Markdown and imported all of that to a very simple Jekyll instance using the Minimal Mistakes theme. Some tweaking to make sure all links stay the same, to make sure that existing links out there don’t break and done.

Over the past few years that has moved from Github to codeberg, complete with automagic regeneration using the Woodpecker CI. Done. Nice. BUT …


I wanted to add the possibilty to comment. I checked many possibilities, but that proved to be quite disappointing. Either I would have to give up the privacy of my audience, switch back to active pages or live with weird, complicated setups that could even cost me quite some money. At that time (2-3 years ago) I was still mostly active on Twitter. But things started to change and I moved more and more to Mastodon. So - could I use Mastodon for comments? Turns out - yes, and it is technically quite simple and straightforward.

The How - API

Mastodon offers a public API which allows you to fetch all replies to a toot. Just call https://YOUR.INSTANCE.TLD/api/v1/statuses/TOOTID/context') and parse the JSON you get back with a bit of JavaScript and BOOM. A coment thread!

A comment Thread Example of a rendered comment thread

And that is just what I did. Inspired by this blog post I created a new file in my Jekyll setup - the aptly named fediverse_comments.html.

Let’s break it down. The three things you need to get the data are:

  • The mastodon server
  • The username
  • The Toot ID of the toot you want to use as the start of the thread

These three things are added in the front matter of every post that should offer comments:

title: Blog Entry Title 
  - Category
  - Tags
  username: jwildeboer
  id: 109926960604430691 

In the template for a blog post page I check if page.comments exists and if YES, include the fediverse_comments.html file.

And that file does all the things I described above. It constructs the API call, fetches the JSON, parses it and injects the resulting HTML to the page.

It also pulls in DOMpurify right at the start, which I have added to the assets/js directory. This should help a bit in mitigating XSS attacks:

<noscript><p>You have to allow JavaScript to view the comments.</p></noscript>
  <script src="/assets/js/purify.min.js"></script>

After that (and some more boring preliminaries) it starts the real work. Build the API call, fetch data, do stuff:

document.getElementById("load-comment").addEventListener("click", function() {
      document.getElementById("load-comment").innerHTML = "Loading";
        .then(function(response) {
          return response.json();

The code takes care of making it look pretty, add the little things like “how many replies, reboosts, favs?”. It’s not really complicated, but may look a bit daunting at first:

 mastodonComment =
  `<article id="comment-${ }" class="js-comment comment" itemprop="comment" itemscope itemtype="">
  <div class="js-comment comment">
  <div class="comment__avatar-wrapper">
    <img class="comment__avatar" src="${escapeHtml(reply.account.avatar_static)}" height=60 width=60 alt="">
  <div class="comment__content-wrapper">
    <h3 class="comment__author" itemprop="author" itemscope itemtype="">
    <a rel="external nofollow" itemprop="url" href="${reply.account.url}" rel="nofollow">
      <p class="comment__date">
        <a itemprop= "url" href="${reply.uri}" rel="nofollow">${reply.created_at.substr(0, 10)}</a>
      <div itemprop="text">${reply.content}</div>
      <div class="status">
        <a href="${reply.url}" rel="nofollow"><i class="fa fa-reply fa-fw"></i>${reply.replies_count}</a>&nbsp;|&nbsp; 
        <a href="${reply.url}" rel="nofollow"><i class="fa fa-retweet fa-fw"></i>${reply.reblogs_count}</a>&nbsp;|&nbsp;
        <a href="${reply.url}" rel="nofollow"><i class="fa fa-star fa-fw"></i>${reply.favourites_count}</a>
  document.getElementById('mastodon-comments-list').appendChild(DOMPurify.sanitize(mastodonComment, {'RETURN_DOM_FRAGMENT': true}));
} else {
  document.getElementById('mastodon-comments-list').innerHTML = "<p>No comments found (yet! be the first!)</p>";

The Workflow

So now when I write a blog entry (like this one) the following things happen:

  • Write/correct/write the content, with the comments section in the front matter commented out.
  • Commit the new entry to the git repository
  • Wait for the CI to finish, open the new page, copy the URL
  • Post a toot about the new blog post and bookmark that toot (see disadvantages section below for the reason)
  • Get the toot ID
  • Uncomment the comments block and add the toot ID to the frontmatter
  • Run the CI again

I have been thinking of adding webhooks or git hooks to automate that process even more, but as I am lazy I didn’t yet work on that :) This current workflow is GoodEnough™ for me.

The Result

And there you have it! A comment section on a static web page! This approach offers a lot of advantages, IMHO. Here are a few:

  • Fully client-side, no tracking on my server
  • Moderation of comments is simple and fast - just check your notfications on your Mastodon client
  • Should a thread go in the completely wrong direction, just create a new toot and replace the ID
  • Should you decide to not want any comments - don’t add the front matter

There are some disadvantages too:

  • CW (Content Warning) is completely ignored
  • Should a toot contain images/media - they are also completely ignored
  • Moderation is an all or nothing game, you cannot hide any specific toot
  • It relies on JavaScript being allowed by the user (though will still offer the link to the toot as a fallback)
  • It also relies on the referenced toot not being deleted, for example through retention settings on your instance. I have solved that by bookmarking the toots and excluding bookmarked toots from deletion.

But in sum the advantages of having interaction on my blog outweigh teh disadvantages for me, so I am really happy with this solution. It is definitely in the GoodEnough™ bucket :)

Looking forward to a discussion in the comments!

More resources

If you use Hugo, Daniel Pecos has put together a similar solution in his blog entry.

Changes I made

Here are the files in my setup that do the hard work:


You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.