Table des matières
Intégrer un fil de discussion Mastodon à un blog
Intégrer un système de gestion de commentaires basé sur Mastodon dans un blog sans recourir à des services externes.
10 nov 2024 · 4 min ·
Introduction
Pour la gestion des commentaires sur ce blog, je ne voulais pas utiliser de système tiers (ni en auto-hébergement, ni en mode SaaS, etc.), comme https://github.com/apps/giscus, https://commentbox.io/, etc.
En faisant quelques recherches, certains blogueurs ont eu l’idée d’utiliser Mastodon comme système de gestion des commentaires. J’ai trouvé une multitude de bouts de code qui permettent d’effectuer cette intégration, mais rien de pleinement satisfaisant.
Mes critères de choix sont :
- N’avoir aucune maintenance à faire
- Respecter la vie privée, le RGPD, etc. (pas de dépôt de cookies, etc.)
- Ne pas dégrader les performances du site
- Ne pas dégrader la vitesse de chargement des pages
- Afficher les entrées du fil hiérarchiquement
- Utiliser le framework CSS Bulma
Résultats :
- Un bouton à la disposition de l’utilisateur pour charger le fil de discussion depuis Mastodon
- Une vue hiérarchique du fil de discussion, sans limites de profondeur
- Utilisation des classes et des styles du framework CSS Bulma (utilisé par ce site)
Mode opératoire : développement
Ajouter un réceptacle pour le fil de discussion à la page
<div id="mastodon-comments" class="content"></div>
Intégrer le bout de code JavaScript
<script>
class MastodonThreadViewer {
constructor(instanceUrl) {
this.instanceUrl = instanceUrl;
this.isLoading = false;
}
// Créer le bouton et le conteneur
createStructure(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = `
<div class="field">
<div class="control">
<button class="button" id="${containerId}-load-btn">
<span class="icon">
<i class="fas fa-comments"></i>
</span>
<span>Charger et afficher la discussion depuis Mastodon</span>
</button>
</div>
</div>
<div id="${containerId}-thread" class="thread-container"></div>
`;
return {
button: document.getElementById(`${containerId}-load-btn`),
threadContainer: document.getElementById(`${containerId}-thread`)
};
}
// Récupérer le fil complet
async getFullThread(statusId) {
try {
// Récupérer le status initial
const statusResponse = await fetch(`${this.instanceUrl}/api/v1/statuses/${statusId}`);
const originalStatus = await statusResponse.json();
// Récupérer le contexte (réponses)
const contextResponse = await fetch(`${this.instanceUrl}/api/v1/statuses/${statusId}/context`);
const context = await contextResponse.json();
return {
originalStatus,
descendants: context.descendants
};
} catch (error) {
console.error('Erreur lors de la récupération du fil :', error);
throw error;
}
}
// Organiser les réponses en structure hiérarchique
organizeThread(originalStatus, replies) {
const statusMap = new Map();
// Ajouter le status original
statusMap.set(originalStatus.id, {
...originalStatus,
children: []
});
// Ajouter toutes les réponses
replies.forEach(reply => {
statusMap.set(reply.id, {
...reply,
children: []
});
});
// Construire la hiérarchie
statusMap.forEach(status => {
if (status.in_reply_to_id && statusMap.has(status.in_reply_to_id)) {
const parent = statusMap.get(status.in_reply_to_id);
parent.children.push(status);
}
});
// Trier les enfants par date
statusMap.forEach(status => {
status.children.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
});
return statusMap.get(originalStatus.id);
}
// Générer le HTML pour un statut et ses réponses
renderStatus(status, isRoot = false) {
const date = new Date(status.created_at).toLocaleString();
const html = `
<article class="media ${isRoot ? 'root-status' : ''}">
<figure class="media-left">
<p class="image is-rounded is-48x48">
<img src="${status.account.avatar}" alt="${status.account.username}">
</p>
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>${status.account.display_name}</strong>
<small>@${status.account.username}</small>
<small>${date}</small>${isRoot ? ' <span class="tag is-info is-small">Post original</span>' : ''}</p>
${status.content}
</div>
${status.children
.map(child => this.renderStatus(child))
.join('')}
</div>
</article>
`;
return html;
}
// Mettre à jour l'état du bouton
updateButtonState(button, isLoading) {
if (!button) return;
const buttonText = button.querySelector('span:not(.icon)');
if (isLoading) {
button.classList.add('is-loading');
button.disabled = true;
if (buttonText) buttonText.textContent = 'Chargement...';
} else {
button.classList.remove('is-loading');
button.disabled = false;
if (buttonText) buttonText.textContent = 'Charger et afficher la discussion depuis Mastodon';
}
}
// Initialiser le viewer avec le bouton de chargement
initialize(statusId, containerId) {
const elements = this.createStructure(containerId);
if (!elements) return;
const { button, threadContainer } = elements;
// Ajouter le CSS pour le post original
const style = document.createElement('style');
style.textContent = `
.root-status {
border-left: 3px solid #3298dc;
padding-left: 1rem;
margin-bottom: 1.5rem;
}
.thread-container {
margin-top: 1rem;
}
`;
document.head.appendChild(style);
button.addEventListener('click', async () => {
if (this.isLoading) return;
this.isLoading = true;
this.updateButtonState(button, true);
try {
const { originalStatus, descendants } = await this.getFullThread(statusId);
const organizedThread = this.organizeThread(originalStatus, descendants);
if (!organizedThread) {
threadContainer.innerHTML = `
<div class="notification is-info">
Impossible de charger la discussion.
</div>
`;
return;
}
threadContainer.innerHTML = this.renderStatus(organizedThread, true);
} catch (error) {
console.error('Erreur lors du chargement de la discussion:', error);
threadContainer.innerHTML = `
<div class="notification is-danger">
Une erreur est survenue lors du chargement de la discussion.
</div>
`;
} finally {
this.isLoading = false;
this.updateButtonState(button, false);
}
});
}
}
// Exemple d'utilisation :
document.addEventListener('DOMContentLoaded', () => {
const viewer = new MastodonThreadViewer('https://mastodon.social');
viewer.initialize('{{ page.mastodonid }}', 'mastodon-comments');
});
</script>
Personnaliser les variables
La personnalisation s’effectue à partir de la ligne 188, dans cet extrait :
// Exemple d'utilisation :
document.addEventListener('DOMContentLoaded', () => {
const viewer = new MastodonThreadViewer('https://mastodon.social');
viewer.initialize('{{ page.mastodonid }}', 'mastodon-comments');
});
Où :
https://mastodon.social
est l’URL de votre instance Mastodon{{ page.mastodonid }}
est l’ID du post Mastodon, racine du fil de discussion
Mode opératoire : opérationnel
- Publier l’article
- Publier un post sur Mastodon
- Récupérer l’ID du post
- Intégrer cet ID dans l’article
- Republier l’article
Résultat
- Au chargement de la page, un bouton apparaît (“Charger et afficher la discussion depuis Mastodon”).
- Au clic sur le bouton “Charger et afficher la discussion depuis Mastodon”, le fil de discussion se charge et s’affiche.
Testez directement le bouton “Charger et afficher la discussion depuis Mastodon” de cette page !
Articles similaires
- Créer une table des matières automatique et masquable
14 nov 2024 · ≤ 1 min ·
- Partager simplement un article sur les réseaux sociaux avec Open Graph
12 nov 2024 · 2 min ·
- Mettre en oeuvre la pagination avec Jekyll et Bulma.
11 nov 2024 · 2 min ·
- Implémenter la numérotation automatique des titres via CSS.
29 oct 2024 · ≤ 1 min ·
- Mémo de syntaxe Markdown.
27 oct 2024 · 3 min ·
- Bulma, démarrage rapide.
18 oct 2024 · 2 min ·
Partage et commentaires
Vous pouvez commenter cet article grâce à ce fil de discussion sur Mastodon (avec Mastodon ou un autre compte ActivityPub/​Fediverse).