NextJS com WebMentions

  • Likes-
  • Mentions -

Webmention é uma Recomendação W3C atualizada pela última vez em 12 de Janeiro de 2017. No artigo, a W3C define webmentions como (em tradução livre):

(...) uma maneira simples de notificar qualquer URL quando esta é mencionada no seu website. Do ponto de vista do recebedor, é uma maneira de requisitar notificações quando outros sites o mencionam.

Resumindo, é uma maneira de permitir a um website saber se foi mencionado em algum lugar, por alguém, de alguma maneira. A especificação também descreve uma maneira de um website notificar outros os quais citou. Isso significa que seu wbesite é agora um canal de mídia social ativo, canalizando comunicação -possivelmente- de vários outros canais (twitter, instagram, mastodon, facebook, só pra citar alguns).

Os passos deste artigo

  1. Declarar um endpoint pra receber webmentions
  2. Converter intereações de mídias sociais em webmentions
  3. Receber webmentions no seu site/app
  4. Configurar saída de webmentions (outbound webmentions)

Pra nossa sorte (exceto pelo passo 3) existem serviços que tornam tudo isso muito simples. E, pra sua sorte, eu vou descrever aqui como eu implementei o passo 3 neste website (se você estiver lendo uma versão que não a original, pode dar uma conferida no post original ).

Este blog é escrito com NextJS e é pré-renderizado no servidor (Server-Side Pre-Rendered). No entanto, eu optei por fazer as requests de Webmention no client-side, portanto vai ser bastante fácil portar o código que vou mostrar nesse artigo pra qualquer outro app ReactJS (e com um pouquinho de refatoração, pra qualquer outra aplicação JS também).

1. Declarando o endpoint

Pra se ter um endpoint aceitando Webmentions, você pode ou escrever um script por conta prória a adicionar no seu próprio servidor, ou usar um serviço como Webmention.io, eu fiz a segunda opção.

Webmention.io é gratuito, pra registrar você só precisa confirmar propriedade do domínio o qual pretende registrar. O serviço oferece várias maneiras de se fazer isso, o jeito que eu achei mais simples (e o que eu fiz) foi adicionar um atributo rel="me" em um (ou mais) link de redes sociais no seu site. Eu logo adicionei em todos, meus links ficaram mais ou menos assim:

<a
href="https://twitter.com/atilafassina"
target="_blank"
rel="me noopener noreferrer"
>
@AtilaFassina
</a>

Nesse caso, também é necessário ter uma referência apontando de volta para o seu site no perfil do twitter. Feito isso, basta ir até Webmention.io e adicionar a sua URL.

Coletando mentions

Beleza, agora temos um endpoint aceitando Webmentions. A última parte desse passo é adicionar 2 tags <link> na <head> das suas páginas para que essas mentions possam ser coletadas.

<link rel="webmention" href="https://webmention.io/{user}/webmention" />
<link rel="pingback" href="https://webmention.io/{user}/xmlrpc" />

Lembre-se de substituir {user} pelo seu username cadastrado em Webmention.io.

2. Social Media Webmention

Ok. Estamos prontos que as webmentions venham. Único problema é: ninguém realmente usa Webmentions - Eu, você, Max Böck, Swyx, e é isso. Então agora precisamos converter todas interações de mídias sociais que nos interessam em webmentions.

Bridgy conecta todo conteúdo de social medias selecionadas e converte para webmentions para que possamos consumí-lo. Com um Single-Sign On podemos alinhar todos os nossos perfis sociais, um por um.

3. Pegando as Mentions

Chegou a nossa vez de carregar o piano agora. Serviços terceiros conseguem manipular e nos trazer os dados, no máximo, ainda nos resta decidir como usá-los e como mostrá-los corretamente. Se você está com pressa dá um pulo nesse Codesandox, copia e cola o que você precisa e volta aqui pro passo 4.

Pegando a contagem de mentions

Pra resumir as coisas, esse é o tipo da resposta:

type TMentionsCountResponse = {
count: number
type: {
like: number
mention: number
reply: number
repost: number
}
}

Quando batemos no endpoint de webmention.io, é isso que recebemos. No código que vou compartilhar com você logo abaixo, você vai notar que eu formatei um pouco essa resposta pra que encaixe melhor no que precisamos. O object que passamos pra nossa UI obedece a esse tipo:

