Postado em 03.05.2023 11:43
Esse site agora possui um botão `Carregar comentários` abaixo das postagens cujos *links* anunciei no Mastodon. Ao clicar nele, as respostas ao *toot* são exibidas. Foi possível fazer isso apenas no lado do cliente com JavaScript acessando a API do Mastodon, então é uma solução para ter comentários em sites estáticos.
Essa ideia não é minha, me baseei fortemente [numa postagem de Joel Garcia](https://joelchrono12.xyz/blog/how-to-add-mastodon-comments-to-jekyll-blog/). Ele não é, porém, o [único que fez isso](https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/). Existe também [quem faça uso de código no lado do servidor](https://dusanmitrovic.xyz/blog/post/2022-05-18/Adding-support-for-comments-through-integration-with-Mastodon).
O cerne da solução é o [seguinte método](https://docs.joinmastodon.org/methods/statuses/#context) na API do Mastodon:
GET /api/v1/statuses/:id/context
A resposta será um JSON com 2 *arrays*, um chamado `ancestors` e outro chamado `descendants`. As respostas ao *toot* estarão em `descendants`. Para *toots* públicos, não é necessário autenticação. A ideia, portanto, é ter um botão[^1] que chama uma função em JavaScript que acessa esse método e cria `div`s com os comentários abaixo de cada postagem.
Criei um [partial](https://gohugo.io/templates/partials/) chamado `comments.html` que trata dos comentários. Listo o código abaixo:
{{ with .Params.comments }} <h3>Comentários</h3> <details> <summary>Responda pelo fediverso</summary> <p>Responda pelo fediverso colando a URL abaixo no seu cliente: <pre>{{ . }}</pre></p> <button onclick="navigator.clipboard.writeText('{{ . }}')">COPIAR URL</button> </details> <a id="load-comments">Carregar comentários</a> <div id="comments-list"></div> {{ $server := replaceRE `(https://.*?)/.*` "$1" . }} {{ $toot_id := replaceRE `https://.+?/([0-9]*)gemini - gemini.ctrl-c.club "$1" . }} <script src="{{ "js/purify.min.js" | relURL }}"></script> <script> document.getElementById('load-comments').addEventListener('click', async () => { document.getElementById('load-comments').remove() const response = await fetch('{{ $server }}/api/v1/statuses/{{ $toot_id }}/context') const data = await response.json() if (data.descendants && data.descendants.length > 0) { let descendants = data.descendants for (let descendant of descendants) { document.getElementById('comments-list').appendChild(DOMPurify.sanitize(createCommentEl(descendant), { 'RETURN_DOM_FRAGMENT': true })) } } else { document.getElementById('comments-list').innerHTML = '<p>⚠️ Sem comentários no fediverso. ⚠️</p>' } }) function createCommentEl(d) { let comment = document.createElement('div') comment.classList.add('comment') let commentHeader = document.createElement('div') commentHeader.classList.add('comment-header') let userAvatar = document.createElement('img') userAvatar.classList.add('avatar') userAvatar.setAttribute('height', 60 ) userAvatar.setAttribute('width', 60 ) userAvatar.setAttribute('src', d.account.avatar_static) let userLink = document.createElement('a') userLink.classList.add('user-link') userLink.setAttribute('href', d.account.url) for (let emoji of d.account.emojis) { d.account.display_name = d.account.display_name.replace( `:${emoji.shortcode}:`, `<img src="${emoji.static_url}" alt="${emoji.shortcode}" height="14px" width="14px" />` ) } let serverName = d.account.url.replace(/https?:\/\/(.+)\/@.+/, '$1') userLink.innerHTML = d.account.display_name + " (@" + d.account.username + "@" + serverName + ")" let commentDateTime = document.createElement('a') commentDateTime.classList.add('comment-date') commentDateTime.setAttribute('href', d.url) commentDateTime.innerHTML = d.created_at.substr(0, 10).replace(/([0-9]{4})-([0-9]{2})-([0-9]{2})/, '$3/$2/$1') commentHeader.appendChild(userAvatar) commentHeader.appendChild(userLink) commentHeader.appendChild(commentDateTime) let commentContent = document.createElement('p') commentContent.innerHTML = d.content comment.appendChild(commentHeader) comment.appendChild(commentContent) return comment } </script> {{ end }}
É bastante parecido com o [código do Joel](https://joelchrono12.xyz/blog/how-to-add-mastodon-comments-to-jekyll-blog/#main-function), as diferenças são as seguinte:
O resto é praticamente igual:
Com o *partial* definido, bastou chamá-lo nos *layouts* das páginas em que eu desejo ativar comentários. Eu quero nos [*posts* do blog](https://github.com/gmgall/gmgall.net/commit/2c0b89bd238e1c136b4086cb1bc5c88d482d161b#diff-5942e866d871d1df37aadd5f735f37d2708ea0c3ebc753ccd1cc38a71a68e19bR19) e em cada [livro que eu postar impressões de leitura](https://github.com/gmgall/gmgall.net/commit/2c0b89bd238e1c136b4086cb1bc5c88d482d161b#diff-48079b3f67152acaf42b23c1205c876a542c5a83e2be44e4548d4a847ab54cb5R34).
Posso ter um *workflow* do GitHub Actions que faz o *toot* anunciando os *posts* novos e insere os *links* para os *toots* no *front matter* para mim. Isso me pouparia o trabalho de fazer o *toot* manualmente e eliminaria o problema de ter um *toot* indicando um *post* novo enquanto o *build* do site ainda está terminando.
Não sei se vou nesse caminho. Deixaria de ser uma solução simples. O *workflow* precisaria se autenticar para fazer o *toot* por mim e seria aumentada a dependência do GitHub Actions.
[^1]: Seria possível chamar a função a cada carregamento de página, mas quero reduzir a quantidade de requisições à API da instância em que tenho conta.