Seit dem Umzug meines Blogs von WordPress hin zu einer statischen Website Anfang des Jahres hatte ich ganz leicht, aber wirklich nur ganz bisschen die Kommentarfunktion vermisst. Es gab hier sowieso nur selten Kommentare. Eine Interaktionsmöglichkeit ist aber trotzdem schön.

Deshalb haben wir heute die Möglichkeit im Blog integriert, in dafür freigeschalteten Blogbeiträgen per Mastodon zu kommentieren.

Vielen Dank an die Menschen, die mir auf Mastodon auf meine Frage hin antworteten. Darüber konnte ich diesen Blogbeitrag finden, der das Vorgehen für Hugo erklärt.

Update
2022-11-06: Die Einbindung von Kommentaren aus Mastodon erfolgt jetzt transparent, siehe hier.

Was ist Mastodon?§

Mastodon ist ein Teil vom Fediverse. Jeder Mensch kann dort einen Account bei einem beliebigen Anbieter einer Instanz erstellen.

Man kann sich das vorstellen wie bei E-Mails. Jeder Mensch hat einen anderen E-Mail-Anbieter und kann trotzdem mit jedem anderen Menschen auf der Welt kommunizieren.

Vorab: Wie es funktioniert§

Für Menschen, die kommentieren§

Entweder nach unten scrollen oder rechts auf die Sprechblase (rechts oben im Blogbeitrag) klicken. Dann landet man hier:

Dann “Kommentare anzeigen (via Mastodon)” anklicken:

Oder einen der oberen Links anklicken, um zu kommentieren oder den Tröt in einem neuen Tab zu öffnen.

Um zu kommentieren, benötigt man entweder einen Mastodon-Account, einen Friendica-Account oder irgend einen anderen Account eines Dienstes des Fediverse.

Für mich als Betreiber des Blogs§

  1. Blogbeitrag veröffentlichen.
  2. Link zum Blogbeitrag auf Mastodon tröten.
  3. Im veröffentlichen Blogbeitrag im Front Matter einfügen:
comments:
  host: social.anoxinon.de
  username: natenom
  id: 123456789
  1. Blogbeitrag erneut veröffentlichen mit dem geänderten Front Matter.

Die ID ist die lange Zahl am Ende des Links eines Tröts (Toots).

Ich habe bewusst entschieden, den Host immer im Front Matter des Blogbeitrag zu hinterlegen, statt ihn global in der Konfiguration von Hugo zu hinterlegen. Denn es könnte sein, dass ich zukünftig eine andere Instanz verwenden werde und dann sollten bisherige Kommentare weiterhin zugreifbar bleiben.

Der Nachteil dieser Lösung ist, dass es immer etwas dauert (da der Blog neu generiert wird), bis Kommentare im Blog eingebunden werden können, obwohl es bereits einen Tröt auf Mastodon gibt. Das macht aber nichts, da man trotzdem bereits auf Mastodon antworten kann und diese Antworten später auch über den Blog sichtbar sind.

(Man könnte auch kurz vor dem Veröffentlichen des Beitrags bereits einen Mastodon-Tröt zum Blogbeitrag schreiben, müsste dann aber das Vorschaubild und den Titel manuell eintragen und würde nur wenig Zeit sparen.)

Umsetzung für meinen Blog§

Hier die Anleitung, wie wir das mit dem hier verwendeten Theme “Hugo Theme Bootstrap” umgesetzt haben.

Wir haben den Quelltext des oben bereits genannten Blogbeitrags an den Blog angepasst.

Info
Vielen Dank an Vri, die dafür gesorgt hat, dass das Ergebnis schön aussieht.

Template§

Dieser Quelltext kommt in die Datei layouts/partials/post/comments/custom.html:

Quelltext der Datei
{{ with .Params.comments }}
<div class="masto-comments datenschutz">
<p><a href="/ueber/datenschutz/#kommentare">(Datenschutzinformationen zu Kommentaren via Mastodon)</a></p>
</div>
<div id="mastodon-comments-wrapper">
  <div id="mastodon-comments-buttons">
    <a class="btn btn-sm btn-outline-primary" href="https://{{ .host }}/interact/{{ .id }}?type=reply" target="_blank">Kommentar schreiben</a>
    <a class="btn btn-sm btn-outline-primary" href="https://{{ .host }}/@{{ .username }}/{{ .id }}" target="_blank">Tröt öffnen</a>
  </div>
  <div id="mastodon-comments-list">
    <button id="load-comments" class="btn btn-sm btn-primary">Kommentare anzeigen (via Mastodon)</button>
  </div>
  <noscript><p>Du benötigst JavaScript, um die Kommentare hier anzeigen zu lassen.</p></noscript>
  <script src="/my_js/purify.min.js"></script>
  <script>
  document.getElementById('load-comments').addEventListener('click', loadComment)

  function escapeHtml(unsafe) {
    return unsafe
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  function loadComment() {
    document.getElementById('load-comments').innerHTML = 'Lade …';
    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" />`);
          });
          replyCreationDate = new Date(reply.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', hour: 'numeric', minute: 'numeric' });
          replyCreationDateLong = new Date(reply.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric', hour: 'numeric', minute: 'numeric' });
          mastodonComment =
          `<div class="mastodon-comment">
            <div class="avatar">
              <img src="${escapeHtml(reply.account.avatar_static)}" alt="">
            </div>
            <div class="content">
              <div class="author-wrapper">
                <a class="author" href="${reply.account.url}" rel="nofollow">
                  <span class="author-name">${reply.account.display_name}</span>
                  <span class="author-handle">${escapeHtml(reply.account.acct)}</span>
                </a>
                <a class="date" href="${reply.uri}" rel="nofollow" title="${replyCreationDateLong}">
                  ${replyCreationDate}
                </a>
              </div>
              <div class="mastodon-comment-content">${reply.content}</div> 
            </div>
          </div>`;
          document.getElementById('mastodon-comments-list').appendChild(DOMPurify.sanitize(mastodonComment, {'RETURN_DOM_FRAGMENT': true}));
        });
      } else {
          document.getElementById('mastodon-comments-list').innerHTML = "<p>Keine Kommentare gefunden</p>";
      }
    });
  };
  </script>
