Mastodon Comments on Hugo

When porting to this site I’d forgotten a good chunk of all this, particularly some key details about what was and what wasn’t required. Wasting hours on unnecessary, complex nonsense in the process.

A few years back I’d looked around for an ActivityPub based blog, far and away the best of these is write.as. However, as yet, ActivityPub interactions aren’t part of the system.

So, dispensing with the notion I could create my own. I pondered if I could pull in activity from an ActivityPub api and display them on my static site. I did what I do best and searched to see if anyone else had already done the work for me. Thankfully, Björn Schießle had, which you can see in action here. His implementation is rather more involved than I was planning but I took the idea to use the mastodon api and nicked his styles.

Bare Bones Implementation

This is what it the comment section looks like in my blog post Hugo template. A div to hold any comments found at runtime and a build time check on a page parameter to see if the blog post has a mastodon post associated with it. If it doesn’t, a message is displayed saying comments not enabled for this particular post.

<div id="comments" class="comments">
    {{ if not .Params.commentid }}  
        <div class="reference">
                Comments are handled by my <a href='https://sarcasm.stream/@basil'>Mastodon account</a>.
        </div>
    {{ end }}
</div>

In the footer Hugo template, I call the javascript that goes looking for mastodon comments like this.

{{ if .Params.commentid }}
<script src="/js/jquery-ajax.js"></script>
<script src="/js/mastodoncomments.js"></script>
<script>
  getComments({{ .Params.commentid }});
</script>
{{ end }}

First, this section has the same check as earlier. If the page doesn’t have a commentid parameter then nothing happens. If it does, then it calls getComments passing the passing the commentid parameter (more on this parameter later).

This getComments function is located in the /js/mastodoncomments.js file you see referenced above and, yes, I’m using jQuery. Sue me.

Within my Hugo project these javascript files are placed in /static/js/ and here’s the content of mastodoncomments.js.

function getComments(statusId) {
  $.ajax({
      url: "https://sarcasm.stream/api/v1/statuses/" + statusId + "/context",
      type: "get",
      success: function(replies) {
          replies.descendants.forEach(reply => {
            var timestamp = Date.parse(reply.created_at);
            var date = new Date(timestamp);
            var comment = "<div class='comment' id='" + reply.id + "'>";
              comment += "<img class='avatar' src='" + reply.account.avatar + "' />";
              comment += "<div class='author'><a class='displayName' href='" + reply.account.url + "'>" + reply.account.display_name + "</a> wrote at ";
              comment += "<a class='date' href='" + reply.url + "'>" + date.toDateString() + ', ' + date.toLocaleTimeString() + "</a></div>";
              comment += "<div class='toot'>" + reply.content + "</div>";
              comment += "</div>";
              if (reply.in_reply_to_id == statusId) {
                document.getElementById("comments").innerHTML = document.getElementById("comments").innerHTML + comment;
              } else {
                var selector = reply.in_reply_to_id;
                document.getElementById(selector).innerHTML = document.getElementById(selector).innerHTML + comment;
            }
          });
          var join = "<div class=\"reference\"><a href=\"https://sarcasm.stream/@basil/" + statusId + "\">Join the discussion on Mastodon.</a></div>"
          document.getElementById("comments").innerHTML = document.getElementById("comments").innerHTML + join;
      }
  });
};

Here we call the Mastodon api which returns the thread of the status. The script iterates over the array of descendants, building an html string it then injects into the <div id="comments" class="comments"> element on our page. If a conversation breaks out in the thread, then it can get a bit much for the layout but I’m yet to think of a solution for this.

Where to look for the replies

Now, back to the {{ .Params.commentid }} that is being repeatedly checked for and sent to the getComments function. This is simply a page variable from the YAML front matter of the markdown file for the blog post e.g.

---
title: 100 Days to Offload
date: 2020-04-26T00:00:00.000Z
draft: false
commentid: "104064788941903280"
---

When a post is first created this variable is blank, hence all of the code above that checks if it exists with {{ if .Params.commentid }}. And here comes the low tech part. After publishing a post, if it’s something I’d like to invite comment on, I’ll manually create a post on my mastodon account, copy the mastodon post id, and add it as the commentid in the YAML block. Doing it this way means I can use the public endpoint to fetch the status details and it can all happen unauthenticated and in the frontend.

Oh, and I almost forgot, here’s the styles in static/css/comments.css.

.comments .comment .avatar {
  float: left;
  width: 50px;
  height: 50px;
  margin-right: 16px;
  border-radius: 50%;
}

.comments .reference {
  text-align: center;
  font-size: 16px;
}

.comments .comment {
  margin-top: 50px;
  margin-bottom: 50px;
  font-size: 16px;
}

.comments .comment .comment {
  padding-left: 20px;
}

.comments .toot {
  padding-left: 66px;
}

.comments .author {
  padding-top: 10px;
  padding-bottom: 10px;
}

And that really is it. There are obvious drawback to this. Most of them I’m fond of because less is more (tm). The big one is that there is no caching going on here, this could become a problem for a well read blog, but I’m not going to bother myself with that hard a problem when I’m not in need of the solution. Especially given this site has no back end. I could cron something on a service somewhere that triggers a Netlify rebuild of the site and fetch the comments at build time only instead, but like I say, I’ve no need for it.