Markdown para agentes: servir Accept: text/markdown
Um agente pede a tua página e recebe HTML. Depois deita fora quase tudo: o menu, o rodapé, o aviso de cookies, os scripts, os atributos de classe, os ícones SVG. Fica com alguns parágrafos de texto.
A versão que ele queria já a tens. Quase ninguém a serve.
O que custa a marcação
Compara o que envias com o que sobrevive:
curl -s https://exemplo.com/about | wc -c
curl -s -H "Accept: text/markdown" https://exemplo.com/about | wc -c
Se os dois números forem iguais, serves uma única representação a toda a gente e é o agente que faz a limpeza. Esse trabalho não é grátis. Tudo o que está dentro do <head>, cada <div> de embrulho, cada atributo data- e cada excerto de analítica entra na janela de contexto antes de alguma coisa decidir que era ruído. Numa página cujo conteúdo real é um título e oito parágrafos, a maior parte dos bytes é andaime.
O gémeo em Markdown mantém os níveis de título, as ligações, as listas, as tabelas e os blocos de código. Perde navegação, estilo e scripts, dos quais nada iria alguma vez ser citado.
Como funciona a negociação com Accept
O cliente diz o que consegue usar:
GET /about HTTP/1.1
Host: exemplo.com
Accept: text/markdown
O servidor escolhe uma representação para o mesmo URL:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Um navegador envia Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 e recebe HTML. Um cliente que peça text/markdown recebe Markdown. O URL canónico não muda, por isso continua a haver um só endereço para ligar e uma só página para partilhar.
Vary: Accept, ou como um navegador acaba a descarregar um ficheiro de texto
O Vary não é opcional e é justamente a parte que as pessoas saltam.
Cada cache entre ti e quem lê (o teu CDN, um proxy inverso, o armazenamento do próprio navegador) indexa as respostas por URL. Se serves dois corpos diferentes num mesmo URL sem avisar, a primeira resposta guardada vale para todos. Um agente pede /about com Accept: text/markdown, o teu CDN guarda o Markdown, e a próxima pessoa que abrir /about recebe uma página de asteriscos ou uma janela de descarga.
Vary: Accept
Essa linha diz à cache para incluir o cabeçalho Accept na sua chave, de modo que HTML e Markdown fiquem guardados em separado. Envia-a nas duas respostas, não só na do Markdown.
Tem um custo real: os cabeçalhos Accept variam muito entre clientes, por isso variar por eles fragmenta a cache. É um argumento honesto a favor da alternativa que se segue.
O gémeo .md
A opção mais simples é um segundo URL. /about serve HTML e /about.md serve o mesmo conteúdo em Markdown. Sem negociação, sem Vary, sem nada que possa correr mal numa cache. É um URL vulgar, portanto pode ser ligado, guardado nos favoritos, posto no sitemap e descarregado por qualquer cliente, incluindo um que não envie nenhum cabeçalho Accept aproveitável.
Depois diz que existe. No HTML:
<link rel="alternate" type="text/markdown" href="https://exemplo.com/about.md">
E nos cabeçalhos da resposta, para o que faz um HEAD e nunca analisa o corpo:
Link: <https://exemplo.com/about.md>; rel="alternate"; type="text/markdown"
Fazer as duas coisas custa uma linha cada e cobre os clientes que só olham para um sítio.
Verifica o teu
# 1. O mesmo URL responde de forma diferente?
curl -s -H "Accept: text/markdown" https://exemplo.com/about | head -20
# 2. A resposta HTML admite que o gémeo existe?
curl -sI https://exemplo.com/about | grep -i "vary\|link"
# 3. O URL .md funciona por si só?
curl -sI https://exemplo.com/about.md | grep -i "content-type\|vary"
O segundo comando é o que apanha a falha habitual. Um site pode ter um gémeo .md impecável que ninguém vai encontrar, porque nenhum cabeçalho e nenhuma etiqueta apontam para ele.
O que fazemos por cá
Todas as páginas deste site têm um gémeo em Markdown, e packages/web/src/lib/content-negotiation.js é tudo isso: cerca de noventa linhas.
Um mapa fixo lista os caminhos estáticos e os seus gémeos: / → /index.md, /about → /about.md, cada página de ferramenta e por aí fora. Os caminhos que levam um identificador ou um slug (os relatórios em /r/<uuid>, os guias, os artigos de blogue como este) são reconhecidos por padrão e recebem <caminho>.md.
A verificação do pedido é grosseira de propósito: se o cabeçalho Accept contiver a cadeia text/markdown, envia-se Markdown. Sem ordenação por valores de q. Um navegador nunca envia essa cadeia, por isso na prática separa bem os dois casos, mas um cliente que liste text/markdown com prioridade baixa recebe Markdown à mesma.
Cada resposta em Markdown sai com isto:
Content-Type: text/markdown; charset=utf-8
Content-Signal: search=yes, ai-input=yes, ai-train=no
Vary: Accept
X-Markdown-Tokens: 4213
Cache-Control: public, max-age=3600
O Content-Signal diz o que se pode fazer com o conteúdo, e não apenas se pode ser descarregado. O X-Markdown-Tokens é uma marca de tamanho: hoje leva a contagem de caracteres do corpo, não uma contagem real de tokens.
As respostas HTML de uma página com gémeo levam Vary: Accept e o cabeçalho Link, e o <head> recebe a etiqueta rel="alternate" tirada do mesmo mapa.
Este último pormenor mordeu-nos. O mapa alimentava o template, mas as rotas reconhecidas por padrão não estavam no mapa, por isso cada relatório de análise servia o seu gémeo em Markdown e anunciava-o no cabeçalho Link, enquanto o seu HTML nada dizia. O gémeo existia e era invisível para tudo o que lesse a marcação.
É uma convenção, não uma norma
Accept: text/markdown é negociação de conteúdo HTTP do costume, e isso é uma norma. Usá-la para Markdown é uma convenção que nasceu da proposta do llms.txt, e nenhum rastreador é obrigado a pedir. A posição honesta é que o gémeo .md é a parte que funciona de certeza, porque é um URL como outro qualquer e qualquer coisa o descarrega, enquanto a negociação custa um cabeçalho e compensa com os clientes que perguntam. Nenhuma das duas te vai fazer mal, e para saber qual está a ser usada lêem-se os registos, não os anúncios.