================

== gmgall.net ==
================
É de compreender que sobretudo nos cansamos. Viver é não pensar.

Adicionando Comentários via Mastodon em Sites Estáticos

tech mastodon html hugo javascript

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. Ele não é, porém, o único que fez isso. Existe também quem faça uso de código no lado do servidor.

O cerne da solução é o seguinte método 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ão1 que chama uma função em JavaScript que acessa esse método e cria divs com os comentários abaixo de cada postagem.

O código

Criei um partial 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]*)$` "$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, as diferenças são as seguinte:

  • Na linha 1 garanto que vai ser incluído o código do partial apenas nas páginas que fornecerem um contexto com um link para um toot numa variável comments. Isso é feito graças à função with do Hugo.

  • Na linha 6 incluo um botão para copiar o link para o toot para a área de transferência. Isso facilita para quem deseja comentar. Não é necessário selecionar e copiar o link manualmente. Como a função with faz um rebinding do contexto, o link estará em {{ . }}.

  • Nas linhas 11 e 12 extraio o servidor e o id do toot do link para o toot com regex. Isso me permite construir a URL para o método GET /api/v1/statuses/:id/context da API do Mastodon na linha 17. Acho que essa é uma diferença interessante entre a minha solução e a do Joel. A minha só precisa que cada página tenha adicionado ao seu front matter uma variável comments com um link para um toot. A dele precisa de 3 variáveis diferentes.

  • Na linha 52 eu extraio o nome do servidor. O Joel não faz questão de mostrar o nome do servidor nos comentários do site dele.

  • Na linha 58 eu troco o formato da data para DD/MM/AAAA.

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 e em cada livro que eu postar impressões de leitura.

Vantagens

  • Simples de implementar.

  • Nada é executado no servidor, funciona para sites estáticos.

  • Basta a adição de uma variável no front matter para ativar os comentários.

Desvantagens

  • Se esse site se tornar acessado demais, ele pode começar a fazer mais requisições ao servidor da instância que uso do que ele suporta.

  • Só tenho o link para toot que anuncia o post depois de fazer o toot e preciso fornecê-lo antes do build da página. Isso quer dizer ter um toot público com um link para um post que só estará disponível depois de uns 3 minutos.

  • Migrações entre instâncias do Mastodon carregam os perfis seguidos e os seguidores, mas ainda não os toots. Se eu migrar de instância, os comentários ficam “presos” na instância anterior.

Possibilidades futuras

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. ↩︎

Comentários

Responda pelo fediverso

Responda pelo fediverso colando a URL abaixo no seu cliente:

https://bolha.us/@gmgall/110306021306905329

Carregar comentários