Adding comments to your static blog with Mastodon

One of the biggest disadvantages of static site generators is that they are static and can’t include comments.

There are multiples solutions to solve this problem. You could add a third party blog engine like Disqus, but this has the drawback of including a third-party tool with a bad privacy record in your website. Another solution would be to host an open-source alternative but this comes at the cost of a higher maintenance burden. Having to host a database was something we wanted to avoid with a static site generator.

In my opinion, a better solution is to leverage the Mastodon and Fediverse platform. Mastodon is a decentralized social network and it allows people to communicate with each other without being on the same server. It is inspired by Twitter, but instead of tweeting, you write toot.

When publishing an article, you now only need to also write a simple toot linking to your article. Then Mastodon has a simple API to fetch the answer to your toot. This is the code I made for my Hugo powered blog, but it is easily adaptable for other static site generators. It will create a button to load comments instead of loading them for every visitor so that it decreases the load on your mastodon server.

{{ with .Params.comments }}
<div class="article-content">
  <p>You can use your Mastodon account to reply to this <a class="link" href="https://{{ .host }}/@{{ .username }}/{{ .id }}">post</a>.</p>
  <p><a class="button" href="https://{{ .host }}/interact/{{ .id }}?type=reply">Reply</a></p>
  <p id="mastodon-comments-list"><button id="load-comment">Load comments</button></p>
  <noscript><p>You need JavaScript to view the comments.</p></noscript>
  <script src="/assets/js/purify.min.js"></script>
  <script type="text/javascript">
    function escapeHtml(unsafe) {
      return unsafe
           .replace(/&/g, "&amp;")
           .replace(/</g, "&lt;")
           .replace(/>/g, "&gt;")
           .replace(/"/g, "&quot;")
           .replace(/'/g, "&#039;");

    document.getElementById("load-comment").addEventListener("click", function() {
      document.getElementById("load-comment").innerHTML = "Loading";
      fetch('https://{{ .host }}/api/v1/statuses/{{ .id }}/context')
        .then(function(response) {
          return response.json();
        .then(function(data) {
          if(data['descendants'] &&
             Array.isArray(data['descendants']) && 
            data['descendants'].length > 0) {
              document.getElementById('mastodon-comments-list').innerHTML = "";
              data['descendants'].forEach(function(reply) {
                reply.account.display_name = escapeHtml(reply.account.display_name);
                reply.account.emojis.forEach(emoji => {
                  reply.account.display_name = reply.account.display_name.replace(`:${emoji.shortcode}:`,
                    `<img src="${escapeHtml(emoji.static_url)}" alt="Emoji ${emoji.shortcode}" height="20" width="20" />`);
                mastodonComment =
                  `<div class="mastodon-comment">
                     <div class="avatar">
                       <img src="${escapeHtml(reply.account.avatar_static)}" height=60 width=60 alt="">
                     <div class="content">
                       <div class="author">
                         <a href="${reply.account.url}" rel="nofollow">
                           <span class="disabled">${escapeHtml(reply.account.acct)}</span>
                         <a class="date" href="${reply.uri}" rel="nofollow">
                           ${reply.created_at.substr(0, 10)}
                       <div class="mastodon-comment-content">${reply.content}</div> 
                document.getElementById('mastodon-comments-list').appendChild(DOMPurify.sanitize(mastodonComment, {'RETURN_DOM_FRAGMENT': true}));
          } else {
            document.getElementById('mastodon-comments-list').innerHTML = "<p>Not comments found</p>";
{{ end }}

This code is using DOMPurify to sanitize the input, since it is not a great idea to load data from third party sources without sanitizing them first. Also thanks to chrismorgan, the code was optimized and is more secure.

In my blog post, I can now add the following information to my frontmatter, to make comments appears magically.

  username: carl
  id: 105105837504372590


You can use your Mastodon account to reply to this post.


Licensed under CC BY-SA 4.0