Munin: links internos para Hugo com IA

Neste post
No post sobre o Hugin contei como resolvi o problema de tags e resumos no meu blog Hugo. Mas tinha outro problema, menos óbvio e mais chato: links internos. Aqueles links que conectam um post a outro, que ajudam o leitor a navegar pelo conteúdo relacionado, e que o Google adora ver num site bem estruturado.
O problema é que ninguém linka nada. Você escreve um post sobre systemd timers, outro sobre cron, outro sobre launchd — e nenhum dos três menciona os outros. São ilhas de conteúdo que poderiam estar conectadas. A solução óbvia é reler cada post, lembrar quais outros existem, e ir inserindo links manualmente. Com 30 posts, é viável. Com 400, é insano.
Então construí o Munin.
O que é#
Munin é o irmão do Hugin — o segundo corvo de Odin, o da memória. Enquanto o Hugin pensa (gera tags e resumos), o Munin lembra (encontra conexões entre posts). Na prática, é outra TUI em Python que varre o mesmo diretório de posts, mas em vez de gerar metadados, descobre onde inserir links internos.
Como encontra posts relacionados#
Munin usa embeddings semânticos. Na primeira execução, baixa um modelo multilíngue (~400 MB, uma vez só) e gera um vetor para cada post baseado no título, tags e descrição. Esses vetores são guardados em cache e atualizados automaticamente quando um post muda.
Quando você seleciona um post, o Munin calcula a similaridade coseno contra todos os outros. Não é busca por palavras-chave — é compreensão semântica. Um post sobre “agendamento de tarefas no Linux” vai encontrar o post sobre “systemd timers” mesmo que as palavras sejam diferentes.
Tudo isso é local, sem LLM, sem gastar tokens. O cálculo é instantâneo.
Links de entrada e saída#
A interface mostra duas operações para cada post:
Incoming (i) mostra quais posts poderiam linkar para este. É a pergunta inversa: “quem no meu blog deveria estar apontando para cá?”. É útil para identificar oportunidades que você perdeu. Os resultados são links clicáveis que navegam direto para o post na lista.

Outgoing (o) é onde o LLM entra. O Munin pega os candidatos que os embeddings encontraram e manda para o modelo junto com o corpo completo do post. O prompt pede para encontrar trechos exatos no texto que serviriam como âncora natural para cada candidato.

Cada sugestão aparece com contexto — o texto ao redor do trecho que será linkado, com a âncora destacada em bold. Você sabe exatamente o que vai acontecer antes de aprovar.
Segurança do Markdown#
O Munin nunca insere links dentro de blocos de código, headings, code inline, imagens ou links que já existem. Antes de mostrar uma sugestão, verifica se o trecho está numa zona segura do Markdown. Se o parágrafo já tem um link, respeita o limite configurável por parágrafo.
Se o LLM sugerir um trecho que não existe verbatim no post — e isso acontece — o Munin tenta uma vez corrigir. Se não conseguir, descarta silenciosamente. Nada de links quebrados ou texto alterado.
Orçamento de links#
Nem todo post precisa de oito links internos. O Munin calcula um orçamento baseado no tamanho do post: um link a cada 300 palavras, com um teto de 8 por post. Posts muito curtos recebem um aviso no painel de metadados e nem oferecem a opção de buscar links.
Posts que já foram analisados sem resultado ficam marcados com um indicador visual que persiste entre sessões — para você não perder tempo tentando de novo.
Sugestão de novos posts#
Uma funcionalidade que surgiu quase por acidente: ao pressionar s, o Munin pede ao LLM para sugerir tópicos de novos posts baseados no conteúdo atual. Antes de mostrar a lista, verifica nos embeddings se algum desses tópicos já existe no blog. O resultado são lacunas reais de conteúdo — ideias para posts que complementariam o que você já tem.

Na prática#
munin ~/blog/content/posts
Na primeira vez, o modelo de embeddings é baixado e todos os posts são indexados. Nas execuções seguintes, só posts novos ou editados são reprocessados. A interface mostra a contagem de links de entrada e saída nos metadados de cada post, então você vê de relance quais estão bem conectados e quais são ilhas.
O fluxo típico: navega até um post, aperta o, revisa as sugestões com contexto, marca as que fazem sentido, aplica. O arquivo é salvo com escrita atômica (arquivo temporário + rename) para nunca corromper dados. O cache é atualizado automaticamente.
Compartilhado com o Hugin#
Munin mora no mesmo repositório e compartilha a infraestrutura: engines, seleção de modelo, configuração. Se você já configurou o Hugin, o Munin funciona sem setup adicional. A mesma tecla e abre o mesmo seletor de engine nos dois.
A configuração própria do Munin fica em ~/.hugin/munin.toml — limites de links, modelo de embeddings, campo de frontmatter para usar nas buscas. Os defaults funcionam bem para a maioria dos blogs.
Stack#
Python 3.11+, Textual para a TUI, sentence-transformers com backend ONNX para embeddings (evita o peso completo do PyTorch), httpx para as chamadas ao LLM, python-frontmatter para ler e escrever YAML. O cache de embeddings é um JSON por diretório.
Código#
Hugin e Munin são open source e vivem no mesmo repositório:
Repositório: github.com/janiosarmento/hugin
Sysadmin, 53 anos, brasileiro trabalhando de casa para o mundo todo. Cuida de servidores Linux, containers LXC, e de gatos que não saem de cima do teclado.