I’ve decided to use the fediverse for blog comments, rather than disqus.

I was motivated by seeing a post by Veronica Olsen (yay! Another physicist!) which referenced Carl Schwan’s article and then lead me to Unixorn’s work.

I’ve modified Unixorn’s work slightly by doing the following.


Updates

2023/04/25
Unixorn has updated the partial / partials discussed in Use existing capabilities
2023/04/25
Remove absolute path to DOMPurify in layouts/partials/mastodon/mastodon.html

Use existing capabilities

Firstly, I use layouts/partials rather than layouts/partial (note the trailing s). I’m not sure if that is a typo in the article or not.

Rather than creating layouts/default/single.html, I leveraged the existing file.

I created layouts/partials/mastodon/head.html to reference the style sheet

<link rel="stylesheet" type="text/css" href="{{.Site.BaseURL}}css/mastodon.css" />

I then added (or created if you don’t already have it) the following to layouts/partials/extend_head.html

<!-- Mastodon comments -->
{{ if .Params.comments.id }}
{{ partial "mastodon/head.html" . }}
{{ end }}

What this does is it only includes a reference to the mastodon.css if I have the commends.id variable defined.

To include comments I then created layouts/partials/comments.html with the content:

{{ if .Params.comments.id }}
<div>
  {{ partial "mastodon/mastodon.html" .}}
</div>
{{ end }}

As before, if comments.id exists, then include the mastodon/mastodon.html partial Unixorn discusses.

Remove absolute path to DOMPurify

When I used Unixorn’s code for mastodon.html code and tested locally everything worked, so I deployed to my https://stewart123579.github.io/blog site.

However, there’s a wrinkle.

My base URL ends in /blog/, but the code for mastodon.html references /assets/js/purify.min.js and with that leading / my base URL drops the /blog/ breaking the display of comments.

It’s easy with Hugo to get the relative URL of a file and fix mastodon.html, all we do is remove the leading / and call the relURL function:

<script src="{{ relURL "assets/js/purify.min.js" }}"></script>

The revised version of mastodon.html is below:

{{ with .Params.comments }}
<div class="article-content">
  <h2>Comments</h2>
  <p>You can use your Mastodon account to reply to this<a class="button" href="https://{{ .host }}/@{{ .username }}/{{ .id }}">post</a>.</p>
  <p><button class="button" id="replyButton" href="https://{{ .host }}/@{{ .username }}/{{ .id }}">Reply</button></p>
  <dialog id="toot-reply" class="mastodon" data-component="dialog">
    <h3>Reply to {{ .username }}'s post</h3>
    <p>
      With an account on the Fediverse or Mastodon, you can respond to this post.
      Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.
    </p>
    <p>Copy and paste this URL into the search field of your favourite Fediverse app or the web interface of your Mastodon server.</p>
    <div class="copypaste">
      <input type="text" readonly="" value="https://{{ .host }}/@{{ .username }}/{{ .id }}">
      <button class="button" id="copyButton">Copy</button>
      <button class="button" id="cancelButton">Close</button>
    </div>
  </dialog>
  <p id="mastodon-comments-list"><button id="load-comment" class="button">Load comments</button></p>
  <noscript><p>You need JavaScript to view the comments.</p></noscript>
  <script src="{{ relURL "assets/js/purify.min.js" }}"></script>
  <script type="text/javascript">
    const dialog = document.querySelector('dialog');

    document.getElementById('replyButton').addEventListener('click', () => {
      dialog.showModal();
    });

    document.getElementById('copyButton').addEventListener('click', () => {
      navigator.clipboard.writeText('https://{{ .host }}/@{{ .username }}/{{ .id }}');
    });

    document.getElementById('cancelButton').addEventListener('click', () => {
      dialog.close();
    });

    dialog.addEventListener('keydown', e => {
      if (e.key === 'Escape') dialog.close();
    });

    const dateOptions = {
      year: "numeric",
      month: "numeric",
      day: "numeric",
      hour: "numeric",
      minute: "numeric",
    };

    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.reply_class = reply.in_reply_to_id == "{{ .id }}" ? "reply-original" : "reply-child";
                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-wrapper">
                    <div class="comment-level ${reply.account.reply_class}"><svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" fill="none" transform="rotate(180)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path stroke="#535358" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.608 12.526l7.04-6.454C13.931 4.896 16 5.806 16 7.546V11c13 0 11 16 11 16s-4-10-11-10v3.453c0 1.74-2.069 2.65-3.351 1.475l-7.04-6.454a2 2 0 010-2.948z"></path> </g></svg></div>
                    <div class="mastodon-comment">
                     <div class="avatar">
                       <img src="${escapeHtml(reply.account.avatar_static)}" height=60 width=60 alt="">
                     </div>
                     <div class="content">
                       <div class="author">
                         <a href="${reply.account.url}" rel="nofollow">
                           <span>${reply.account.display_name}</span>
                           <span class="disabled">${escapeHtml(reply.account.acct)}</span>
                         </a>
                         <a class="date" href="${reply.uri}" rel="nofollow">
                           ${reply.created_at.substr(0, 10)}
                         </a>
                       </div>
                       <div class="mastodon-comment-content">${reply.content}</div>
                     </div>
                   </div>
                  </div>`;
                document.getElementById('mastodon-comments-list').appendChild(DOMPurify.sanitize(mastodonComment, {'RETURN_DOM_FRAGMENT': true}));
              });
          } else {
            document.getElementById('mastodon-comments-list').innerHTML = "<p>No comments found</p>";
          }
        });
      });
  </script>
</div>
{{ end }}

Add dark mode to the CSS

I’m not very good with CSS, so I hacked two changes into the CSS mentioned in the article.

.dark .mastodon-comment {
  background-color: #36383d;
}
.dark .mastodon-comment .disabled {
  color: #ad55fd;
}

Otherwise I couldn’t see the discussions in my preferred dark theme.

Usage

I use ox-hugo to export my blog posts from Org Mode, so to replicate the comments stanza mentioned in the article, i.e.

comments:
  host: hachyderm.io
  username: unixorn
  id: 110149495764332469

I simply need to add the following PROPERTIES to my post in Org Mode

:PROPERTIES:
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :comments.host     hachyderm.io
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :comments.username unixorn
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :comments.id       110149495764332469
:END:

Next

I’m planning to move the comments.host and comments.username into my config.yaml so I don’t need to replicate for each post. I’ll update this post if I remember.