Inicio Noticias Inteligencia Artificial Gadgets Guías y Tutoriales Tutoriales IA Reviews ✍️ El autor 📬 Contacto

Las 7 cosas que casi rompen mi sitio web este mes (y cómo las arreglé)

397 artículos, 58 cron jobs, agentes de IA publicando solos y una Raspberry Pi 5 de 80€ como servidor. Suena bien hasta que algo se rompe y no hay nadie para arreglarlo excepto tú. Este mes me pasó 7 veces.

En resumen

  • Tengo un sitio web con 397 artículos gestionado por agentes de IA en una Raspberry Pi 5. Este mes casi lo pierdo 7 veces. Aquí están los bugs reales, las causas y las soluciones.
  • 397 artículos, 58 cron jobs, agentes de IA publicando solos y una Raspberry Pi 5 de 80€ como servidor. Suena bien hasta que algo se rompe y no hay nadie para arreglarlo excepto tú.
  • La causa: permisos de archivo. Los scripts de publicación se ejecutan como mi usuario (luigy), pero Caddy corre como el usuario caddy.
Terminal con errores en un servidor Raspberry Pi

El setup que suena a fantasía (y lo es, hasta que falla)

Tengo una Raspberry Pi 5 de 8GB — 80 euros — funcionando como servidor de producción 24/7 en mi casa. Sobre ella corren 58 cron jobs, un pipeline editorial autónomo que publica artículos sin intervención humana, 24 servicios systemd y un gestor de contenidos que genera HTML estático para 397 artículos.

Todo esto cuesta unos 14 euros al mes en APIs. Cero cloud. Cero DevOps. Cero humanos en el loop.

El problema de un sistema así no es montarlo. Es lo que pasa cuando se rompe a las 3 de la mañana y eres la única persona que sabe cómo funciona.

Julio de 2026 ha sido el mes en que más bugs de producción he tenido desde que lancé La Frontera IA en junio. No fueron fallos de la IA. Fueron errores de configuración, regex mal escritas, permisos de archivo ignorados y suposiciones erróneas sobre cómo funciona Cloudflare. Errores de sysadmin de los de verdad. De los que te hacen sudar frío.

Aquí van los 7 bugs que casi me cuestan el sitio. En orden de más catastrófico a más sutil.

1. El día que todos los links de mi web apuntaban a páginas .html.html

27 de julio de 2026. Me despierto, abro la web, hago clic en un artículo desde la sección de Guías. Error 404. Pienso que es un fallo puntual. Hago clic en otro. 404. Otro. 404.

Todas las páginas de sección — Guías, Noticias, IA, Gadgets, Tutoriales, Reviews — tenían los 397 links rotos. Cada enlace apuntaba a /articulos/raspberry-pi-5.html.html. Doble extensión. Cero páginas accesibles desde las secciones.

La causa: una regex de Caddy que llevaba mal desde junio pero solo se activó cuando las secciones empezaron a enlazar con .html explícito. La regla original era:

# ❌ Regex rota (junio-julio 2026)
path_regexp articulos ^/articulos/(.+)$
rewrite @articulos_ruta /data/articles/{re.articulos.1}.html

El grupo (.+) capturaba todo después de /articulos/, incluyendo el .html final. Caddy añadía otro .html. Resultado: slug.html/data/articles/slug.html.html. 404 en todas partes.

El fix: un grupo no capturante opcional. Tan simple como añadir (?:\.html)?:

# ✅ Fix (27 jul 2026)
path_regexp articulos ^/articulos/(.+?)(?:\.html)?$
rewrite @articulos_ruta /data/articles/{re.articulos.1}.html

El (?:\.html)? es opcional y no captura. El grupo 1 siempre es el slug limpio. Funcione con /articulos/slug o /articulos/slug.html, el rewrite produce exactamente /data/articles/slug.html.

Lección: las regex que capturan "todo" (.+) son bombas de tiempo. Siempre especifica qué NO quieres capturar. Y si tu servidor web funciona con rewrites, escribe tests para todas las variantes de URL que usan tus páginas.

2. Las 6 secciones estuvieron vacías 3 semanas y nadie se dio cuenta

Entre el 25 de junio y el 17 de julio de 2026, las páginas de sección de La Frontera IA estuvieron completamente vacías. Visitabas /guias/, /noticias/ o /tutoriales-ia/ y veías una página en blanco con el nav. Ni un solo artículo.

La causa: las secciones usaban JavaScript para cargar artículos dinámicamente:

// ❌ No funciona detrás de Cloudflare
fetch('/data/articles-data.json')
  .then(r => r.json())
  .then(data => { /* renderizar */ })

Cloudflare cacheaba el HTML de la sección antes de que el JavaScript se ejecutara. El resultado: página servida con DOM vacío. Sin contenido. Invisible para Google. Invisible para los usuarios.