type TMentionsCount = {
mentions: number
likes: number
total: number
}

O endpoint é https://webmention.io/api/count.json?target=${post_url}/, preste atenção na última /. A request não falha sem, nenhum erro ocorre, mas os dados também não chegam.

Tanto Swyx quanto Max Böck somam likes com reposts e mentions com replies, pro Twitter elas são análogas.

const getMentionsCount = async (postURL: string): TMentionsCount => {
const resp = await fetch(
`https://webmention.io/api/count.json?target=${postURL}/`
)
const { type, count } = await resp.json()
return {
likes: type.like + type.repost,
mentions: type.mention + type.reply,
total: count,
}
}

Pegar uma lista de mentions

Antes de irmos pra responsa do serviço, saiba que a resposta é paginada, o endpoint recebe 3 parâmetros na query:

  • page: o índice da página que estamos pedindo
  • per-page: quantas mentions queremos por página
  • target: a url para a qual estamos buscando as webmentions

Quando você bater em https://webmention.io/api/mentions, passando os parâmetros citados acima, uma resposta bem-sucedida será um objeto com apenas uma chave links que será um array de mentions de acordo com o tipo abaixo:

type TMention = {
source: string
verified: boolean
verified_date: string // date string
id: number
private: boolean
data: {
author: {
name: string
url: string
photo: string // url, hosted in webmention.io
}
url: string
name: string
content: string // encoded HTML
published: string // date string
published_ts: number // ms
}
activity: {
type: 'link' | 'reply' | 'repost' | 'like'
sentence: string // pure text, shortened
sentence_html: string // encoded html
}
target: string
}

Os dados acima são mais do que suficientes pra termos uma seção lista-de-comentários no nosso website. Em Typescript, a request deve ser algo como:

const getMentions = async (
page: string,
postsPerPage: number,
postURL: string
): { links: TWebMention[] } => {
const resp = await fetch(
`https://webmention.io/api/mentions?page=${page}&per-page=${postsPerPage}&target=${postURL}/`
)
const list = await resp.json()
return list.links
}

Hooking it all up in NextJS

Agora que já temos todos os dados, caso você não tenha um app em NextJS, já pode enlouquecer mexendo na sua UI que você quer montar e nos encontramos no passo 4 quando formos gerenciar outbound mentions .

Desde a versão 9.3.0, NextJS tem 3 métodos diferentes pra se buscar dados:

  1. getStaticProps: busca os dados durante o build
  2. getStaticPaths: especifica rotas dinâmicas precisam de dados durante pre-render
  3. getServerSideProps: busca os dados em cada requisição

Agora é a hora de decidir em qual momento você fará a primeira requisição. Você pode requisitar as mentions no servidor e renderizar no servidor já com elas, ou você pode renderizar no sevidor sem mentions e fazer a requisição no cliente. Eu optei por manter estas requisições client-side.

Caso você também opte por client-side, eu recomendo usar SWR. É um custom hook de autoria do time da Zeit, oferece caching, estados de erro e loading, e inclusive suporta React.Suspense.

Fazer o Contador

Olhando para a página desse post, você pode notar que existem 2 contadores: no topo, logo abaixo do título, e abaixo, logo acima da lista de webmentinos. Para conseguir isso, eu dividí minha webmentions em 2 componentes: <WebmentionsCounter /> e <Webmentions />. Graças a SWR, apesar de eu ter 2 instâncias do WebmentionsCounter, ambas aproveitam o mesmo cache e apenas uma request é feita.

const MentionsCounter = ({ postUrl }) => {
const { t } = useTranslation()
// Setting a default value for `data` because I don't want a loading state
// otherwise you could set: if(!data) return <div>loading...</div>
const { data = {}, error } = useSWR(postUrl, getMentionsCount)
if (error) {
return <ErrorMessage>{t('common:errorWebmentions')}</ErrorMessage>
}
// The default values cover the loading state
const { likes = '-', mentions = '-' } = data
return (
<MentionCounter>
<li>
<Heart title="Likes" />
<CounterData>{Number.isNaN(likes) ? 0 : likes}</CounterData>
</li>
<li>
<Comment title="Mentions" />{' '}
<CounterData>{Number.isNaN(mentions) ? 0 : mentions}</CounterData>
</li>
</MentionCounter>
)
}

Fique a vontade pra dar uma espiada no código:

