9.1 KiB
Contexto de Desarrollo y Arquitectura - Almeira Web
Este archivo sirve como memoria técnica e histórica para los desarrolladores y agentes de IA (como Antigravity) que continúen trabajando en este proyecto.
🛠️ Stack Tecnológico
- Core: Next.js 16 (App Router, con Server Actions habilitados).
- Base de Datos y Backend: Appwrite (Autenticación, Base de datos y Almacenamiento).
- Editor de Contenido: Tiptap Editor (Rich Text con soporte para imágenes, videos y embeds).
- Estilos: TailwindCSS.
- Empaquetado: Node standalone.
📂 Configuración del Entorno (.env.local)
Para levantar el proyecto en desarrollo local (npm run dev), se requiere un archivo .env.local en la raíz con el siguiente formato:
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://appwrite.almeira.com.co/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=69b84df5003bd8957c5c
APPWRITE_API_KEY=standard_cce3cd... (Ver Gitea o Consola Appwrite)
APPWRITE_DATABASE_ID=almeira
NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID=media_bucket
# Evolution API (WhatsApp)
EVOLUTION_API_URL=https://evolution.almeira.com.co
EVOLUTION_API_KEY=mgXxu2eA...
EVOLUTION_INSTANCE_NAME=AutomataAlmeira
# Server Config
SERVER_ACTIONS_BODY_SIZE_LIMIT=100mb
🎬 Solución del Reproductor de Video (Hito Clave)
Durante el desarrollo, se resolvió un problema crítico donde los videos subidos a las publicaciones no se reproducían en producción:
1. El Origen del Problema
Appwrite utiliza el endpoint /preview para imágenes y miniaturas. Sin embargo, para archivos de video, el navegador requiere soporte de reproducción por rangos (Range Requests) y streaming. El endpoint /preview limita la reproducción y bloquea el video en Safari y Chrome. La solución es usar el endpoint /view.
2. Implementación de la Corrección
- En el Editor (
RichTextEditor.tsx/MediaGridSelector.tsx): Se configuró la biblioteca de medios y el nodo de video de Tiptap para reemplazar automáticamente/preview?por/view?en las URLs de origen de los videos. - En el Servidor (
entities.ts): Se actualizó el validadorsafeHtmlSchemaque limpia el código HTML conDOMPurifypara permitir las etiquetas<video>y<source>, junto a los atributoscontrols,src,muted,loop,autoplay,preloadyposter. Además, normaliza cualquier URL de video errónea antes de guardarla en la base de datos. - En la Publicación (
[slug]/page.tsx): Se implementó una mejora dinámica que extrae el primer video del contenido y lo coloca como Hero Media en la cabecera (usando la imagen destacada comoposterde portada), eliminando el video redundante del cuerpo del texto. También normaliza URLs antiguas al vuelo.
🔄 Scripts de Despliegue y Compilación
package-hostinger.sh: Script Bash que compila el proyecto en modostandalone, copia los assets estáticos de.next/staticypublic/, inyecta la configuración para PM2 (ecosystem.config.js), configura elpackage.jsonpara que Hostinger corra directamenteserver.jsy comprime todo en un archivo zip listo para subir:almeira-web-deploy.zip.
Comando para empaquetar:
PATH="/home/carlos/.nvm/versions/node/v22.14.0/bin:$PATH" bash package-hostinger.sh
💡 Instrucciones para Agentes de IA (Continuar Trabajo)
Si eres un asistente de codificación de IA (como Antigravity), ten en cuenta lo siguiente al iniciar:
- Este proyecto utiliza Next.js standalone. Cualquier cambio en las rutas o componentes debe comprobarse compilando el proyecto localmente.
- La sanitización de HTML es muy estricta. Si necesitas añadir nuevos tags interactivos a las publicaciones, debes agregarlos al whitelist de
safeHtmlSchemaensrc/app/actions/entities.tsy al sanitizador ensrc/app/[slug]/page.tsx.
🏛️ Historial de Hitos Técnicos y Errores Resueltos (Logs de Sesiones)
A lo largo del desarrollo de la plataforma, se han resuelto varios problemas críticos de arquitectura y renderizado. A continuación, se detallan para evitar regresiones:
1. Error de Serialización de Componentes Servidor a Cliente ("Only plain objects...")
- Problema: Al intentar pasar colecciones de documentos directamente desde las Server Actions o Server Components de Next.js a componentes cliente (como
NewsCardoGlobalSidebar), Next.js arrojaba el error: "Only plain objects can be passed to Client Components". Esto ocurría porque los SDKs de Appwrite y ciertas funciones devuelven instancias de clase complejas u objetos con métodos prototípicos no serializables. - Solución: Se implementó una política estricta de desestructuración y serialización profunda en los flujos de datos. En las Server Actions (por ejemplo, en
src/app/actions/entities.tsy al consultar datos enHome), se aplica un parseo profundoJSON.parse(JSON.stringify(data))o mapeo explícito de propiedades antes de retornar los datos al cliente.
2. Resolución de IDs de Media y Logotipo Corporativo Roto
- Problema: El logotipo corporativo y varios recursos multimedia en la interfaz fallaban en renderizarse de forma intermitente debido a inconsistencias al resolver los IDs de documentos de Appwrite frente a los IDs reales de los archivos en Storage.
- Solución: Se independizó la tabla de base de datos
mediade Appwrite de los IDs de archivo reales del Storage. Se ajustó el helpergetAppwriteImageUrlpara recibir elfile_idy elbucket_idcorrectos mapeados desde el documento de la colecciónmedia. También se implementó una detección robusta de tipos de archivo (mimetype) en las acciones del servidor al subir archivos para categorizar correctamente imágenes y videos.
3. Renderizado de Contenido Mixto (Texto Plano con HTML)
- Problema: Al publicar noticias, las etiquetas HTML guardadas por el editor enriquecido se mostraban en el frontend como texto plano legible (ej. se veía literalmente
<p>Texto</p>), o bien el texto plano sin estructura perdía los saltos de línea. - Solución: Se integró
isomorphic-dompurifyjunto con la funciónformatTextToHtmlen la vista de detalle. Esta función detecta si el contenido ya contiene etiquetas de estructura HTML. Si las tiene, respeta los elementos (sanitizándolos con un whitelist de seguridad XSS); si es texto plano, convierte inteligentemente los dobles saltos de línea en párrafos (<p>) y saltos individuales en retornos de carro (<br />), garantizando un diseño limpio.
4. Migración de Infraestructura a Coolify y Soporte Multi-dominio
- Problema: Caídas constantes del servidor en Hostinger debido a limitaciones de memoria y errores 503 en despliegues tradicionales de Node.js.
- Solución: Se migró el entorno de despliegue a Coolify, una plataforma de self-hosting que gestiona contenedores Docker. Se configuró el enrutamiento para soportar múltiples orígenes y dominios sin errores de cabeceras CORS en el backend, mejorando la disponibilidad global de la web de Almeira.
5. Automatización de Publicaciones vía WhatsApp con IA y Whitelist en Appwrite
- Arquitectura: Se integró Evolution API con Google Gemini (
gemini-3.6-flash) y Appwrite para procesar mensajes y fotos enviados por clientes o salas de prensa directamente en WhatsApp. - Almacenamiento en Base de Datos Appwrite: Los chats y clientes autorizados se registran y gestionan directamente en la base de datos de Appwrite (
whatsapp_messagescon estadowhitelist_active/whitelist_paused), permitiendo añadir, pausar o eliminar clientes en tiempo real desde el Dashboard sin necesidad de redesplegar o reiniciar el servidor. - Sincronización con Repositorio: Cada modificación realizada en Appwrite se sincroniza automáticamente como respaldo en
src/config/whatsapp-chats.json. - Resolución Dinámica de Nombres: Al recibir un evento en
/api/webhooks/whatsapp, el webhook resuelve el nombre exacto del grupo o remitente consultando Evolution API (con caché en memoria) y valida coincidencias en tiempo real contra los registros activos de Appwrite ensrc/lib/whatsapp-filter.ts. - Gestión Visual en Dashboard: En
/dashboard/settings, los administradores pueden añadir nuevos clientes por nombre exacto, pausar/activar chats, o sincronizar automáticamente los grupos detectados en Evolution API con persistencia inmediata en Appwrite. - Flujo Editorial y Biblioteca de Medios: Chat autorizado envía mensaje/multimedia -> Si se reciben múltiples imágenes (álbum o ráfaga), el webhook recopila las imágenes candidatas y Google Gemini analiza visualmente cada una para seleccionar la más nítida, profesional y representativa (
selected_image_indexyimage_selection_reason) -> La imagen destacada elegida se sube a Appwrite Storage (media_bucket) y se registra en la Biblioteca de Medios (media), vinculándola comoimagen_destacada_iden el post -> Se calculan las dimensiones y previsualizaciones exactas (640x360 px / 16:9 para Noticias; 640x640 px / 1:1 para Eventos y Clasificados) -> Se crea post en Appwrite en estadoborrador-> El bot responde al grupo solicitando aprobación e indicando qué foto fue seleccionada -> Administrador responde "Aprobar" -> El post pasa a estadopublicado.