Introduction
Recently, I posted about a new blog artical on Mastodon and got a response which made me think maybe I should add comments to the articals I am writing. Now I didn’t just want to add some random box that anyone can post in, rather I wanted to display any comments I get Mastodon on my website.
One thing to note, I am using Hugo with the PaperMod theme. This guide will be dependent on PaperMod as it has a built in way to add comments, it might work on other themes but you will need to check.
I want to be clear, I didn’t code this. Rather, I found a few guides online which I will be in the sources at the bottom.
Setup
You will need to edit themes/papermod/layouts/partials/comments.html
and delete everything in there and add:
{{ with .Params.comments }}
<br>
<hr>
<br>
<section id="comments" class="article-content">
<h2>Comments</h2><br>
<p>You can respond to this post on Mastodon: <a href="https://{{ .host }}/@{{ .username }}/{{ .id }}">🔗</a> or click <button onclick="copyURL()">📋</button> to copy the URL to your clipboard so you can paste it into your client.
<p id="mastodon-comments-list"><button id="load-comment">Load comments</button></p>
<div id="comments-wrapper">
<noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://{{ .host }}/@{{ .username }}/{{ .id }}">the original post</a> on Mastodon.</p></noscript>
</div>
<noscript>You need JavaScript to view the comments.</noscript>
<script src="/js/purify.min.js"></script>
<script type="text/javascript">
function copyURL() {
// Get the text field
var copyText = "https://{{ .host }}/@{{ .username }}/{{ .id }}";
navigator.clipboard.writeText(copyText);
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function emojify(input, emojis) {
let output = input;
emojis.forEach(emoji => {
let picture = document.createElement("picture");
let source = document.createElement("source");
source.setAttribute("srcset", escapeHtml(emoji.url));
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let img = document.createElement("img");
img.className = "emoji";
img.setAttribute("src", escapeHtml(emoji.static_url));
img.setAttribute("alt", `:${ emoji.shortcode }:`);
img.setAttribute("title", `:${ emoji.shortcode }:`);
img.setAttribute("width", "20");
img.setAttribute("height", "20");
picture.appendChild(source);
picture.appendChild(img);
output = output.replace(`:${ emoji.shortcode }:`, picture.outerHTML);
});
return output;
}
function loadComments() {
let commentsWrapper = document.getElementById("comments-wrapper");
document.getElementById("load-comment").innerHTML = "";
fetch('https://{{ .host }}/api/v1/statuses/{{ .id }}/context')
.then(function(response) {
return response.json();
})
.then(function(data) {
let descendants = data['descendants'];
if(
descendants &&
Array.isArray(descendants) &&
descendants.length > 0
) {
commentsWrapper.innerHTML = "";
descendants.forEach(function(status) {
console.log(descendants)
if( status.account.display_name.length > 0 ) {
status.account.display_name = escapeHtml(status.account.display_name);
status.account.display_name = emojify(status.account.display_name, status.account.emojis);
} else {
status.account.display_name = status.account.username;
};
let instance = "";
if( status.account.acct.includes("@") ) {
instance = status.account.acct.split("@")[1];
} else {
instance = "{{ .host }}";
}
const isReply = status.in_reply_to_id !== "{{ .id }}";
let op = false;
if( status.account.acct == "{{ .username }}" ) {
op = true;
}
status.content = emojify(status.content, status.emojis);
let avatarSource = document.createElement("source");
avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let avatarImg = document.createElement("img");
avatarImg.className = "avatar";
avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
avatarImg.setAttribute("alt", `@${ status.account.username }@${ instance } avatar`);
let avatarPicture = document.createElement("picture");
avatarPicture.appendChild(avatarSource);
avatarPicture.appendChild(avatarImg);
let avatar = document.createElement("a");
avatar.className = "avatar-link";
avatar.setAttribute("href", status.account.url);
avatar.setAttribute("rel", "external nofollow");
avatar.setAttribute("title", `View profile at @${ status.account.username }@${ instance }`);
avatar.appendChild(avatarPicture);
let instanceBadge = document.createElement("a");
instanceBadge.className = "instance";
instanceBadge.setAttribute("href", status.account.url);
instanceBadge.setAttribute("title", `@${ status.account.username }@${ instance }`);
instanceBadge.setAttribute("rel", "external nofollow");
instanceBadge.textContent = instance;
let display = document.createElement("span");
display.className = "display";
display.setAttribute("itemprop", "author");
display.setAttribute("itemtype", "http://schema.org/Person");
display.innerHTML = status.account.display_name;
let header = document.createElement("header");
header.className = "author";
header.appendChild(display);
header.appendChild(instanceBadge);
let permalink = document.createElement("a");
permalink.setAttribute("href", status.url);
permalink.setAttribute("itemprop", "url");
permalink.setAttribute("title", `View comment at ${ instance }`);
permalink.setAttribute("rel", "external nofollow");
permalink.textContent = new Date( status.created_at ).toLocaleString('en-US', {
dateStyle: "long",
timeStyle: "short",
});
let timestamp = document.createElement("time");
timestamp.setAttribute("datetime", status.created_at);
timestamp.appendChild(permalink);
let main = document.createElement("main");
main.setAttribute("itemprop", "text");
main.innerHTML = status.content;
let interactions = document.createElement("footer");
if(status.favourites_count > 0) {
let faves = document.createElement("a");
faves.className = "faves";
faves.setAttribute("href", `${ status.url }/favourites`);
faves.setAttribute("title", `Favorites from ${ instance }`);
faves.textContent = "★ " + status.favourites_count;
interactions.appendChild(faves);
}
let comment = document.createElement("article");
comment.id = `comment-${ status.id }`;
comment.className = isReply ? "comment comment-reply" : "comment";
comment.setAttribute("itemprop", "comment");
comment.setAttribute("itemtype", "http://schema.org/Comment");
comment.appendChild(avatar);
comment.appendChild(header);
comment.appendChild(timestamp);
comment.appendChild(main);
comment.appendChild(interactions);
if(op === true) {
comment.classList.add("op");
avatar.classList.add("op");
avatar.setAttribute(
"title",
"Blog post author; " + avatar.getAttribute("title")
);
instanceBadge.classList.add("op");
instanceBadge.setAttribute(
"title",
"Blog post author: " + instanceBadge.getAttribute("title")
);
}
commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
});
}
});
}
window.onload = loadComments
//document.getElementById("load-comment").addEventListener("click", loadComments);
</script>
<a href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">Find out how to add comments to your hugo website - 🔗</a>
</section>
{{ end }}
Now you will want add this to the top of your artical (like where you put the name etc)
comments:
host: MASTODON_URL
username: URL
id: POSTID
You will need to get the ID of the post you want as the comments before you publish. Sort of a pain but not the end of the world. What I am going to do is just post the artical then post on Mastodon and then update the page.
Next we need to add some files. First, you will need to download purify.min.js
and add it to static/js/
. You can donwload it from HERE.
Then you will want to add the styiling to static/css/comments.css
section#comments #comments-wrapper {
margin: 1.5em 0;
padding: 0 var(--card-padding);
}
section#comments .comment {
display: grid;
column-gap: 1rem;
grid-template-areas: "avatar name" "avatar time" "avatar post" "...... interactions";
grid-template-columns: min-content;
justify-items: start;
margin: 0em auto 0em -1em;
padding: 0.5em;
}
section#comments .comment.comment-reply {
margin: 0em auto 0em 1em;
}
section#comments .comment .avatar-link {
grid-area: avatar;
height: 4rem;
position: relative;
width: 4rem;
}
section#comments .comment .avatar-link .avatar {
height: 100%;
width: 100%;
}
section#comments .comment .avatar-link.op::after {
background-color: var(--accent-color);
border-radius: 50%;
bottom: -0.25rem;
color: var(--accent-color-text);
content: "✓";
display: block;
font-size: 1.25rem;
font-weight: bold;
height: 1.5rem;
line-height: 1.5rem;
position: absolute;
right: -0.25rem;
text-align: center;
width: 1.5rem;
}
section#comments .comment .author {
align-items: center;
display: flex;
font-weight: bold;
gap: 0.5em;
grid-area: name;
}
section#comments .comment .author .instance {
background-color: var(--code-background-color);
border-radius: 9999px;
color: var(--neutral);
font-size: smaller;
font-weight: normal;
padding: 0.25em 0.75em;
}
section#comments .comment .author .instance:hover {
opacity: 0.8;
text-decoration: none;
}
section#comments .comment .author .instance.op {
background-color: var(--accent-color);
color: var(--accent-color-text);
}
section#comments .comment .author .instance.op::before {
content: "✓";
font-weight: bold;
margin-inline-end: 0.25em;
margin-inline-start: -0.25em;
}
section#comments .comment .emoji {
display: inline;
height: 1.25em;
vertical-align: middle;
width: 1.25em;
}
section#comments .comment .invisible {
display: none;
}
section#comments .comment .ellipsis::after {
content: "…";
}
Last thing we need to do is make sure the JS and CSS are loaded, so go ahead and eddit themes/papermod/layouts/partials/extend_head.html
with these two lines:
<script type="text/javascript" src="/js/purify.min.js"></script>
<link rel="stylesheet" href="/css/comments.css">
And that is it, you should see comments at the bottom of your page.
Sources
Once again, I did not write this code. I am really not a front end person. I just used these people’s posts to guide me and made some edits along my way. Most of the Credit for the code goes to Carl but I did use the others to figure out what I was doing wrong.
Comments
You can respond to this post on Mastodon: 🔗 or click to copy the URL to your clipboard so you can paste it into your client.