Description
Cet article décrit avec plus ou moins de finesse - ç-à-d que je vais m’efforcer de ne pas oublier de détails - l’inclusion d’un système de commentaires affiché en bas de page d’articles fait avec Hugo, utilisant le fediverse, Mastodon principalement.
Mes exigences à ce propos :
- être le plus simple à mettre en place
- utiliser le fediverse, afin de ne pas avoir à “modèrer” et surtout ne pas avoir à modèrer localement
Utiliser le fediverse nécessite :
- d’avoir un compte sur un serveur gérant ActivityPub
- donne du sens pour un site web parlant d’informatique
- améliore grandement la visibilité
Mastodon-comments
Il se trouve que le sentier est déjà fortement balisé !
Un développeur, nommé Daniel Pecos Martínez, a écrit il y a trois ans un composant web qui intégre les commentaires, republiages et autres fonctionnalités venant d’un compte Mastodon.
Un détail à bien comprendre :
⇒ il est strictement nécessaire de :
- publier l’article créé avec Hugo sur n’importe quel hébergement
- de faire une annonce depuis un compte sur Mastodon,
- de copier l’identificant du toot pour l’intégrer dans le frontmatter de l’article,
- puis de republier l’article sur l’hébergement dédié.
Et, oui, cela se fait en deux temps…
Le code est disponible sur : https://github.com/dpecos/mastodon-comments
Quelques détails à assimiler :
- ce code utilise quelques bibliothèques (à savoir DOMPurify, Font Awesome) depuis un des CDN de CloudFare ; le choix du développeur ne voulant pas héberger lesdites bibliothèques.
- l’outil est chargé de manière dynamique au grés du scroll du lecteur
Si ces choix vous plaisent, il n’y a vraiment qu’à copier l’exemple du fichier index pour intégrer directement et très rapidement ce système de commentaires.
Mais (c|s)es choix ne me vont pas !
Je ne veux pas :
- utiliser de bibliothèques (plus ou moins) lourdes à distance, et surtout pas depuis CloudFare.
- utiliser Font Awesome qui nécessite le chargement de plusieurs icônes pour n’afficher au final que quelques unes.
- d’autant que principalement, j’utilise celles disponibles sur le site tablericons.com localement au-travers du système de données Hugo.Data
Car oui, je tiens à utiliser du code SVG pour afficher les icônes disponibles.
Mastodon-comments-modifié
Hugo.Data
Dans le répertoire data, j’utilise un fichier nommé SVG.yaml, où l’on retrouve les icônes copiés principalement depuis Tablericons.
Je dépose ici le code des 4 utilisés :
menu-order: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-menu-order" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h16" /><path d="M4 14h16" /><path d="M9 18l3 3l3 -3" /><path d="M9 6l3 -3l3 3" /></svg>'
repeat: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-repeat" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" /><path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" /></svg>'
reply: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-reply" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" /><path d="M11 8l-3 3l3 3" /><path d="M16 11h-8" /></svg>'
star: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-star" viewBox="0 0 24 24" fill="currentColor"><path d="M8.243 7.34l-6.38 .925l-.113 .023a1 1 0 0 0 -.44 1.684l4.622 4.499l-1.09 6.355l-.013 .11a1 1 0 0 0 1.464 .944l5.706 -3l5.693 3l.1 .046a1 1 0 0 0 1.352 -1.1l-1.091 -6.355l4.624 -4.5l.078 -.085a1 1 0 0 0 -.633 -1.62l-6.38 -.926l-2.852 -5.78a1 1 0 0 0 -1.794 0l-2.853 5.78z" /></svg>'
Dans le répertoire _partials, j’ai deux autres sous-répertoires :
- un premier nommé
utilsoù se situe le fichier permettant l’appel des icônes SVG, nommé très basiquementicon.html - un deuxième nommé
componentsoù j’ai placé le script JS nécessaire que j’ai modifié pour utiliser lesdites icônes SVG.
partial utils/icon
{{- $ := index . "$" -}}
{{- $name := .name -}}
{{- $class := .class | default $name -}}
{{- $icon := index hugo.Data.SVG $name -}}
{{- $class := printf "icon %s" $class -}}
{{- return (replace $icon "icon" $class | safeHTML) -}}
Ce code n’a pas été écrit par mes soins… mais il est simple et fonctionnel
ajouts CSS
Il est nécessaire d’ajouter une taille aux icônes SVG dans le code CSS, tel que :
article.mastodon-comment svg
{
height: 24px;
width: 24px;
}
#mastodon-stats svg {
height: 32px;
width: 32px;
}
- Bien sûr, à vous de vous adapter !
Pour finir cette partie, je redéfinis dans le fichier principal CSS la taille de l’élément mastodon-comments -et non plus dans le code source HTML.
#comments mastodon-comments {
max-width: 1024px;
}
- Ce qui permettra de l’adapter selon les medias utilisés…
partial component/mastodon-comments-modified.js
<script defer type="module">
// ============================================================
// Set language dependend texts
// ============================================================
const i18n = {
en: {
commentsTitle: "Comments",
replyIntro:
"You can use your Fediverse (i.e. Mastodon, among many others) account to reply to this",
replyLinkText: "post",
replyButtonLabel: "Reply",
loadingText: "Loading comments from the Fediverse...",
noCommentsText: "No comments found",
dialogTitle: "Reply to this post",
dialogCloseTitle: "Close",
dialogCloseSymbol: "×",
dialogExplain:
"Comments are powered by Mastodon. With an account on Mastodon (or elsewhere on the Fediverse), you can respond to this post. Simply enter your mastodon instance below, and add a reply:",
goButtonLabel: "Go",
copyAlternativeText:
"Alternatively, copy this URL and paste it into the search bar of your Mastodon app:",
copyButtonLabel: "Copy",
copiedButtonLabel: "Copied!",
instancePlaceholder: "mastodon.social",
instanceMissingAlert: "Please provide the name of your instance",
dateLocale: "en-US",
},
de: {
commentsTitle: "Kommentare",
replyIntro:
"Du kannst mit deinem Fediverse-Konto (z.B. Mastodon) auf diesen",
replyLinkText: "Beitrag antworten",
replyButtonLabel: "Antworten",
loadingText: "Kommentare werden aus dem Fediverse geladen...",
noCommentsText: "Keine Kommentare gefunden",
dialogTitle: "Auf diesen Beitrag antworten",
dialogCloseTitle: "Schließen",
dialogCloseSymbol: "×",
dialogExplain:
"Kommentare werden über Mastodon bereitgestellt. Mit einem Konto bei Mastodon (oder anderswo im Fediverse) kannst du auf diesen Beitrag antworten. Gib einfach deine Mastodon-Instanz unten ein:",
goButtonLabel: "Los",
copyAlternativeText:
"Alternativ kannst du diese URL kopieren und in die Suchleiste deiner Mastodon-App einfügen:",
copyButtonLabel: "Kopieren",
copiedButtonLabel: "Kopiert!",
instancePlaceholder: "mastodon.social",
instanceMissingAlert: "Bitte gib den Namen deiner Instanz an",
dateLocale: "de-DE",
},
fr: {
commentsTitle: "Commentaires",
replyIntro:
"Vous pouvez utiliser votre compte Fediverse (ex. Mastodon) pour répondre à ce",
replyLinkText: "billet",
replyButtonLabel: "Répondre",
loadingText: "Chargement des commentaires depuis le Fediverse…",
noCommentsText: "Aucun commentaire trouvé",
dialogTitle: "Répondre à ce billet",
dialogCloseTitle: "Fermer",
dialogCloseSymbol: "×",
dialogExplain:
"Les commentaires sont alimentés par Mastodon. Avec un compte sur Mastodon (ou ailleurs sur le Fediverse), vous pouvez répondre à ce billet. Entrez simplement votre instance Mastodon ci-dessous : ",
goButtonLabel: "Aller",
copyAlternativeText:
"Vous pouvez aussi copier cette URL et la coller dans la barre de recherche de votre application Mastodon : ",
copyButtonLabel: "Copier",
copiedButtonLabel: "Copié !",
instancePlaceholder: "mastodon.social",
instanceMissingAlert: "Veuillez indiquer le nom de votre instance",
dateLocale: "fr-FR",
},
es: {
commentsTitle: "Comentarios",
replyIntro:
"Puedes usar tu cuenta del Fediverso (ej. Mastodon, entre otros) para comentar este",
replyLinkText: "toot",
replyButtonLabel: "Responder",
loadingText: "Cargando comentarios del Fediverso...",
noCommentsText: "No se han encontrado comentarios",
dialogTitle: "Responde a este post",
dialogCloseTitle: "Cerrar",
dialogCloseSymbol: "×",
dialogExplain:
"Comentarios gracias a Mastodon. Con una cuenta en Mastodon (o en cualquier otro sitio en el Fediverso), puedes responder a esta entrada. Simplemente introduce tu instancia y pulsa responder:",
goButtonLabel: "Ir",
copyAlternativeText:
"O puedes copiar esta URL y pegarla en el cuadro de búsqueda de tu aplicación Mastodon:",
copyButtonLabel: "Copiar",
copiedButtonLabel: "¡Copiado!",
instancePlaceholder: "mastodon.social",
instanceMissingAlert: "Por favor introduce tu instancia",
dateLocale: "es-ES",
},
};
const DEFAULT_LANG = "fr";
// ============================================================
const icon_clock = "{{ partial "utils/icon" (dict "$" $ "name" "clock") }}";
const icon_reply = "{{ partial "utils/icon" (dict "$" $ "name" "reply") }}";
const icon_retweet = "{{ partial "utils/icon" (dict "$" $ "name" "repeat") }}";
const icon_star = "{{ partial "utils/icon" (dict "$" $ "name" "star") }}";
const styles = `
:root {
--font-color: #5d686f;
--font-size: 1.0rem;
--block-border-width: 1px;
--block-border-radius: 3px;
--block-border-color: #ededf0;
--block-background-color: #f7f8f8;
--dialog-background-color: #ffffff;
--muted-color: #5d686f;
--reply-color: #003eaa;
--reblog-color: #8c8dff;
--favourite-color: #ca8f04;
--button-color: #595aff;
--button-hover-color: #4849ff;
--comment-indent: 40px;
}
@media (prefers-color-scheme: dark) {
:root {
--font-color: #d1d1d1;
--block-border-color: #3e3e3e;
--block-background-color: #1e1e1e;
--dialog-background-color: #2d2d2d;
--muted-color: #8a9ba3;
--reply-color: #7b9fff;
--reblog-color: #a8a9ff;
--favourite-color: #e6b44a;
--button-color: #6c6dff;
--button-hover-color: #595aff;
}
}
[data-theme="dark"] {
--font-color: #d1d1d1;
--block-border-color: #3e3e3e;
--block-background-color: #1e1e1e;
--dialog-background-color: #2d2d2d;
--muted-color: #8a9ba3;
--reply-color: #7b9fff;
--reblog-color: #a8a9ff;
--favourite-color: #e6b44a;
--button-color: #6c6dff;
--button-hover-color: #595aff;
}
mastodon-comments {
font-size: var(--font-size);
}
p {
margin: 0 0 1rem 0;
}
#mastodon-stats {
text-align: center;
font-size: calc(var(--font-size) * 1.5);
}
#mastodon-title {
font-size: calc(var(--font-size) * 1.5);
font-weight: bold;
}
#mastodon-comments-list {
margin: 0 auto;
padding: 0;
}
#mastodon-comments-list ul {
padding-left: var(--comment-indent);
}
#mastodon-comments-list li {
list-style: none;
}
.mastodon-comment {
background-color: var(--block-background-color);
border-radius: var(--block-border-radius);
border: var(--block-border-width) var(--block-border-color) solid;
padding: 15px;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
color: var(--font-color);
}
.mastodon-comment p {
margin-bottom: 0px;
}
.mastodon-comment .author {
padding-top:0;
display:flex;
}
.mastodon-comment .author a {
text-decoration: none;
}
.mastodon-comment .author .avatar img {
margin-right:1rem;
min-width:60px;
border-radius: 5px;
}
.mastodon-comment .author .details {
display: flex;
flex-direction: column;
line-height: 1.2em;
}
.mastodon-comment .author .details .name {
font-weight: bold;
}
.mastodon-comment .author .details .user {
color: var(--muted-color);
font-size: medium;
}
.mastodon-comment .author .date {
margin-left: auto;
font-size: small;
}
.mastodon-comment .content {
margin: 15px 0;
line-height: 1.5em;
}
.mastodon-comment .author .details a,
.mastodon-comment .content p {
margin-bottom: 10px;
}
.mastodon-comment .attachments {
margin: 0px 10px;
}
.mastodon-comment .attachments > * {
margin: 0px 10px;
}
.mastodon-comment .attachments img {
max-width: 100%;
}
.mastodon-comment .status > div, #mastodon-stats > div {
display: inline-block;
margin-right: 15px;
}
.mastodon-comment .status a, #mastodon-stats a {
color: var(--muted-color);
text-decoration: none;
}
.mastodon-comment .status .replies.active a, #mastodon-stats .replies.active a {
color: var(--reply-color);
}
.mastodon-comment .status .reblogs.active a, #mastodon-stats .reblogs.active a {
color: var(--reblog-color);
}
.mastodon-comment .status .favourites.active a, #mastodon-stats .favourites.active a {
color: var(--favourite-color);
}
@media only screen and (max-width: 640px) {
.mastodon-comment .author {
flex-wrap: nowrap;
align-items: flex-start;
}
.mastodon-comment .author .date {
margin-left: 0;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
}
dialog {
border: none;
border-radius: var(--block-border-radius);
box-shadow: 0 0 10px rgba(0,0,0,0.1);
padding: 20px;
max-width: 500px;
width: 90%;
color: var(--font-color);
background-color: var(--dialog-background-color);
}
dialog::backdrop {
background: rgba(0,0,0,0.5);
}
dialog h3 {
margin-top: 0;
}
dialog #close {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--font-color);
}
.input-row {
display: flex;
gap: 10px;
margin: 10px 0;
}
.input-row input {
flex-grow: 1;
padding: 8px;
border: 1px solid var(--block-border-color);
border-radius: var(--block-border-radius);
background-color: var(--block-background-color);
color: var(--font-color);
}
.button {
padding: 8px 16px;
background-color: var(--button-color);
color: white;
border: none;
border-radius: var(--block-border-radius);
cursor: pointer;
}
#add-comment {
display: block;
margin: 10px 0;
}
.button:hover {
background-color: var(--button-hover-color);
}
`;
class MastodonComments extends HTMLElement {
constructor() {
super();
this.host = this.getAttribute("host");
this.user = this.getAttribute("user");
this.tootId = this.getAttribute("tootId");
this.filter = this.getAttribute("filter");
this.commentsLoaded = false;
this.tootAccountURI = null;
// Resolve locale: attribute → <html lang> → default
const lang =
this.getAttribute("lang") ||
document.documentElement.lang?.split("-")[0] ||
navigator.language?.split("-")[0] ||
DEFAULT_LANG;
this.locale = i18n[lang] ?? i18n[DEFAULT_LANG];
this.dateFormatter = new Intl.DateTimeFormat(this.locale.dateLocale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
formatMatcher: "basic",
});
if (!document.getElementById("mastodon-comments-styles")) {
const styleElem = document.createElement("style");
styleElem.id = "mastodon-comments-styles";
styleElem.innerHTML = styles;
document.head.appendChild(styleElem);
}
}
connectedCallback() {
this.mastodonPostUrl = `https://${this.host}/@${this.user}/${this.tootId}`;
this.innerHTML = `
<div id="mastodon-stats"></div>
<div id="mastodon-title">${this.locale.commentsTitle}</div>
<p>${this.locale.replyIntro} <a class="link"
href="${this.mastodonPostUrl}" rel="ugc">${this.locale.replyLinkText}</a>.
<button id="add-comment" class="button">${this.locale.replyButtonLabel}</button>
</p>
<ul id="mastodon-comments-list"></ul>
<dialog id="comment-dialog">
<h3>${this.locale.dialogTitle}</h3>
<button title="${this.locale.dialogCloseTitle}" id="close">${this.locale.dialogCloseSymbol}</button>
<p>${this.locale.dialogExplain}</p>
<div class="input-row">
<input type="text" inputmode="url" autocapitalize="none" autocomplete="off" value="${
this.escapeHtml(localStorage.getItem("mastodonUrl")) ?? ""
}" id="instanceName" placeholder="${this.locale.instancePlaceholder}">
<button class="button" id="go">${this.locale.goButtonLabel}</button>
</div>
<p>${this.locale.copyAlternativeText}</p>
<div class="input-row">
<input type="text" readonly id="copyInput" value="${this.mastodonPostUrl}">
<button class="button" id="copy">${this.locale.copyButtonLabel}</button>
</div>
</dialog>
`;
const comments = this.querySelector("#mastodon-comments-list");
const rootStyle = this.getAttribute("style");
if (rootStyle) {
comments.setAttribute("style", rootStyle);
}
this.respondToVisibility(comments, this.loadComments.bind(this));
this.initDialog();
}
initDialog() {
const dialog = this.querySelector("#comment-dialog");
const addCommentBtn = this.querySelector("#add-comment");
const closeBtn = this.querySelector("#close");
const goBtn = this.querySelector("#go");
const copyBtn = this.querySelector("#copy");
const instanceNameInput = this.querySelector("#instanceName");
const copyInput = this.querySelector("#copyInput");
addCommentBtn.addEventListener("click", () => {
dialog.showModal();
});
closeBtn.addEventListener("click", () => {
dialog.close();
});
dialog.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
dialog.close();
}
});
dialog.addEventListener("click", (event) => {
var rect = dialog.getBoundingClientRect();
var isInDialog =
rect.top <= event.clientY &&
event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width;
if (!isInDialog) {
dialog.close();
}
});
goBtn.addEventListener("click", () => {
let url = instanceNameInput.value.trim();
if (url === "") {
window.alert(this.locale.instanceMissingAlert);
return;
}
localStorage.setItem("mastodonUrl", url);
if (!url.startsWith("https://")) {
url = `https://${url}`;
}
window.open(
`${url}/authorize_interaction?uri=${this.mastodonPostUrl}`,
"_blank",
);
});
instanceNameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
goBtn.dispatchEvent(new Event("click"));
}
});
copyBtn.addEventListener("click", () => {
copyInput.select();
navigator.clipboard.writeText(this.mastodonPostUrl);
copyBtn.innerHTML = this.locale.copiedButtonLabel;
window.setTimeout(() => {
copyBtn.innerHTML = this.locale.copyButtonLabel;
}, 1000);
});
}
escapeHtml(unsafe) {
return (unsafe || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
toot_active(toot, what) {
var count = toot[what + "_count"];
return count > 0 ? "active" : "";
}
toot_count(toot, what) {
var count = toot[what + "_count"];
return count > 0 ? count : "";
}
toot_stats(toot) {
return `
<div class="replies ${this.toot_active(toot, "replies")}">
<a href="${
toot.url
}" rel="ugc nofollow">${icon_reply} ${this.toot_count(
toot,
"replies",
)}</a>
</div>
<div class="reblogs ${this.toot_active(toot, "reblogs")}">
<a href="${
toot.url
}/reblogs" rel="nofollow">${icon_retweet} ${this.toot_count(
toot,
"reblogs",
)}</a>
</div>
<div class="favourites ${this.toot_active(toot, "favourites")}">
<a href="${
toot.url
}/favourites" rel="nofollow">${icon_star} ${this.toot_count(
toot,
"favourites",
)}</a>
</div>
`;
}
user_account(account) {
var result = `@${account.acct}`;
if (account.acct.indexOf("@") === -1) {
const domain = new URL(account.url);
result += `@${domain.hostname}`;
}
return result;
}
formatDate(dateString) {
return this.dateFormatter
.format(new Date(dateString))
.replace(",", "")
.replace(/(\d+)\/(\d+)\/(\d+)/, "$3-$1-$2"); // only necessary when dealing with "en-US" locale
}
async render_toots(toots, in_reply_to) {
const filterFunction = async (toot) => {
const isReplyToToot = toot.in_reply_to_id === in_reply_to;
let isFilteredOut = false;
if (isReplyToToot && this.filter) {
let isTootByOwner = false;
let isFavoritedByOwner = false;
if (this.filter === "favorites") {
isTootByOwner = toot.account.username === this.user;
if (!isTootByOwner && toot.favourites_count > 0) {
const data = await fetch(
"https://" +
this.host +
"/api/v1/statuses/" +
toot.id +
"/favourited_by",
).then((response) => response.json());
isFavoritedByOwner = data.some(
(record) => record.username === this.user,
);
}
isFilteredOut = !isTootByOwner && !isFavoritedByOwner;
}
}
return isReplyToToot && !isFilteredOut;
};
// apply async filter and wait for all to finish
const tootsToRender = [];
await Promise.all(
toots.map(async (toot) => {
if (await filterFunction(toot)) {
tootsToRender.push(toot);
}
}),
);
tootsToRender
.sort((a, b) => a.created_at.localeCompare(b.created_at))
.forEach((toot) => {
this.render_toot(toots, toot);
});
}
render_toot(toots, toot) {
toot.account.display_name = this.escapeHtml(toot.account.display_name);
toot.account.emojis.forEach((emoji) => {
toot.account.display_name = toot.account.display_name.replace(
`:${emoji.shortcode}:`,
`<img src="${this.escapeHtml(emoji.static_url)}" alt="Emoji ${
emoji.shortcode
}" height="20" width="20" />`,
);
});
const mastodonComment = `
<article class="mastodon-comment">
<div class="author">
<div class="avatar">
<a class="user" href="${
toot.account.url
}" rel="nofollow"><img src="${this.escapeHtml(
toot.account.avatar_static
)}" height=60 width=60 alt="${
toot.account.username
}" loading="lazy">
</a>
</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">${this.user_account(toot.account)}</a>
</div>
<a class="date" href="${toot.url}" rel="nofollow">
${icon_clock}
<time datetime="${toot.created_at}">
${this.formatDate(toot.created_at)}${toot.edited_at ? " (*)" : ""}
</time>
</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="ugc nofollow"><img src="${
attachment.preview_url
}" alt="${this.escapeHtml(attachment.description)}" loading="lazy" /></a>`;
} else if (attachment.type === "video") {
return `<video controls preload="none"><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="ugc nofollow">${attachment.type}</a>`;
}
})
.join("")}
</div>
<div class="status">
${this.toot_stats(toot)}
</div>
</article>
`;
var li = document.createElement("li");
li.setAttribute("id", toot.id);
li.innerHTML =
typeof DOMPurify !== "undefined"
? DOMPurify.sanitize(mastodonComment.trim())
: mastodonComment.trim();
if (toot.in_reply_to_id === this.tootId) {
this.querySelector("#mastodon-comments-list").appendChild(li);
} else {
const parentToot = toots.find((t) => t.id === toot.in_reply_to_id);
if (parentToot) {
const ul = document.createElement("ul");
this.querySelector(`[id="${toot.in_reply_to_id}"]`)
.appendChild(ul)
.appendChild(li);
}
}
this.render_toots(toots, toot.id);
}
loadComments() {
if (this.commentsLoaded) return;
this.querySelector("#mastodon-comments-list").innerHTML =
this.locale.loadingText;
const _this = this;
fetch(`https://${this.host}/api/v1/statuses/${this.tootId}`)
.then((response) => response.json())
.then((toot) => {
this.querySelector("#mastodon-stats").innerHTML = this.toot_stats(toot);
this.tootAccountURI = toot.account.uri;
});
fetch(`https://${this.host}/api/v1/statuses/${this.tootId}/context`)
.then((response) => response.json())
.then((data) => {
if (
data.descendants &&
Array.isArray(data.descendants) &&
data.descendants.length > 0
) {
this.querySelector("#mastodon-comments-list").innerHTML = "";
_this.render_toots(data.descendants, _this.tootId, 0);
} else {
this.querySelector("#mastodon-comments-list").innerHTML =
`<p>${this.locale.noCommentsText}</p>`;
}
_this.commentsLoaded = true;
});
}
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);
}
}
customElements.define("mastodon-comments", MastodonComments);
</script>
Où l’on remarquera les détails suivants :
- modification de la langue par défaut en
fr - l’ajout des 4 icônes par la création de 4 constantes adéquats, nécessitant la modification du code, principalement celui de la fonction
toot_stats. En passant, remarquez l’inclusion de l’appel au partieliconci-dessus. - je n’ai pas touché aux définitions de style CSS.
PS : En passant, j’ai fait une PR à l’auteur pour de très petites corrections linguistiques, en rapport avec le FR… en espérant qu’il veuille bien l’intégrer, sinon ce n’est pas grave, d’autant qu’elles sont faites dans mon code.
partial comments
Création d’un partiel nommé logiquement comments qui est appelé depuis le fichier principal single :
{{- if and site.Params.enable.comments .Params.mastodon.tootId }}
{{- $u := urls.Parse .Params.mastodon.tootId -}}
{{- $s := strings.Split $u.RequestURI "/" -}}
{{- $user := substr (index $s 1) 1 | safeHTML -}}
{{- $tootId := index $s 2 | safeHTML -}}
{{- partial "components/mastodon-comments-modified.js" . -}}
<div id="comments">
<mastodon-comments host="{{ $u.Hostname }}" user="{{ $user }}" tootId="{{ $tootId }}">
</mastodon-comments>
</div>
{{- end }}
Comme vous pouvez le remarquer, il nécessite la création et l’utilisation de nouveaux paramètres Hugo.
- un premier paramètre permettant l’activation du système de commentaires au sein de Hugo.
- un dernier, celui-ci qui sera créé dans le frontmatter de l’article à promouvoir.
Le code ne s’affichera que si et seulement si le système de commentaires est activé, dans les paramètres Hugo, ET s’il y a bien un identifiant de message Mastodon dans le frontmatter de l’article en question !
PS : J’ai eu dans l’idée, très forte, de simplifier encore plus afin de n’avoir qu’à copier-coller l’URL complète de l’identifiant du post au sein de Mastodon dans le frontmatter et “d’éclater” l’URL copié pour obtenir les noms d’hôte et d’utilisateur dédié.
params enable:comments
Dans le fichier de configuration d’Hugo, ajoutons -ici au format YAML, depuis un fichier dédié nommé params.yaml ; à vous d’adapter… :
enable:
(…)
comments: true
modification de l’archétype
Modification de l’archétype afin d’ajouter lors de la création de nouvel article le paramètre adéquat :
params:
mastodon:
tootId:
- la variable
tootIdrecevra l’identifiant du post Mastodon. Il faudra copier l’URL complète dans le frontmatter de l’article concerné !
mastodon-comments.css
Dans le sous-répertoire css de mes assets, j’ai crée le fichier mastodon-comments.css pour y copier-coller le code ad hoc :
/*
mastodon-comments {
--font-color: #5d686f;
--font-size: 1.0rem;
--block-border-width: 1px;
--block-border-radius: 3px;
--block-border-color: #ededf0;
--block-background-color: #f7f8f8;
--comment-indent: 40px;
}
*/
@media only screen and (max-width: 1024px) {
mastodon-comments {
--comment-indent: 20px;
}
}
@media only screen and (max-width: 640px) {
mastodon-comments {
--comment-indent: 0px;
}
}
body {
font-family: sans-serif;
background-color: #ffffff;
color: #222222;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #d1d1d1;
}
}
C’est exactement celui fourni par le projet mastodon-comments. Je n’ai fait que le mettre dans un fichier CSS à part, qui est appelé dans mon code Hugo écrit dans le partial link qui sert à gérer le code CSS.
Je ne montre pas l’appel CSS écrit dans le partial.
purify.js
Dans le sous-répertoire js de mes assets, j’ai un énième répertoire nommé vendors où j’ai déposé le code copié du projet DOM Purify, qui est appelé par le code Hugo écrit dans le partial js pour la gestion des appels javascript.
Là encore, j’ai modifié mon appel des scripts javascript afin qu’il tienne compte de l’activation du système de commentaires dans les paramètres Hugo. Si celui-ci n’est pas actif, alors l’appel au fichier JS relatif ne se fait pas, tel que :
(…)
{{- if site.Params.enable.comments }}
{{- $purify := resources.Get "js/vendors/purify.js" -}}
{{- $global = $global | append $purify -}}
{{- end }}
{{- $global = $global | resources.Concat "js/1.js" -}}
{{- with $global }}
(…)
- Bien sûr, à vous d’adapter selon votre code Hugo !
PS : Je me demande même s’il ne serait pas intéressant de n’inclure ce code si ET seulement si un id de message mastodon existe aussi dans le frontmatter… à creuser !
Voilà !
J’espère sincérement ne rien avoir oublié…
Cet article vous donnera peut-être des idées à partager vous aussi ; faites-le en commentaires ;-)
URLs
Retrouvez-ici les différentes URL utiles :
- le projet DOM Purify et sa bibliothèque disponible depuis le dépôt GitHub.
⇒ mes modifications de code :
- css/mastodon-comments.css
- partial comments
- partial component/mastodon-comments-modified.js
Remerciements
Il y a deux articles, dont l’un renvoie vers le premier qui m’ont énormément aidé dans la démarche :