Como eu implementei comentários no meu blog usando o Mastodon
Introdução
Hoje mudei (novamente) o sistema de comentários do meu site. Antes usava o Giscus - que é muito bacana e funciona através de discussões no GitHub - e agora estou usando o Fediverso! Fiz isso com base neste post aqui, do Daniel Pecos Martínez.
Nada contra o Giscus, pelo contrário: Ele é ótimo para quem quer fugir do Disqus (que não é muito legal para privacidade, e coloca anúncios no seu site), e não exige que você tenha um servidor funcionando para operar os comentários, tal qual eu tinha com o Isso.
Recentemente eu estive em uma onda muito boa de criatividade (mais sobre isso em um post futuro) e resolvi começar a usar o Mastodon para escrever alguns pensamentos do dia a dia. A qualidade das conversas lá é muito boa. Então decidi resgatar uma ideia antiga: A de usar posts no Mastodon para um sistema de comentários. Infelizmente não manjo o suficiente de programação para implementar do zero. Mas se tem uma coisa que eu sou bom é adaptar esse tipo de implementação para minhas necessidades. Foi assim que encontrei o post linkado acima. E fiz o seguinte aqui no meu site (lembrando que o código completo do site está no meu GitHub):
Parte técnica
Conteúdo dos arquivos
No arquivo config.toml
[params.comment]
  [params.comment.fediverse]
    host = "ursal.zone"
    user = "patrickcamillo"
É só colocar a instância e o nome de usuário. A princípio será para o seu site pessoal, então será sempre fixo. Mas dá pra adaptar em outro lugar caso esteja lidando com um site multi-autor ou algo assim. INCLUSIVE, bela ideia.
No arquivo (novo) themes/just-another-bear/layouts/partials/comments.html
<hr>
<h1>{{T `comments`}}</h1>
<noscript>{{T `noscriptWarning`}}</noscript>
<p style="font-size: large; ">
    {{T `commentsDesc1`}}
    <a class="link"
        href="https://{{ .Site.Params.comment.fediverse.host }}/@{{ .Site.Params.comment.fediverse.user }}/{{ .Params.fediverse }}">
        {{T `commentsDesc2`}}
    </a>
    {{T `commentsDesc3`}}
</p>
<p id="mastodon-comments-list"></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js" integrity="sha512-uHOKtSfJWScGmyyFr2O2+efpDx2nhwHU2v7MVeptzZoiC7bdF6Ny/CmZhN2AwIK1oCFiVQQ5DA/L9FSzyPNu6Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
  var host = '{{ .Site.Params.comment.fediverse.host }}';
  var user = '{{ .Site.Params.comment.fediverse.user }}';
  var id = '{{ .Params.fediverse }}'
  function escapeHtml(unsafe) {
    return unsafe
      .replace(/&/g, "&")
      .replace(/</g, "<")
      .replace(/>/g, ">")
      .replace(/"/g, """)
      .replace(/'/g, "'");
  }
  var commentsLoaded = false;
  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" />`);
    });
    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 = "{{T `commentLoad`}}";
    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>{{T `noComments`}}</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);
  }
  var comments = document.getElementById("mastodon-comments-list");
  respondToVisibility(comments, loadComments);
</script>
Algumas dessas linhas estão com um código referente ao i18n para possibilitar a tradução do meu site para inglês. Se você não usa internacionalização, é só substituir as chaves abaixo no código acima.
No arquivo em português themes/just-another-bear/i18n/pt-br.toml
[commentsDesc1]
other = "Você pode usar sua conta no Fediverso (por exemplo Mastodon, e muitas outras redes) e responder a "
[commentsDesc2]
other = "este post"
[commentsDesc3]
other = "para comentar aqui."
[comments]
other = "Comentários"
[commentLoad]
other = "Carregando comentários do Fediverso..."
[noComments]
other = "Nenhum comentário."
No arquivo para tradução para o inglês themes/just-another-bear/i18n/en.toml
[commentsDesc1]
other = "You can use your Fediverse (i.e. Mastodon, among many others) account to reply to "
[commentsDesc2]
other = "this post"
[commentsDesc3]
other = "to leave a comment here."
[comments]
other = "Comments"
[commentLoad]
other = "Loading comments from the Fediverse..."
[noComments]
other = "No comments found."
No arquivo themes/just-another-bear/layouts/partials/style.html
Por fim, algumas alterações no estilo:
    :root {
      --font-size: 1.0rem;
      --block-border-width: 1px;
      --block-border-radius: 3px;
      --mastodon-comment-indent: 40px;
    }
    /* Mastodon comments */
    .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);
    }
    .mastodon-comment p {
      margin-bottom: 0px;
    }
    .mastodon-comment img {
      border: none;
    }
    .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;
    }
    .mastodon-comment .author .details .name {
      font-weight: bold;
    }
    .mastodon-comment .author .details .user {
      color: #5d686f;
      font-size: medium;
    }
    .mastodon-comment .author .date {
      margin-left: auto;
      font-size: small;
    }
    .mastodon-comment .content {
      margin: 15px 20px;
    }
    .mastodon-comment .attachments {
      margin: 0px 10px;
    }
    .mastodon-comment .attachments > * {
      margin: 0px 10px;
    }
    .mastodon-comment .content p:first-child {
      margin-top:0;
      margin-bottom:0;
    }
    .mastodon-comment .status > div {
      display: inline-block;
      margin-right: 15px;
    }
    .mastodon-comment .status a {
      color: #5d686f;
      text-decoration: none;
    }
    .mastodon-comment .status .replies.active a {
      color: #003eaa;
    }
    .mastodon-comment .status .reblogs.active a {
      color: #8c8dff;
    }
    .mastodon-comment .status .favourites.active a {
      color: #ca8f04;
    }
Como usar
Agora, os posts e páginas só precisam ter a propriedade comments = true para habilitar o bloco de comentários. Do contrário, nada do arquivo comments.html será renderizado.
E para apontar o bloco de comentários, basta preencher a propriedade fediverse com o id do seu post no Fediverso. Assim:
O truque aqui é que você precisa postar primeiro (para gerar um id) e então editar o post para adicionar o id na propriedade. Coisa rápida.
Então o link é adicionado automaticamente no final do post, bem como todas as respostas são renderizadas corretamente. Eba!
Resultado final
Se tudo der certo, fica assim:
Se não der certo e você precisar de ajuda, pode me chamar que eu tento te apoiar!
 
    
  

Comentários
Você pode usar sua conta no Fediverso (por exemplo Mastodon, e muitas outras redes) e responder a este post para comentar aqui.