Get the actual mentions

Agora que temos a contagem, é hora de pegar aquele caldo das mídias sociais!

No momento da publicação deste artigo, useSWRpages ainda não está documentada. Aliado a isso, o fato de que o endpoint de webmention.io não oferece informação da coleção na resposta (sem offset e sem quantidade total de páginas), eu simplesmente não consegui encontrar uma maneira de usar SWR aqui.

Portanto, minha implementação atual usa um estado pra guardar a informação da página atual, outro estado pra lidar com o array de mentions, e por fim, useEffect pra lidar com a request. O botão de “mais posts” é desabilitado uma vez que a requisição retorna um array vazio.

const Webmentions = ({ postUrl }) => {
const { t } = useTranslation()
const [page, setPage] = useState(0)
const [mentions, addMentions] = useState([])
useEffect(() => {
const fetchMentions = async () => {
const olderMentions = await getMentions(page, 50, postUrl)
addMentions((mentions) => [...mentions, ...olderMentions])
}
fetchMentions()
}, [page])
return (
<>
{mentions.map((mention, index) => (
<Mention key={mention.data.author.name + index}>
<AuthorAvatar src={mention.data.author.photo} lazy />
<MentionContent>
<MentionText
data={mention.data}
activity={mention.activity.type}
/>
</MentionContent>
</Mention>
))}
</MentionList>
{mentions.length > 0 && (
<MoreButton
type="button"
onClick={() => {
setPage(page + 1)
}}
>
{t('common:more')}
</MoreButton>
)}
</>
)
}

O código foi simplificado pra permitir foco no assunto do artigo. Mais uma vez, fique a vontade pra ver a implementação completa:

4. Gerenciando outbound mentions

Graças a Remy Sharp gerenciar outbound mentions do seu site pra outroas é bastante simples e você tem uma opção pra cada caso-de-uso que possível.

A maneira mais rápida e fácil é ir até Webmention.app, pegar um token da API, e configura um webhook. Agora, se você já tem um RSS fica especialmente mais fácil com um applet IFTT, ou ainda com um deploy hook.

Caso você prefira não adicionar mais um serviço terceiro pra essa feature, ele também disponibilizou um pacote CLI chamado wm que pode ser rodado como um postbuild script.

Considerações finais

Eu espero que este post tenha te ajudado a conhecer um pouco mais sobre Webmention (ou ainda sobre IndieWeb), e talvez até tenha te ajudado a adicionar essa feature no seu site ou app. Se o artigo foi útil pra você, por favor compartilhe na sua rede. Como um bônus, seu comentário vai aparecer logo abaixo!

Microformats support

Mas isso não é suficiente pra lidar com as mentions de saída. Pra que nossa mentions incluam mais informação do que apenas a URL do remetente, nós precisamos adicionar microformats na nossa informação.

Por sorte, não é difícil e sequer interfere com a nossa UI. Resumidamente, microformats são um tipo de notação baseado em classe para o nosso HTML, isso provém melhor semântica para cada dado marcado. No caso de um post, nós vamos usar 2 tipos de microformat:

  • h-entry: para informações sobre o post
  • h-card: para informações sobre o autor do post

Praticamente toda a informação necessária para o post normalmente pode ser encontrada no cabeçalho da página. Então, nosso <header> vai acabar algo parecido com:

<header class="h-entry">
<time datetime="2020-04-22T00:00:00.000Z" class="dt-published">
2020-04-22
</time>
<h1 class="p-name">
Webmentions com NextJS
</h1>
</header>

E é isso. Lembre-se: se você estiver escrito em JSX é necessário substituir class por className, dateTime é escrito em camelCase (dateTime), e você pode usar um método como new Date('2020-04-22').toISOString() pra ele.

Pra informação do autor não é tão diferente. Na maioria dos casos (como este aqui), a informação do autor pode ser encontrada abaixo do artigo. Então, o rodapé dos posts se parece com:

<footer class="h-card">
<span class="p-author">Atila Fassina</span>
<img
alt="Foto do autor: Atila Fassina"
class="u-photo"
src="/images/internal-avatar.jpg"
lazy
/>
</footer>

E é isso! De agora em diante quando você enviar uma mention a partir da sua página, ela carregará toda a informação para quem quer que a esteja recebendo.

Todos em inglês.

Agradecimento especial a Ademílson Tonato pela revisão!