Lo peor: me enteré por casualidad. Tres semanas con las secciones rotas. Cero tráfico desde Google a esas páginas en todo ese período.

El fix: HTML estático, sin una línea de JavaScript. El script regenerate-sections.py ahora precocina las 6 páginas de sección con 50 artículos cada una, hardcodeados. Google las indexa correctamente. Los usuarios las ven al instante.

Lección: el JavaScript del lado del cliente es invisible para crawlers y frágil con CDNs. Si tu contenido es estático, tu HTML también debería serlo.

3. La homepage se rompió porque las variables estaban vacías

Una mañana de julio abrí la homepage y vi esto: hero sin imagen, cards sin títulos, enlaces con href="". La portada de mi sitio era un cascarón vacío.

La causa: el JavaScript del index usaba template literals que interpolar las variables ${a.slug}, ${a.image}, ${a.title}. Pero el script se generó con los placeholders escapados como strings JavaScript normales, no como template literals reales:

// ❌ Variables escapadas, no interpoladas
`<a href=\"/articulos/${a.slug}.html\">`
// Cuando a.slug era undefined, quedaba:
// <a href=\"/articulos/.html\"> ← link roto

El modelo de IA que generó el JS no entendió la diferencia entre escapar comillas y preservar la interpolación. El resultado era HTML con href="" y src="/images/placeholder.jpg" en todas partes.

El fix: extraer las variables a variables locales antes del template literal. Si son undefined, se convierten en string vacío antes de llegar al HTML:

// ✅ Variables locales como red de seguridad
const aSlug = a.slug || '';
const aImg = a.imagen || a.image || '';
`<a href=\"/articulos/${aSlug}.html\">`

Además, el regenerate-index.py ahora genera HTML estático para SEO (indexado por Google) y deja el JS dinámico solo como fallback.

Lección: si generas HTML con IA, verifica que las URLs no estén vacías. Un simple grep -c 'href=""' index.html después de cada deploy te habría pillado esto en segundos.

4. Error 403 en todos los artículos nuevos (y Caddy no era el culpable)

Un día cualquiera de julio: publico un artículo, voy a la URL, y 403 Forbidden. El archivo existe. La ruta es correcta. Pero Caddy se niega a servirlo.

Paso 20 minutos debugueando el Caddyfile, las reglas @bad_bots, los rewrites. Nada.

La causa: permisos de archivo. Los scripts de publicación se ejecutan como mi usuario (luigy), pero Caddy corre como el usuario caddy. Si el archivo se crea con permisos restrictivos (600 o 640), Caddy no puede leerlo. Resultado: 403.

# ❌ El archivo se crea así
-rw-r----- 1 luigy luigy  8.2K jul 28 10:00 articulo.html
# Caddy (usuario 'caddy') → no tiene permiso de lectura → 403

