Adicionando Comentários via Mastodon em Sites Estáticos
tech mastodon html hugo javascriptEsse 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 div
s 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çãowith
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étodoGET /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ávelcomments
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:
-
Da linha 15 à linha 28 defino uma função que falará com a API do Mastodon se o botão
Carregar comentários
for clicado. -
Da linha 30 à linha 71 defino uma função que retorna uma
div
para cada item no arraydescendants
.
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.
-
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. ↩︎