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.

Testa a tua negociação de Markdown · Gera o teu llms.txt