Commits
Tres fases desplegadas en un día: asistente, voz y secciones
Tres hitos en una tarde: el asistente de creación, el audio con comentarios
Hoy ha sido el día más intenso del proyecto: tres fases desplegadas en una sola tarde, con sus ajustes y más de un fix sobre la marcha. Lo apunto todo aquí porque mañana no me acordaré de ninguno.
Hito 1: el asistente "De la idea al publicado"
El primer despliegue ha sido el asistente unificado de creación en /admin/editor/. Tres orígenes posibles: búsqueda con Tavily, URLs concretas o contenido base mío. Revisión en línea con mejora SEO y generación de imagen destacada con el endpoint que ya tenía.
Sin cambios de esquema ni backend: solo frontend. npm run check con 0 errores, build correcto y despliegue en fxserver con docker compose up -d --build. La validación E2E por HTTP devolvió HTML 200 del wizard y el asset del editor con el texto esperado.
El primer bug llegó pronto: crear un documento con Keyword + Tavily acababa en 504 porque el POST síncrono tarda más que el proxy. La solución ha sido mover el flujo a un job corto con polling desde el asistente, sin mantener una petición abierta.
Hito 2: audio, comentarios y chat con IA
El segundo despliegue añade la capa de voz y comunidad: uso de embeddings configurable, ingesta de artículos publicados en el conocimiento, endpoint público /api/public/ask con citas, TTS hacia audio_url, comentarios públicos con moderación por IA y su panel. Migración de Alembic incluida para articles.audio_url y la tabla comments.
Verificación: python -m unittest con 13 tests en verde, /api/health y /api/articles con 200, y un artículo público cargado entero. /api/public/ask responde 400 explícito porque aún no hay uso de embeddings asignado en Ajustes → IA: la ruta funciona, falta configurarlo.
Aquí vino la serie de fixes más larga de la tarde. La voz de MiniMax rechazaba textos largos (text too long), así que ahora se parten en fragmentos de 4.000 caracteres y se concatenan los audios. También rechazó la voz Spanish_Female (voice id not exist): ahora hay una lista de voces de recambio y el backend prueba la siguiente si falla. Y si no hay proveedor configurado, los artículos públicos ofrecen "Escuchar el artículo" con la Web Speech API del navegador: coste cero en el servidor.
El chat público también se ha rediseñado: burbujas tipo mensajería, aviso visible de que hablas con una IA, markdown mínimo con escape de HTML (negrita, cursiva, código, enlaces) y limpieza de los bloques <think> y <thought> que algunos modelos dejan escapar. Además, chat y voz tienen configuración separada: uso chat propio y voice_id en los ajustes, sin tocar código.
Hito 3: secciones configurables
El tercer despliegue trae secciones: migración sections + articles.section_id, CRUD en Ajustes → Secciones, selector en el editor, filtro en la tabla de artículos, navegación pública dinámica y página /<seccion> que reutiliza el listado del blog. Tests en verde y validación E2E completa: crear sección, verla pública, borrarla y comprobar el 404.
Después, dos ajustes de UX desde los commits: "Artículos" queda como vista de blog sin sección, cada sección tiene su propio item en el menú del admin, y un par de fixes en el router (un import que faltaba y el listado para editores).
Hito 4, primera tanda
Para acabar, las bases de la fase de versiones: autoguardado con debounce de 10 segundos e indicador, tabla article_versions con instantáneas y restauración desde el editor, columna publish_at con consulta pública filtrando publish_at <= now(), selector de fecha en el editor, badge "Programado" y cola editorial real en el rail.
La segunda tanda añade roles editor/admin verificados en backend, gestión de usuarios, búsqueda global Cmd+K, copiloto en el editor y clonado traducido vía uso translation.
SEO que descubrí roto
Revisando la fase de revisión encontré que robots.txt apuntaba a un sitemap generado con localhost y que el sitemap estático incluía rutas de admin. Ahora /sitemap.xml se genera dinámico con home, blog, secciones públicas y artículos publicados; robots.txt apunta allí y bloquea /admin/, y SITE_URL usa el dominio público para canonical y sitemap. Cosas que solo ves cuando las miras.