</div>
{{ else }}
<p class="mastodon-comments nocomment">Kommentare sind für diesen Blogbeitrag (noch) nicht aktiviert. Nach der Veröffentlichung eines Blogbeitrags dauert das in der Regel noch ein paar Minuten.</p>
{{ end }}
<p><details class="mastodon-comments email">
  <summary class="mastodon-comments email-summary">Alternative: Anmerkung per E-Mail</summary>
  <div class="mastodon-comments emailcontent">
    <p>Du kannst mir Anmerkungen zum Blogbeitrag per E-Mail schicken, <a href='mailto:natenom@mailbox.org?subject=Anmerkung zum Blog&body=Hallo Natenom. Zum Blogbeitrag "{{ $.Page.Permalink }}" habe ich eine Anmerkung:'>wenn du hier klickst</a>. Der Inhalt davon bleibt privat und wird nicht hier im Kommentarbereich veröffentlicht.</p>
  </div>
</details></p>
Info
Der Inhalt des Blogbeitrags, von dem ich den oberen Quelltext entnommen habe, ist lizensiert unter Creative Commons BY-SA 4.0. Daher gehe ich davon aus, dass auch der dort gelistete Quelltext unter diese Lizenz fällt. Sollte dem nicht so sein, so bitte ich um eine Nachricht. Entsprechend steht der hier angepasste Quelltext unter der selben Lizenz zur Verfügung.

CSS§

Das SCSS kommt in die Datei assets/main/scss/_custom.scss:

CSS für die Kommentarfunktion
/* Für Kommentar, die von Mastodon geladen werden. :) */
#mastodon-comments-wrapper {
	#mastodon-comments-buttons {
		margin-bottom: 0.5em;
	}
	#mastodon-comments-list {
		button#load-comments {
			border: 0;
			border-radius: var(--#{$prefix}border-radius);
			background-color: var(--#{$prefix}btn-border-color);
			margin: 0 auto;
		}
		.mastodon-comment {
			display: grid;
			grid-gap: 1em;
			grid-template-columns: auto 1fr;
			.avatar img {
				border-radius: var(--#{$prefix}border-radius);
				position: relative;
				top: 6px;
				width: 48px;
			}
			.content {
				.author-wrapper {
					.author {
						.author-name {
							font-weight: bold;
						}
						&:hover {
							color: var(--hbs-link-color);
							text-decoration: none;
							.author-name {
								color: var(--hbs-link-hover-color);
								text-decoration: underline;
							}
						}
					}
					.date {
						float: right;
					}
				}
			}
		}
	}
}

JavaScript§

Es wird die Bibliothek DomPurify benötigt. Die steht unter einer freien Lizenz zur Verfügung und kann somit verwendet werden.

Die Datei habe ich heruntergeladen und nach static/my_js/purify.min.js abgelegt.

Header (auf dem Webserver)§

Auf meinem Server sind die Header für den Blog relativ streng eingestellt.

Damit JavaScript Daten von extern laden darf, musste der Header in der Nginx-Konfiguration von:

Eadd_header Content-Security-Policy "default-src 'none'; font-src 'self'; connect-src 'self'; form-action 'self'; img-src 'self' data: https://*.natenom.com https://*.natenom.de; media-src https://*.natenom.com https://*.natenom.de; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none';";

geändert werden nach:

Eadd_header Content-Security-Policy "default-src 'none'; font-src 'self'; connect-src 'self' https://social.anoxinon.de; form-action 'self'; img-src 'self' https://social.anoxinon.de data: https://*.natenom.com https://*.natenom.de; media-src https://*.natenom.com https://*.natenom.de; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none';";

Sowohl im Bereich connect-src als auch bei img-src musste die Domain der Mastodon-Instanz eingetragen werden.

Datenschutz§

Die Informationen zum Datenschutz wurden um den Bereich “Kommentare” erweitert, siehe hier.

Was noch fehlt§

Tröts für Kommentarfunktion nicht automatisch löschen lassen§

Meine Mastodon-Tröts werden bei mir derzeit nach einem Monat automatisch gelöscht. Kommentare würden so nach dieser Zeit nicht mehr angezeigt werden.

Es ist jedoch in Mastodon einstellbar, dass bestimmte Tröts nicht gelöscht werden, z. B. solche, die man zu den Lesezeichen hinzugefügt hat. Dadurch könnte ich die Lesezeichen-Funktion aber nicht mehr für andere Inhalte benutzen.

Eine Alternative wäre, die Automatisch-Löschen-Funktion von Mastodon nicht mehr zu verwenden und stattdessen ein externes Script (siehe hier). Dieses Tool könnte man so konfigurieren, dass es keine Toots löscht, die ein bestimmtes Hashtag beinhalten.

Eingerückt§

Es wäre schön, wenn auch auf der Seite erkennbar wäre, auf welchen Mastodon-Tröt genau jemand geantwortet hat. Die Reihenfolge passt zwar schon, aber die Einrückungen fehlen noch.