Insights / Artigos técnicos
Renderizar links Markdown com segurança em respostas de chat com IA
Nota de implementação sobre converter links Markdown em respostas de IA para HTML seguro. Separar parsing tolerante a espaços, trim de href, allowlist, renderização DOM, fallback e testes torna o padrão reutilizável em outros sites.
Sumário
Quando um chat com IA responde Veja [Serviços]( /services/ ), o link pode não ser renderizado e o Markdown bruto pode permanecer na tela.
Acecore encontrou esse caso no chat de contato e ajustou o renderizador em o PR que corrigiu a renderização de links Markdown.
Este artigo usa essa correção pequena como ponto de partida para converter respostas de IA em DOM com segurança.
Respostas de IA não são HTML confiável
Trate a saída do modelo como texto.
Links, negrito e listas são úteis, mas inserir a resposta em innerHTML faz o navegador interpretar qualquer string gerada pelo modelo.
Não é necessário implementar Markdown completo. É melhor detectar só os recursos suportados pelo chat e criar apenas nós DOM seguros.
O problema não é só espaço
O bug direto era um link assim:
[Serviços](/services/)
Uma regex rígida costuma assumir que a URL não tem espaços:
/\[([^\]]+)\]\(([^)\s]+)\)/;
[^)\s]+ rejeita espaços, então ( /services/ ) não vira link. A correção é permitir espaços dentro dos parênteses e normalizar depois.
/\[([^\]]+)\]\(\s*([^)]+?)\s*\)/;
Depois disso, a validação continua obrigatória.
Faça trim antes de validar
O fluxo deve ser:
- Extrair label e raw href
- Aplicar
trim()no raw href - Validar o href com allowlist
- Criar
<a>apenas se for permitido
const href = String(rawHref || "").trim();
if (label && isSafeMarkdownHref(href)) {
const link = document.createElement("a");
link.href = href;
link.rel = "noopener noreferrer";
if (/^https?:\/\//i.test(href)) {
link.target = "_blank";
}
link.textContent = label;
parent.appendChild(link);
}
Valide o mesmo valor que será renderizado. Caso contrário, a checagem perde valor.
A allowlist depende do site
Cada produto deve decidir quais URLs a IA pode mostrar.
| Tipo | Exemplo | Decisão |
|---|---|---|
| Path interno | /services/ |
Permitir |
| Mesmo origin | https://acecore.net/... |
Permitir |
| LINE oficial | https://lin.ee/... |
Permitir quando for o canal oficial |
| mailto | mailto:info@acecore.net |
Só endereço fixo |
| tel | tel:05088902788 |
Só número fixo |
| Outros externos | Qualquer URL | Não linkar por padrão |
function isSafeMarkdownHref(href) {
if (href.startsWith("/")) return true;
try {
const url = new URL(href, window.location.origin);
if (url.origin === window.location.origin) return true;
if (url.hostname === "acecore.net") return true;
if (url.hostname === "lin.ee") return true;
} catch {
return false;
}
return href === "mailto:info@acecore.net" || href === "tel:05088902788";
}
Um site de recrutamento pode liberar portais de vaga; um SaaS pode liberar documentação e status page. A função deve refletir o produto.
Faça fallback para texto
Se um link falha na validação, normalmente é melhor preservar o Markdown como texto do que apagá-lo.
O usuário mantém contexto, e a equipe consegue ver o que o modelo tentou emitir. O renderizador deve criar links seguros e também falhar de forma segura.
Teste os casos errados
Inclua pelo menos:
| Entrada | Resultado esperado |
|---|---|
[Serviços](/services/) |
Link interno |
[Serviços]( /services/ ) |
Link interno após trim |
[LINE]( https://lin.ee/example ) |
Link externo permitido |
[Ruim](javascript:alert(1)) |
Não vira link |
[Externo](https://example.com/) |
Não vira link se o domínio não for permitido |
[Quebrado](/services/ |
Mostra como texto |
No PR #99, as variações com e sem espaços foram confirmadas como a mesma URL pretendida.
Não implemente todo Markdown por padrão
Para chat, geralmente basta:
- Parágrafos
- Listas
- Negrito
- Código inline
- Links
Tabelas, imagens, HTML bruto e notas de rodapé aumentam muito a responsabilidade. Mesmo com uma biblioteca, a política de HTML e links precisa ser definida separadamente.
Resumo
Renderizar links Markdown em respostas de IA parece um ajuste pequeno, mas define o limite de confiança na saída do modelo.
A regra prática é: texto primeiro, subconjunto pequeno, trim antes da validação, allowlist rígida e fallback seguro.
Fluxo de renderização de links
Text
Trate a resposta do modelo primeiro como texto puro.
Parse
Detecte apenas o Markdown que o chat realmente usa.
Validate
Faça trim do href e permita apenas URLs internas ou domínios aprovados.
Render
Crie elementos seguros com DOM API em vez de innerHTML.
Decisões que precisam ficar separadas
Renderização frouxa
- Colocar respostas de IA diretamente em innerHTML
- Tentar implementar Markdown completo de uma vez
- Falhar quando há espaços ao redor da URL
- Tratar URLs externas e javascript: da mesma forma
Renderização pequena e segura
- Receber respostas como texto e converter só o necessário em DOM
- Suportar apenas o subconjunto usado no chat
- Validar URLs depois de trim
- Manter URLs não permitidas como texto
Checklist de implementação
- Não confiar em respostas de IA como HTML
- Aceitar espaços ao redor de URLs Markdown
- Sempre fazer trim de href antes de validar
- Permitir apenas paths internos, origin atual e domínios necessários
- Definir target e rel em links externos
- Preservar links não permitidos como texto
- Testar URLs perigosas e Markdown quebrado
Páginas relacionadas
Perguntas frequentes
Usar markdown-it ou marked basta?
Mesmo com biblioteca, ainda é preciso decidir como tratar HTML, quais destinos de link permitir, como adicionar target e rel e como rejeitar URLs perigosas. Para chat, um renderizador pequeno pode bastar.
Permitir espaços ao redor da URL é perigoso?
O risco não está nos espaços. O ponto importante é validar o href depois do trim. Assim o renderizador tolera variações do modelo sem afrouxar a allowlist.
Links não permitidos devem ser removidos?
Normalmente deixá-los como texto ajuda a depurar e preserva contexto. Se a política exigir ocultar strings suspeitas, remover o link inteiro também é válido.