# ✅ Fix inmediato
chmod 644 /var/www/lafronteraia/data/articles/*.html

La solución permanente fue triple: (1) un chmod 644 en el pipeline de publicación justo después de crear cada archivo, (2) un watchdog que corrige permisos cada 30 minutos, y (3) en el publish-finalize.py, cada archivo tocado recibe chmod 644 automáticamente.

Lección: en Linux, el 90% de los errores "misteriosos" son permisos. Si algo funciona en tu terminal pero no desde el navegador, revisa con qué usuario corre el servidor web. Caddy → caddy. Nginx → www-data. Apache → www-data.

5. Artículos con slug nulo: links a /articulos/null.html

Un poco surrealista: algunos artículos en el articles-data.json tenían "slug": null o "slug": "None". Los enlaces generados en la homepage y secciones apuntaban a /articulos/null.html o /articulos/None.html.

La causa: cuando el pipeline de publicación no lograba generar un slug a partir del título (título demasiado corto, caracteres especiales, o simplemente un bug en el script), el valor None de Python se colaba al JSON. La homepage y las secciones lo tomaban como un string literal.

El fix: dos cosas. Primero, un matcher de slugs que cruza los archivos reales en /data/articles/ con los títulos del JSON y reconstruye los slugs perdidos. Segundo, una verificación post-publicación:

# Detectar slugs nulos después de cada tanda
python3 -c "
import json
d = json.load(open('/var/www/lafronteraia/data/articles-data.json'))
bad = [a for a in d if not a.get('slug') or a.get('slug') in ('None','null')]
print(f'Slugs nulos: {len(bad)}')
if bad:
    for a in bad:
        print(f'  - {a.get(\"title\",\"SIN TITULO\")[:60]}')
"

Lección: None de Python no es null de JSON. Si serializas objetos Python a JSON, asegúrate de que las funciones que generan slugs nunca devuelvan None. Mejor: devuelve un string vacío y valida antes de serializar.

6. Google dice que mi web tiene "páginas alternativas con canónica adecuada"

Google Search Console empezó a reportar docenas de páginas marcadas como "Página alternativa con etiqueta canónica adecuada". La validación de indexación fallaba una y otra vez.

La causa: el servidor web servía el mismo contenido desde dos URLs diferentes para cada sección:

  • /ia/ y /ia/index.html → mismo contenido
  • /noticias/ y /noticias/index.html → mismo contenido
  • Lo mismo para /guias/, /gadgets/, /tutoriales-ia/, /reviews/

Google detectaba ambas URLs, veía que apuntaban a la misma canónica, y las marcaba como duplicadas. La canónica era técnicamente correcta, pero la duplicación seguía existiendo.

El fix: redirecciones 301 en Caddy:

# Caddyfile — dentro del site block
redir /index.html / 301
redir /ia/index.html /ia/ 301
redir /noticias/index.html /noticias/ 301
redir /guias/index.html /guias/ 301
redir /gadgets/index.html /gadgets/ 301
redir /tutoriales-ia/index.html /tutoriales-ia/ 301
redir /reviews/index.html /reviews/ 301

Con esto, /ia/index.html devuelve 301 → /ia/. Google solo ve una URL por sección. Problema resuelto en el siguiente rastreo.

Lección: canonical es necesario pero no suficiente. Si sirves el mismo contenido en dos URLs, Google igual lo marca como duplicado. La redirección 301 es la única forma de decirle "esta URL ya no existe, usa la otra".

7. Dos artículos distintos con la misma imagen (y casi nadie lo notó)

Este es el más sutil. SDXL, el modelo que uso para generar imágenes de los artículos, produce a veces la misma composición para prompts diferentes. Dos artículos de temas distintos — uno sobre monitores gaming y otro sobre fuentes de alimentación — terminaron con la misma imagen de portada.

Esto es un problema grave de cara a Google y a la experiencia de usuario. Si dos artículos comparten imagen, parecen duplicados aunque el contenido sea diferente.

La causa: prompts demasiado genéricos. "Tecnología futurista con luces azules y placa base" + "Hardware de alto rendimiento con LED azules" → misma imagen. SDXL tiende al mínimo común denominador cuando el prompt es vago.

El fix: dos capas. Primero, generate-image.py ahora verifica el hash MD5 de cada imagen generada contra todas las imágenes existentes. Si detecta duplicado, regenera con parámetros más agresivos:

# Si el MD5 coincide → regenerar con prompt modificado
new_prompt = f"{original_prompt} unique composition variant {slug}"
# Parámetros más extremos: steps=30, guidance=8.0

Segundo, un self-healing cada 30 minutos escanea /var/www/lafronteraia/images/ en busca de MD5 duplicados y regenera las imágenes afectadas.

Lección: si generas imágenes con IA, valida la unicidad por hash, no por nombre de archivo. Dos archivos con nombres diferentes pueden ser idénticos byte a byte.

Lo que nadie te dijo sobre autoalojar un sitio con agentes de IA

Cuando empecé con La Frontera IA en junio, la narrativa era: "Agentes de IA + Raspberry Pi 5 = sitio web autónomo por 14€ al mes". Y es verdad. El sitio funciona. Los artículos se publican. Google indexa.

Lo que nadie cuenta es la deuda de configuración. Los bugs que aparecen no son de IA. Son bugs de sysadmin clásico: regex mal escritas, permisos de archivo, redirecciones HTTP, hashes duplicados. Cosas que un ingeniero de infraestructura resolvería en minutos pero que un pipeline automatizado no detecta hasta que algo se rompe de forma visible.

Mi setup ahora incluye 10 self-healing guards que corrigen automáticamente los fallos más comunes. Pero cada guard existe porque ese fallo ya ocurrió al menos una vez.

Algunas lecciones universales que me llevo:

  • Las regex con .+ son peligrosas. Siempre especifica qué no capturar.
  • El JavaScript del lado del cliente es frágil. Si tu contenido no cambia en tiempo real, sirve HTML estático.
  • Los permisos de archivo en Linux importan. Tu usuario no es el mismo que el del servidor web.
  • None de Python ≠ null de JSON. Valida la salida de tus scripts antes de serializar.
  • Canonical sin 301 = duplicado para Google. Redirige /index.html a /.
  • Verifica la unicidad de imágenes por hash, no por nombre.

Y la más importante: revisa tu sitio en el navegador de vez en cuando. Los dashboards, los monitores y los cron jobs te dicen que todo está verde. Pero nada sustituye a abrir la web y hacer clic en tres enlaces aleatorios. Esa verificación manual me ha pillado más bugs que todos los health checks juntos.

✍️ Luigy García — Editor de La Frontera IA. Llevo desde junio de 2026 gestionando este sitio con agentes de IA desde una Raspberry Pi 5 en mi casa. Si tienes una experiencia similar con autoalojamiento o quieres compartir tus propios bugs de producción, escríbeme.