Hugo Relearn Mastodon Comments
Um Kommentare unter der den Artikeln zu ermöglichen, habe ich die Vorlage von Client-side comments with Mastodon on a static Hugo website (Vielen Dank dafür!) für das Hugo Relearn Theme angepasst und leicht modernisiert.
Wie im Original-Artikel beschrieben, ist in jedem Artikel im Frontmatter die ID des Mastodon Posts zu ergänzen:
comments:
id: [Mastodon toot id]
Dateien
Konfiguration und Archetype
config.yaml
Erweitere mich…
params:
comments:
host: "social.tchncs.de"
username: "wrdlbrmpft"
archetypes/default.md
Erweitere mich…
---
title: "{{ replace .Name "-" " " | title }}"
description: ""
date: {{ .Date }}
draft: true
{{- if and (isset .Site.Params.comments "host") (isset .Site.Params.comments "username") }}
{{- if and (ne .Site.Params.comments.host "") (ne .Site.Params.comments.username "") }}
comments:
host: {{ .Site.Params.comments.host }}
username: {{ .Site.Params.comments.username }}
id:
blockedcomments:
{{ end -}}
{{ end -}}
---
Templates
layouts/_partials/content-footer.html
Erweitere mich…
{{ partial "comments" . }}
layouts/_partials/comments.html
Erweitere mich…
{{- $comments := dict
"host" .Site.Params.comments.host
"user" (string .Site.Params.comments.username)
"id" (string .Params.comments.id)
"blockedcomments" .Params.comments.blockedcomments
}}
{{- if and ($comments.host) ($comments.user) ($comments.id) }}
<hr>
<h2>{{ i18n "Comments" }}</h2>
<p>
{{ i18n "With an account on the Fediverse or Mastodon, you can respond to this" }}
<a href="https://{{ $comments.host }}/@{{ $comments.user }}/{{ $comments.id }}">{{ i18n "post" }}</a>.
{{ i18n "Known replies are displayed below" }}:
</p>
<p id="mastodon-comments-list" data-params="{{ $comments | jsonify }}">
<button type="button" class="_js_loadcomments" type="button">
<i class="fa-fw fas fa-download"></i>
{{ i18n "Load Comments from " }}
{{ $comments.host }}
</button>
</p>
<script src="{{ "DOMPurify/purify.min.js" | relURL }}" defer></script>
<script src="{{ "comments.js" | relURL }}" defer></script>
{{- with (resources.Get "css/comments.scss") }}
{{- $opts := dict
"enableSourceMap" hugo.IsDevelopment
"outputStyle" (cond hugo.IsDevelopment "expanded" "compressed")
"transpiler" "dartsass"
"vars" site.Params.styles
}}
{{- $style := css.Sass . | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $style.Permalink }}" integrity="{{ $style.Data.Integrity }}">
{{- end }}
{{- end }}
Javascript
- DOMPurify in
static/DOMPurify/purify.min.js
static/DOMPurify/purify.min.js.map
static/DOMPurify/purify.js
static/DOMPurify/purify.js.map
static/comments.js
Erweitere mich…
(function () {
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function toot_active(toot, what) {
var count = toot[what + "_count"];
return count > 0 ? "active" : "";
}
function toot_count(toot, what) {
var count = toot[what + "_count"];
return count > 0 ? count : "";
}
function user_account(account) {
var result = `@${account.acct}`;
if (account.acct.indexOf("@") === -1) {
var domain = new URL(account.url);
result += `@${domain.hostname}`;
}
return result;
}
function render_toots(toots, in_reply_to, depth) {
var tootsToRender = toots
.filter((toot) => toot.in_reply_to_id === in_reply_to)
.sort((a, b) => a.created_at.localeCompare(b.created_at));
tootsToRender.forEach((toot) => render_toot(toots, toot, depth));
}
function render_toot(toots, toot, depth) {
toot.account.display_name = escapeHtml(toot.account.display_name);
toot.account.emojis.forEach((emoji) => {
toot.account.display_name = toot.account.display_name.replace(
`:${emoji.shortcode}:`,
`<img src="${escapeHtml(emoji.static_url)}" alt="Emoji ${emoji.shortcode}" height="20" width="20" />`,
);
});
if (!blockedToots.includes(toot.url)) {
mastodonComment = `<div class="mastodon-comment" style="margin-left: calc(var(--mastodon-comment-indent) * ${depth})">
<div class="author">
<div class="avatar">
<img src="${escapeHtml(toot.account.avatar_static)}" height=60 width=60 alt="">
</div>
<div class="details">
<a class="name" href="${toot.account.url}" rel="nofollow">${toot.account.display_name}</a>
<a class="user" href="${toot.account.url}" rel="nofollow">${user_account(toot.account)}</a>
</div>
<a class="date" href="${toot.url}" rel="nofollow">${toot.created_at.substr(0, 10)} ${toot.created_at.substr(11, 8)}</a>
</div>
<div class="content">${toot.content}</div>
<div class="attachments">
${toot.media_attachments
.map((attachment) => {
if (attachment.type === "image") {
return `<a href="${attachment.url}" rel="nofollow"><img src="${attachment.preview_url}" alt="${attachment.description}" /></a>`;
} else if (attachment.type === "video") {
return `<video controls><source src="${attachment.url}" type="${attachment.mime_type}"></video>`;
} else if (attachment.type === "gifv") {
return `<video autoplay loop muted playsinline><source src="${attachment.url}" type="${attachment.mime_type}"></video>`;
} else if (attachment.type === "audio") {
return `<audio controls><source src="${attachment.url}" type="${attachment.mime_type}"></audio>`;
} else {
return `<a href="${attachment.url}" rel="nofollow">${attachment.type}</a>`;
}
})
.join("")}
</div>
<div class="status">
<div class="replies ${toot_active(toot, "replies")}">
<a href="${toot.url}" rel="nofollow"><i class="fa fa-reply fa-fw"></i>${toot_count(toot, "replies")}</a>
</div>
<div class="reblogs ${toot_active(toot, "reblogs")}">
<a href="${toot.url}" rel="nofollow"><i class="fa fa-retweet fa-fw"></i>${toot_count(toot, "reblogs")}</a>
</div>
<div class="favourites ${toot_active(toot, "favourites")}">
<a href="${toot.url}" rel="nofollow"><i class="fa fa-star fa-fw"></i>${toot_count(toot, "favourites")}</a>
</div>
</div>
</div>`;
document
.getElementById("mastodon-comments-list")
.appendChild(
DOMPurify.sanitize(mastodonComment, { RETURN_DOM_FRAGMENT: true }),
);
}
render_toots(toots, toot.id, depth + 1);
}
function loadComments() {
if (commentsLoaded) return;
document.getElementById("mastodon-comments-list").innerHTML =
"Loading comments from the Fediverse...";
console.debug(
"Comment URL: https://" + host + "/api/v1/statuses/" + id + "/context",
);
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 = "";
render_toots(data["descendants"], id, 0);
} else {
document.getElementById("mastodon-comments-list").innerHTML =
"<p>No comments found</p>";
}
commentsLoaded = true;
});
}
function respondToVisibility(element, callback) {
var options = {
root: null,
};
var observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
callback();
}
});
}, options);
observer.observe(element);
}
const comments = document.getElementById("mastodon-comments-list");
const params = JSON.parse(comments.dataset.params);
const host = params.host;
const id = params.id;
const blockedToots = params.blockedcomments || [];
var commentsLoaded = false;
if (comments) {
comments.querySelector('button._js_loadcomments').addEventListener('click', (ev) => {
respondToVisibility(comments, loadComments);
ev.preventDefault();
});
respondToVisibility(comments, loadComments);
}
})();
CSS
assets/css/comments.scss
Erweitere mich…
#mastodon-comments-list {
button {
padding: 0.3em;
border-radius: 0.3em;
}
}
.mastodon-comment {
background-color: var(--block-background-color);
border-radius: var(--block-border-radius);
border: 1px var(--block-border-color) solid;
padding: 20px;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
color: var(--font-color);
font-size: var(--font-size);
p {
margin-bottom: 0px;
}
.author {
padding-top: 0;
display: flex;
a {
text-decoration: none;
}
.avatar {
img {
margin-right: 1rem;
min-width: 60px;
border-radius: 5px;
}
}
.details {
display: flex;
flex-direction: column;
.name {
font-weight: bold;
}
.user {
color: #5d686f;
font-size: medium;
}
}
.date {
margin-left: auto;
font-size: small;
}
}
.content {
margin: 15px 20px;
p:first-child {
margin-top: 0;
margin-bottom: 0;
}
}
.attachments {
margin: 0px 10px;
> * {
margin: 0px 10px;
}
}
.status {
> div {
display: inline-block;
margin-right: 15px;
}
a {
color: #5d686f;
text-decoration: none;
}
.replies.active a {
color: #003eaa;
}
.reblogs.active a {
color: #8c8dff;
}
.favourites.active a {
color: #ca8f04;
}
}
}