O llms.txt é um arquivo Markdown servido em /llms.txt que resume o site pra modelos de linguagem e agentes: um título, uma frase do que é o projeto e listas de links pras páginas que importam, cada uma com uma nota curta. A proposta, de Jeremy Howard, define o formato em poucas linhas, e a expectativa descrita nela é que o agente leia ou pesquise o arquivo pra achar o que precisa e depois siga os links relevantes.
Este post é só prático: escrever o arquivo, gerar a partir de uma lista curada, validar com um script e publicar num site Next.js, incluindo a novidade da v2. Estratégia de visibilidade em IA fica no pilar de GEO.
Entenda o formato em cinco regras
Pela especificação, nesta ordem:
- Um H1 com o nome do site ou projeto. É a única parte obrigatória.
- Um blockquote com um resumo curto, com as informações-chave pra entender o resto.
- Zero ou mais blocos de texto de qualquer tipo, menos títulos, com detalhes.
- Zero ou mais seções H2 com "listas de arquivos".
- Cada item da lista é um link Markdown obrigatório,
[nome](url), opcionalmente seguido de:e uma nota.
# Nome do site
> Uma ou duas frases dizendo o que é, pra quem e o que tem aqui.
Detalhes que ajudam a ler o resto. Sem títulos.
## Seção
- [Página](https://exemplo.com/pagina): nota curta sobre o que tem nelaDuas mudanças da v2, de agosto de 2026, que tutoriais antigos ainda não refletem:
- A seção
Optionalperdeu o significado especial. Na v1, ela dizia pra ferramentallms_txt2ctxo que cortar ao montar o contexto. A ferramenta saiu da proposta. A seção continua permitida, como convenção pra links secundários. - Agora existe descoberta. Uma página pode apontar pro llms.txt que a descreve com
rel="describedby", e pra sua versão em Markdown comrel="alternate" type="text/markdown", via<link>no HTML ou header HTTPLink:.
Liste as páginas num arquivo curado
O llms.txt é uma seleção, e um arquivo que lista tudo não ajuda o agente a escolher nada. Comece por uma lista curta, num YAML que alguém revisa:
site: Capiva
resumo: >-
Consultoria boutique de IA (Brasil, EUA e Reino Unido). Combina estratégia de
negócio com execução técnica: diagnóstico, prova de conceito com dados reais e
harness engineering para agentes em produção.
detalhes: |
- Conteúdo em português em /pt/ e em inglês em /en/, com os mesmos slugs.
- Para dúvidas sobre preço e escopo, use as páginas de serviço; o blog é educativo.
secoes:
Serviços:
- titulo: Diagnóstico Estratégico
url: https://capiva.tech/pt/strategic-diagnosis
nota: investigação de 3 a 8 semanas que mapeia onde IA cria valor na operação
- titulo: Validação Técnica
url: https://capiva.tech/pt/technical-validation
nota: prova de conceito com dados reais antes do investimento maior
Guias:
- titulo: O que é um AI Harness
url: https://capiva.tech/pt/blog/o-que-e-ai-harness
nota: post pilar do cluster de harness engineering
- titulo: O que é GEO / SEO para IA
url: https://capiva.tech/pt/blog/o-que-e-geo-seo-para-ia
nota: post pilar do cluster de visibilidade em IA
Optional:
- titulo: Epidemia da Inutilidade Profissional
url: https://capiva.tech/pt/resources/professional-uselessness-epidemic
nota: ensaio interativo sobre IA e trabalhoA nota é o que o agente lê pra decidir se segue o link. "Saiba mais" não diz nada. "Investigação de 3 a 8 semanas que mapeia onde IA cria valor" diz.
Gere o arquivo
#!/usr/bin/env python3
"""Gera llms.txt a partir de um YAML curado.
Uso: python3 gerar_llms_txt.py paginas.yaml > public/llms.txt
"""
import sys
import yaml
def gerar(cfg: dict) -> str:
partes = [f"# {cfg['site']}", "", f"> {' '.join(cfg['resumo'].split())}", ""]
if cfg.get("detalhes"):
partes += [cfg["detalhes"].strip(), ""]
for secao, paginas in cfg["secoes"].items():
partes.append(f"## {secao}")
partes.append("")
for p in paginas:
linha = f"- [{p['titulo']}]({p['url']})"
if p.get("nota"):
linha += f": {p['nota']}"
partes.append(linha)
partes.append("")
return "\n".join(partes)
if __name__ == "__main__":
with open(sys.argv[1], encoding="utf-8") as f:
sys.stdout.write(gerar(yaml.safe_load(f)))pip install pyyaml
python3 gerar_llms_txt.py paginas.yaml > public/llms.txtSaída:
# Capiva
> Consultoria boutique de IA (Brasil, EUA e Reino Unido). Combina estratégia de negócio com execução técnica: diagnóstico, prova de conceito com dados reais e harness engineering para agentes em produção.
- Conteúdo em português em /pt/ e em inglês em /en/, com os mesmos slugs.
- Para dúvidas sobre preço e escopo, use as páginas de serviço; o blog é educativo.
## Serviços
- [Diagnóstico Estratégico](https://capiva.tech/pt/strategic-diagnosis): investigação de 3 a 8 semanas que mapeia onde IA cria valor na operação
- [Validação Técnica](https://capiva.tech/pt/technical-validation): prova de conceito com dados reais antes do investimento maior
## Guias
- [O que é um AI Harness](https://capiva.tech/pt/blog/o-que-e-ai-harness): post pilar do cluster de harness engineering
- [O que é GEO / SEO para IA](https://capiva.tech/pt/blog/o-que-e-geo-seo-para-ia): post pilar do cluster de visibilidade em IA
## Optional
- [Epidemia da Inutilidade Profissional](https://capiva.tech/pt/resources/professional-uselessness-epidemic): ensaio interativo sobre IA e trabalhoRepare na lista antes do primeiro H2. É permitida: a regra 3 só proíbe títulos ali. Já dentro de uma seção H2, todo item precisa ser link.
Valide antes de publicar
O formato é simples o bastante pra quebrar sem ninguém perceber: alguém adiciona um item sem link, uma página muda de URL, o separador vira travessão. Este validador checa a estrutura e, opcionalmente, o status HTTP de cada link (sem seguir redirecionamento, pra você ver o 301 em vez de mascarar):
#!/usr/bin/env python3
"""Valida um llms.txt contra o formato de llmstxt.org.
Uso: python3 validar_llms_txt.py ARQUIVO_OU_URL [--checar-links]
"""
import re
import sys
import urllib.request
ITEM = re.compile(r"^- \[(?P<nome>[^\]]+)\]\((?P<url>[^)\s]+)\)(?P<resto>.*)$")
def ler(origem: str) -> str:
if origem.startswith("http"):
req = urllib.request.Request(origem, headers={"User-Agent": "validar-llms-txt"})
with urllib.request.urlopen(req, timeout=15) as r:
return r.read().decode("utf-8-sig")
with open(origem, encoding="utf-8-sig") as f: # utf-8-sig: o BOM é permitido
return f.read()
def status_http(url: str, metodo: str = "HEAD") -> str:
class SemRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
abridor = urllib.request.build_opener(SemRedirect)
req = urllib.request.Request(
url, method=metodo, headers={"User-Agent": "validar-llms-txt"}
)
try:
with abridor.open(req, timeout=15) as r:
return str(r.status)
except urllib.error.HTTPError as e:
if e.code == 405 and metodo == "HEAD": # servidor que não aceita HEAD
return status_http(url, "GET")
return f"{e.code} -> {e.headers.get('Location', '')}".strip(" ->")
except Exception as e: # noqa: BLE001 (DNS, timeout, TLS)
return f"falhou ({e.__class__.__name__})"
def validar(
texto: str, checar_links: bool = False
) -> tuple[list[str], list[str], list[str]]:
erros, avisos, links = [], [], []
linhas = [li.rstrip() for li in texto.splitlines()]
conteudo = [li for li in linhas if li.strip()]
if not conteudo or not conteudo[0].startswith("# "):
erros.append(
"a primeira linha com conteúdo precisa ser um H1 ('# Nome do site')"
)
if sum(1 for li in linhas if li.startswith("# ")) > 1:
erros.append("há mais de um H1")
if len(conteudo) < 2 or not conteudo[1].startswith("> "):
avisos.append(
"sem blockquote de resumo logo após o H1 (opcional, mas recomendado)"
)
secao = None
for n, li in enumerate(linhas, 1):
if re.match(r"^#{3,} ", li):
avisos.append(f"linha {n}: o formato só usa H1 e H2 ({li[:40]!r})")
elif li.startswith("## "):
secao = li[3:].strip()
elif secao and li.startswith("- "):
m = ITEM.match(li)
if not m:
erros.append(f"linha {n}: item de lista sem link markdown [nome](url)")
continue
links.append(m["url"])
resto = m["resto"]
if resto and not resto.startswith(":"):
achei = resto[:3].strip()
avisos.append(f"linha {n}: notas vêm depois de ':' (achei {achei!r})")
tokens = len(texto) // 4
if tokens > 10_000:
avisos.append(
f"~{tokens} tokens (chars/4): o arquivo deveria caber folgado no contexto"
)
if checar_links:
for url in links:
s = status_http(url)
if s != "200":
avisos.append(f"link {url} respondeu {s}")
return erros, avisos, links
if __name__ == "__main__":
origem = sys.argv[1]
erros, avisos, links = validar(ler(origem), "--checar-links" in sys.argv)
for e in erros:
print("ERRO ", e)
for a in avisos:
print("AVISO", a)
print(f"{len(links)} links, {len(erros)} erros, {len(avisos)} avisos")
sys.exit(1 if erros else 0)$ python3 validar_llms_txt.py public/llms.txt --checar-links
5 links, 0 erros, 0 avisosO validador aceita URL também, então dá pra rodar contra produção. Sai com código 1 quando há erro, o que basta pra pôr no CI. O limite de 10 mil tokens (estimados por caracteres/4) é escolha minha, não da especificação; ajuste ao seu caso.
Publique no Next.js
Arquivo estático em public/ é servido na raiz. public/llms.txt vira https://seu-site/llms.txt, sem código.
Pra implementar a descoberta da v2, adicione o header Link nas páginas cobertas pelo arquivo:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Cada post aponta para o llms.txt que o descreve (link relation da v2 da proposta)
source: '/pt/blog/:slug',
headers: [{ key: 'Link', value: '</llms.txt>; rel="describedby"' }],
},
]
},
}
export default nextConfigTestei num app Next.js 16.4 com next build && next start:
$ curl -sI http://localhost:3124/pt/blog/x | grep -i link
Link: </llms.txt>; rel="describedby"
$ curl -sI http://localhost:3124/llms.txt | grep -i content-type
Content-Type: text/plain; charset=UTF-8text/plain é o que o Next.js serve pra .txt, e a especificação não pede outro tipo. A outra metade da v2, versões .md de cada página (pagina.md ou pagina.html.md), exige gerar Markdown do conteúdo. Fica pra quando o blog tiver essa saída.
Se o site tem áreas com donos diferentes, a v2 também define arquivos em subcaminho: um /docs/llms.txt cobre as páginas sob /docs/, e vale o arquivo mais específico.
Como usamos na Capiva
Rodei o validador contra o llms.txt da Capiva antes de escrever este post. Resultado: 35 links, 10 erros e 37 avisos. Os erros eram itens sem link dentro de seções H2 (Serviços, Fundador, Contato), escritos como texto em negrito. A maioria dos avisos era o separador: usávamos — em vez de : antes das notas. Dois links não respondiam 200: um apontava pra um domínio antigo que devolvia 404, e /en/blog redirecionava com 308. Corrigimos o arquivo e rodamos de novo com --checar-links: 34 links, 0 erros, 0 avisos. Em 12 de outubro de 2026, depois de consolidarmos quatro posts nos pilares, a mesma checagem deu 30 links, 0 erros e 0 avisos. Nenhum daqueles erros impedia um modelo de ler o arquivo. Eles mostram a deriva que acontece quando ninguém valida: um validador no CI acusa na hora, e quem olha o arquivo a olho não percebe.
O Lighthouse, aliás, tem uma auditoria de llms.txt: se o arquivo não existe (404), a auditoria fica como não aplicável; se o servidor dá erro ao buscá-lo, a página é sinalizada.
Checklist de troubleshooting
- [ ] O arquivo não abre em
/llms.txt. No Next.js, ele precisa estar empublic/, não emapp/. Confira se nenhum middleware ou rewrite captura a rota. - [ ] O validador acusa item sem link. Dentro de seção H2, todo item é
- [nome](url). Texto solto vai antes do primeiro H2. - [ ] Links redirecionando. Use a URL final no arquivo. Cada redirecionamento é uma requisição a mais pro agente.
- [ ] Acentos quebrados. Salve em UTF-8. O BOM é permitido pela especificação, e o validador aceita.
- [ ] O header
Linknão aparece. Osourcedoheaders()precisa casar com a rota exata; teste comcurl -Iem produção, porque CDN pode descartar headers. - [ ] Arquivo enorme. Se passou de alguns milhares de linhas, você fez um sitemap. Corte pras páginas que respondem às perguntas de quem procura você.
- [ ] O agente não acha a página nova. O llms.txt é estático. Gere no build a partir do YAML pra ele não ficar pra trás.
Perguntas frequentes
O llms.txt substitui o robots.txt ou o sitemap.xml?▼
Não. O robots.txt diz o que pode ser rastreado, o sitemap lista tudo pra indexação, e o llms.txt é uma seleção comentada pra leitura por modelos.
Preciso do llms-full.txt?▼
A especificação não define um llms-full.txt. Alguns sites publicam um com todo o conteúdo concatenado, como convenção própria.
Quem publica llms.txt?▼
A OpenAI e a Anthropic, pra documentação, por exemplo. Isso mostra adoção em sites de docs, não que buscadores usem o arquivo pra ranquear.
Garante que o ChatGPT vai citar meu site?▼
Não. O arquivo ajuda agentes a navegar seu conteúdo. Pra saber se você está sendo citado, meça.
Leia também
- O que é GEO / SEO para IA (o hub)
- Como aparecer no ChatGPT
- Como medir se o ChatGPT cita sua empresa
- Como escrever um CLAUDE.md que o agente segue, o mesmo problema pra agentes de código
- Como criar um servidor MCP em Python e configurar no Claude Code
Quer saber como seu site aparece pra IA, llms.txt incluso? Faça o Diagnóstico GEO gratuito.