Renderizar con seguridad enlaces Markdown en respuestas de chat con IA
Índice
Si un chat con IA responde Consulta [Servicios]( /services/ ), el enlace puede no renderizarse y el Markdown bruto puede quedar visible.
Acecore encontró este caso en el chat de contacto y ajustó el renderizador en el PR que corrigió el renderizado de enlaces Markdown.
Este artículo toma esa corrección pequeña como punto de partida para convertir respuestas de IA en DOM de forma segura.
Las respuestas de IA no son HTML confiable
La salida del modelo debe tratarse como texto.
En un chat son útiles los enlaces, negritas y listas, pero poner la respuesta en innerHTML permite que el navegador interprete cualquier cadena generada por el modelo.
No necesitas implementar Markdown completo. Necesitas detectar solo las expresiones que el chat soporta y crear nodos DOM seguros.
El problema no es solo el espacio
El bug concreto era un enlace como:
[Servicios](/services/)
Una expresión regular estricta suele asumir que la URL no contiene espacios:
;/\[([^\]]+)\]\(([^)\s]+)\)/
[^)\s]+ rechaza espacios, por lo que ( /services/ ) no se reconoce. La solución es tolerar espacios dentro del paréntesis y normalizar después.
;/\[([^\]]+)\]\(\s*([^)]+?)\s*\)/
Pero relajar el parser no basta. Después hay que validar el valor normalizado.
Recorta href antes de validar
El orden debe ser estable:
- Extraer label y raw href
- Ejecutar
trim()sobre raw href - Validar el href recortado con una allowlist
- Crear
<a>solo si está 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)
}
Valida exactamente el mismo valor que insertas en el DOM. Si no, el control pierde fuerza.
La allowlist depende del producto
Cada sitio decide qué URLs puede mostrar su IA.
| Tipo | Ejemplo | Decisión |
|---|---|---|
| Ruta interna | /services/ | Permitir |
| Mismo origin | https://acecore.net/... | Permitir |
| LINE oficial | https://lin.ee/... | Permitir si es el canal oficial |
| mailto | mailto:[email protected] | Solo dirección fija |
| tel | tel:05088902788 | Solo número fijo |
| Otros externos | Cualquier URL | No enlazar por defecto |
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:[email protected]' || href === 'tel:05088902788'
}
Un sitio de empleo puede permitir portales de reclutamiento; un SaaS puede permitir documentación y página de estado. La función debe ser propia de cada producto.
Fallback a texto para enlaces no permitidos
Cuando un enlace no pasa la validación, no siempre conviene borrarlo.
En un chat de contacto, dejar el Markdown como texto conserva contexto para el usuario y ayuda a depurar qué intentó emitir el modelo.
El renderizador debe saber crear enlaces seguros y también fallar de forma segura.
Pruebas mínimas
Prueba más que el caso feliz.
| Entrada | Resultado esperado |
|---|---|
[Servicios](/services/) | Enlace interno |
[Servicios]( /services/ ) | Enlace interno después de trim |
[LINE]( https://lin.ee/example ) | Enlace externo permitido |
[Malo](javascript:alert(1)) | No se enlaza |
[Externo](https://example.com/) | No se enlaza si el dominio no está permitido |
[Roto](/services/ | Se muestra como texto |
En PR #99 se confirmó que las variantes con y sin espacios producen la misma URL esperada.
No implementes todo Markdown por defecto
Para un chat suele bastar con:
- Párrafos
- Listas
- Negrita
- Código inline
- Enlaces
Tablas, imágenes, HTML crudo y notas al pie amplían demasiado la responsabilidad. Si usas una librería más adelante, la política de HTML y URLs sigue siendo una decisión separada.
Resumen
Renderizar enlaces Markdown en respuestas de IA parece un detalle de UI, pero define cuánto confías en la salida del modelo.
La pauta es simple: texto primero, subconjunto pequeño, trim antes de validar, allowlist estricta y fallback seguro.
Flujo de renderizado de enlaces
Text
Tratar la respuesta del modelo como texto plano.
Parse
Detectar solo el Markdown que el chat realmente soporta.
Validate
Recortar href y permitir solo URLs internas o dominios aprobados.
Render
Crear elementos seguros con DOM API, no con innerHTML.
Renderizado laxo
- Poner respuestas de IA directamente en innerHTML
- Intentar implementar todo Markdown de una vez
- Fallar cuando la URL tiene espacios alrededor
- Tratar URLs externas y javascript: como si fueran iguales
Renderizado pequeño y seguro
- Recibir respuestas como texto y convertir solo lo necesario en DOM
- Soportar solo el subconjunto usado por el chat
- Validar URLs después de trim
- Mantener URLs no permitidas como texto
- Completado: No confiar en respuestas de IA como HTML
- Completado: Aceptar espacios alrededor de URLs Markdown
- Completado: Hacer trim de href antes de validar
- Completado: Permitir solo rutas internas, origin actual y dominios necesarios
- Completado: Definir target y rel en enlaces externos
- Completado: Conservar enlaces no permitidos como texto
- Completado: Probar URLs peligrosas y Markdown roto
¿Basta con usar markdown-it o marked?
¿Permitir espacios alrededor de la URL es peligroso?
¿Hay que eliminar las URLs no permitidas?
Comentarios
Gui
CEO de Acecore. Lidera sistemas de negocio, web, bases de datos e infraestructura, calidad y adopción de IA desde la definición de problemas de negocio hasta el diseño, la puesta en marcha y la mejora posterior. Se apoya en capacidad práctica con C#/.NET y también cubre PHP/JavaScript, SQL Server/PostgreSQL/MySQL y Linux/Windows Server, diseñando requisitos, selección tecnológica, estándares de calidad y operaciones de desarrollo basadas en GitHub como un flujo coherente. Incorpora la IA generativa en procesos de desarrollo, verificación y organización de información, como una base práctica para que equipos pequeños entreguen más rápido y con mayor fiabilidad.
¿Quiere saber más sobre nuestros servicios?
Ofrecemos soporte integral en desarrollo de sistemas, diseño web, diseño gráfico y educación IT.
Artículos relacionados
Diseñar un sitio Astro + Cloudflare que crece función por función 7 de junio de 2026, 19:00
Cómo añadir comentarios a un blog Astro usando solo Cloudflare 7 de junio de 2026, 18:00
Guía de instalación de Sveltia CMS 7 de junio de 2026, 16:00