Disponible para proyectos

ALEJANDRO GONZALEZ LOPEZ

Construyendo interfaces escalables.

Soy Frontend Lead / Product Engineer con foco en arquitecturas de interfaces de alto rendimiento y diseño de sistemas escalables.

  • ANGULAR
  • NESTJS
  • ASTRO
  • TYPESCRIPT
  • TAILWINDCSS

Sobre mí

Soy Frontend Developer / Product Engineer con varios años construyendo productos digitales: aplicaciones web en producción, MVPs y proyectos propios donde pruebo stack y enfoque.

Me especializo en arquitecturas de interfaces de alto rendimiento, diseño de sistemas escalables y la frontera entre ingeniería y producto. Este sitio es mi CV vivo: proyectos reales y una bitácora con los problemas que he resuelto en el camino.

Influencias y referentes

Alan Buscaglia (ConquerBlocks Academy - Gentleman Programming)

Arquitectura Frontend con Angular - Buenas Prácticas - Clean Architecture

Evolución hacia la lógica estructural, Clean Architecture y adopción de Angular con tipado estricto como framework principal. También me introdujo a Engram y a una metodología de trabajo con agentes de IA.

LinkedIn

Bienvenido Sáez (ConquerBlocks Academy)

Rigor Técnico - Semantica - SEO

Construcción de cimientos técnicos robustos y adopción de un enfoque pragmático ante retos complejos. También me introdujo a la semantica y a la importancia del SEO en el desarrollo de aplicaciones web.

LinkedIn

Elena Hernández (ConquerBlocks Academy)

Metodología Base - Lógica de Programación - Estructuras de control

Asentamiento de la lógica de programación y el rigor algorítmico utilizando Python. Más que sintaxis, esta etapa forjó una disciplina metodológica esencial para aprender a abstraer problemas complejos y estructurar el pensamiento antes de escribir código en un lenguaje de programación.

LinkedIn

Santiago Corocotta (Upgrade-hub Academy)

Inmersión práctica en el ecosistema JavaScript

Su mentoría en Upgrade-hub no solo me impulsó a desarrollar interfaces interactivas, sino que sentó las bases de mis aptitudes de organización de código y el uso profesional de Git para el control de versiones.

LinkedIn

IA en el flujo de trabajo

Contexto, memoria, tests y aplicación diaria

Mi integración de la IA comenzó con Spec-Driven Development: definir el qué antes del cómo me llevó a prompts atómicos y contexto acotado. Eso evolucionó hacia Engram para memoria por proyecto, un Cerebro Global sincronizado con Git (`brain-push` / `brain-pull`) y reglas Cursor por repo (`.cursor/rules/*.mdc`). En features críticas el criterio de éxito de la spec incluye tests: el agente lee `docs/TESTING.md`, genera specs siguiendo patrones del monorepo y ejecuta la suite antes de cerrar. Uso la IA como acelerador con reglas, no como sustituto de criterio.

  1. Spec-Driven Development

    Guío al agente con prompts tipo Tech Lead: una intención por mensaje, sin mezclar refactor, diseño y bugfix. La spec incluye objetivo, restricciones, archivos de referencia obligatorios (`.cursor/rules/project.mdc`, `DESIGN.md`) y criterio de éxito verificable (typecheck, lint, tests concretos).

    spec-driven
    • Cursor
    • Prompts
    • Specs
  2. Testing con el agente

    Automatizo la calidad en el mismo hilo de implementación. El agente investiga el árbol de archivos, aplica patrones de `docs/TESTING.md` (Vitest + Angular Testing Library en web, Jest + Supertest en API), crea helpers reutilizables (`*.testing.ts`) y ejecuta suites acotadas (`ng test --include`, `pnpm --filter api run test`) antes de dar la tarea por cerrada. GitHub Actions replica el gate en cada PR.

    agent-testing
    • Vitest
    • Jest
    • GitHub Actions
  3. Memoria de Proyecto

    Documento decisiones técnicas, ADRs ligeros y aprendizajes de la bitácora en Engram. Estrategias de testing, convenciones Angular o incidencias de deploy quedan en `~/.engram/engram.db` y se exportan con `engram sync` para que el agente retome contexto en la siguiente sesión.

  4. Cerebro Global

    Centralizo memorias de agente en un repo Git aparte del código del producto (`cerebro-global`). Scripts Bash en `~/.zshrc` (`brain-push`, `brain-pull`, `brain-status`) sincronizan chunks entre la BD local, el export del proyecto y GitHub. Misma memoria en otra máquina sin copiar notas a mano.

  5. Revisión y Control

    Mantengo el criterio de producto y la responsabilidad final del código que mergeo. Reviso diff, corro lint/typecheck/tests en local y valido que la solución encaja con la spec. Las reglas del repo marcan qué es obligatorio testear (auth, permisos, dinero, validaciones); el resto no se fuerza por inercia.

    diff-review
    • Code Review
    • CI

El objetivo no es escribir más rápido: es mantener contexto, trazabilidad, tests reproducibles y criterio técnico cuando el asistente cambia en cada sesión. Lo que el agente valida en local, CI lo confirma en cada PR.

Proyectos

  • Academia Digital Finca El Sarao

    En Desarrollo

    Plataforma EdTech para Academia Digital Finca El Sarao con cursos online, campus de alumnos, panel de administración, blog editorial y seguimiento de progreso por lección.

    Por qué nace

    La academia necesitaba matrículas, progreso de lecciones, pagos y vídeo bajo su propia identidad, sin acoplarse a un LMS genérico. La Academia Digital Finca El Sarao nace para centralizar el campus, el blog editorial y la administración en una plataforma a medida.

    Monorepo pnpm con api/ (NestJS 11 + Prisma 7) y web/ (Angular 21 zoneless). Frontend en Vercel (fincaelsarao.com, Root = web); API y PostgreSQL en Railway (api.fincaelsarao.com). Arquitectura cross-origin (sin proxy Vercel salvo rewrite de /sitemap.xml). Proveedores desacoplados (PaymentProvider, VideoProvider) con mocks en desarrollo.

    Fase I–II en curso: auth JWT, blog SEO (Fase A: SeoService, JSON-LD, sitemap dinámico), campus del alumno, panel admin con tema oscuro, toasts con @starting-style e iconografía vía AppIcon. Incidencia de deploy Vercel documentada en la bitácora (jul 2026: monorepo pnpm + Prisma + Root Directory).

    El problema de las vistas previas al compartir enlaces en WhatsApp y otras redes quedó documentado como caso práctico en la bitácora técnica.

    • Angular 21
    • NestJS 11
    • Prisma 7
    • PostgreSQL
    • Tailwind CSS v4
    • DaisyUI 5
  • CV interactivo y bitácora

    En Desarrollo

    CV interactivo y bitácora de ingeniería, portfolio vivo con design system dark-mode, grid Bento de proyectos y timeline de problemas técnicos resueltos con búsqueda difusa.

    Por qué nace

    Un CV en PDF difícilmente refleja las decisiones técnicas, la forma de trabajar y la evolución profesional que hay detrás de cada proyecto. Este espacio nace para reunir portfolio y bitácora en una experiencia viva, visual y consultable.

    Contenido validado por Zod en content.config.ts (colecciones projects y logbook). Cada proyecto expone origin («Por qué nace») y un title de producto; cada entrada de bitácora exige headline descriptivo y summary de una frase. El listado cerrado muestra fecha, tecnologías, titular, resumen y CTA textual dentro de <details>/<summary>. Anclas in-page: /#logbook-{id} (sin rutas por slug).

    Iconografía UI vía @lucide/astro y <Icon /> tipado (src/components/ui/); brand logos del Hero con simple-icons. La bitácora usa un Web Component <logbook-search> que serializa índices planos al cliente (incluye summary) y filtra con búsqueda híbrida: literal en DOM primero, Fuse.js ponderado después. Navegación con /#sección para funcionar desde /cv; grid Bento con align-items: start.

    Identidad visual definida en DESIGN.md: modo oscuro nativo, tipografía Inter + JetBrains Mono, acentos teal/violeta y grid Bento asimétrico sin box-shadows.

    • Astro 6
    • Tailwind CSS v4
    • Zod
    • Fuse.js
    • TypeScript
  • Plataforma GTVMOTOR

    En Desarrollo

    Plataforma integral para concesionarias de vehículos (compra, alquiler, lavado, tasación y panel administrativo), con SSR híbrido y despliegue en producción en AWS EC2.

    Por qué nace

    La gestión del inventario, las reservas y las operaciones se realizaba mediante hojas de cálculo y llamadas, provocando desajustes de stock, conflictos de agenda y pérdida de oportunidades. GTVMOTOR nace para centralizar todo el flujo operativo en una única plataforma.

    Multirepo (Master / Backend / Frontend) con API NestJS sobre runtime Bun y frontend Angular 100 % standalone: signals, interceptores funcionales, Lucide y Clean Architecture por features lazy-loaded.

    En producción: EC2 Ubuntu + Elastic IP, PostgreSQL 16 en Docker con volumen persistente, Nginx como reverse proxy y servidor estático híbrido (browser + SSR en :4000 vía systemd). Dominio canónico gtvmotor.es con redirecciones 301 y SSL Let’s Encrypt. Runbook operativo documentado en SPECIFICATION_PROD.md. Migración a Angular 21 en curso.

Bitácora

Aquí recopilo algunos de los fallos reales de los que he aprendido, a base de chocazos contra la pared, y que me recuerdan porque nunca voy a dejar de aprender dentro de este maravilloso mundo del software.

Cada caso muestra un titular descriptivo y un resumen del incidente. Con Ver diagnóstico se abre el detalle: contexto, error, investigación, solución y aprendizaje. También puedes usar el buscador.

  • WhatsApp no mostraba el logo: los metadatos de Angular no estaban en el HTML inicial

    Los scrapers sociales no ejecutaban el JavaScript de Angular; añadí un fallback Open Graph estático y pospuse las tarjetas específicas por ruta hasta SSR o prerenderizado. Ver diagnóstico → Ocultar diagnóstico ↑

    Tras completar la Fase A de SEO de Finca El Sarao, la aplicación Angular CSR ya estaba desplegada en Vercel. El siguiente objetivo era conseguir que los enlaces compartidos en WhatsApp y otras redes generasen una vista previa corporativa reconocible, con una imagen Open Graph de 1200 × 630 píxeles, el logo de la marca y metadatos coherentes, sin introducir todavía SSR ni prerenderizado.

    La imagen Open Graph por defecto seguía apuntando a hero-cover.jpg, una fotografía de paisaje sin identidad corporativa. Aunque Angular actualizaba los metadatos al navegar, los scrapers sociales recibían únicamente el HTML inicial y no ejecutaban JavaScript, por lo que nunca veían esos cambios. Además, definir un og:url o un canonical fijo en index.html habría atribuido incorrectamente la misma URL a todas las rutas profundas de la aplicación.

    Comparé los metadatos que mostraba el DOM después de arrancar Angular con los incluidos en la respuesta HTML original. El problema no era la disponibilidad de la imagen ni el funcionamiento del SeoService: Vercel entregaba el mismo index.html para todas las rutas y los scrapers sociales no ejecutaban el JavaScript encargado de modificarlo. También descarté construir la URL de la imagen con window.location.origin, porque el crawler necesita encontrar una URL absoluta antes de que la aplicación se ejecute.

    Creé una tarjeta Open Graph corporativa de 1200 × 630 píxeles y dejé en index.html los metadatos invariantes que debían estar disponibles sin JavaScript. Convertí DEFAULT_OG_IMAGE en una URL absoluta, mantuve SITE_LOGO como recurso independiente para los datos estructurados y centralicé los metadatos variables de cada vista mediante SeoService.setPageMeta(). Evité declarar valores globales engañosos para og:url y canonical. La implementación y sus ajustes quedaron integrados en los PR #60 y #61.

    En una aplicación CSR, lo que no está en el HTML inicial no existe para un scraper sin JavaScript. Una tarjeta social estática puede ser fiable; si cada URL necesita una vista previa propia, la solución no es añadir más JavaScript, sino generar HTML por ruta mediante SSR o prerenderizado.

  • Vercel bloqueaba el despliegue de Angular por errores de tipos de NestJS

    El pipeline de Vercel fallaba tras un ng build correcto porque typechequeaba NestJS/Prisma del monorepo; fijé Root Directory en web con Include outside enabled. Ver diagnóstico → Ocultar diagnóstico ↑

    Estaba cerrando el deploy de Finca El Sarao (monorepo pnpm: Angular 21 en web/, NestJS 11 + Prisma 7 en api/) tras la Fase A SEO. El frontend va a Vercel (fincaelsarao.com); la API y PostgreSQL corren en Railway (api.fincaelsarao.com). No hay proxy same-origin como en GTVMOTOR: la SPA llama cross-origin con cookies HttpOnly para refresh. El build local y ng build en Vercel pasaban; el pipeline fallaba en fases posteriores o al cambiar Root Directory.

    Cadena de fallos en CI de Vercel: (1) `Command prisma:generate not found` al ejecutar scripts desde cwd incorrecto. (2) Con Root Directory = `./`, `ng build` terminaba bien pero después aparecían decenas de ciclos `Using TypeScript 5.9.3` y `TS2305: Module @prisma/client has no exported member UserRole` en `api/src/...`. Prisma sí generaba el cliente en postinstall. (3) Tras mover Root a `web` y desactivar «Include files outside the root directory», el install falló con `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` en `/vercel` porque `cd ..` / `pnpm --dir ..` no veía el `package.json` raíz del monorepo.

    El log mostraba que el error TS2305 ocurría **después** de Application bundle generation complete, no durante vercel-build. Eso apunta a la fase de Serverless Functions de Vercel, no al script de build del repo. Con Root = ./, Vercel reserva la carpeta api/ para compilar NestJS como functions aunque el buildCommand solo ejecute Prisma + Angular. Con Root = web, ../api queda fuera del proyecto Vercel (solo reservaría web/api/, que no existe). Pero un monorepo pnpm necesita el lockfile y workspaces en la raíz: sin «Include outside» el sandbox solo monta web/ y --dir .. apunta a /vercel vacío. Documenté la cronología en docs/DEPLOY.md y contrasté con GTVMOTOR (Nginx same-origin) vs Finca (cross-origin + rewrite único de /sitemap.xml).

    Config validada en main (ee3c489): Root Directory = web, «Include files outside…» = **Enabled**, overrides del dashboard vacíos. web/vercel.json: installCommand: HUSKY=0 pnpm install --dir .., buildCommand: pnpm run vercel-build. web/package.json delega: vercel-buildpnpm --dir .. run vercel-build. Script raíz: pnpm --filter api exec prisma generate && pnpm --filter web run build (sin nest build). .npmrc: node-linker=hoisted + hoist de @prisma/client. api/package.json: postinstall: prisma generate por si Vercel aísla installs sobre api/. Build ~1 min, deploy OK en fincaelsarao.com.

    En monorepos en Vercel, Root Directory y «Include outside» son dos ejes independientes: el primero evita que carpetas hermanas (api/) se traten como serverless; el segundo permite que el install vea el package.json raíz. Un ng build verde no garantiza deploy verde si la plataforma compila más código después. Si Prisma generate pasa pero TS2305 aparece en api/, sospecha de Serverless Functions, no de schema. Para SPA + API en otro host, documentar cross-origin (CORS + withCredentials) y no asumir que la receta same-origin de un proyecto anterior aplica tal cual.

  • Fuse.js mostraba entradas sin resaltar: el filtro fuzzy no coincidía con el DOM

    El buscador devolvía coincidencias aproximadas sin poder resaltarlas en el DOM; prioricé la coincidencia literal y limité el resaltado fuzzy a los matches reales de Fuse. Ver diagnóstico → Ocultar diagnóstico ↑

    El buscador de la bitácora (<logbook-search>) ya filtraba con Fuse.js, pero al añadir resaltado literal en el DOM apareció un fallo de confianza: entradas visibles sin la palabra buscada marcada en ningún sitio. Quería que cada resultado fuera comprobable (solo mostrar lo que se puede resaltar, contador honesto, <details> abiertos con sentido), aunque eso vaya en contra de lo que Fuse hace por defecto: tolerar similitudes sin exigir el texto exacto.

    Con Fuse.js como único filtro (`threshold: 0.35`), búsquedas como `inje` abrían entradas sin ningún resaltado visible. Fuse devolvía similitudes difusas; el `<mark>` solo pintaba subcadenas literales del input. El contador decía «4 veces · 4 entradas» pero tres artículos no mostraban la palabra buscada. Al pasar a solo DOM literal se corrigió el falso positivo, pero se perdía la tolerancia a typos y la relevancia fuzzy que justificaba mantener Fuse en `package.json`.

    Se evaluaron tres caminos: (1) eliminar Fuse y quedarse con substring en el DOM, simple pero sin aproximaciones; (2) volver a fuzzy puro alineando resaltado con includeMatches de Fuse, sin mostrar el query literal cuando no existe; (3) híbrido: prioridad a coincidencia exacta en el DOM, fallback fuzzy solo si Fuse encuentra la entrada y el DOM puede resaltar los spans de result.matches. También hizo falta alinear índice y DOM: research y learning entraron al JSON, y cada sección larga del caso lleva un identificador en el HTML para que el resaltado fuzzy caiga en el bloque correcto (investigación, solución o aprendizaje), no en cualquier párrafo.

    Lo quiero todo, adapatación al modelo híbrido. En LogbookSearch.astro, cada entrada pasa primero por #highlightLiteral(query) (teal). Si hits === 0, se consulta Fuse con includeMatches: true y #highlightFuseMatches pinta en ámbar el fragmento que Fuse emparejó, no el texto del input. Las aproximadas llevan badge ~aprox en el summary. Si Fuse sugiere una entrada pero el DOM no puede marcar nada, se oculta. El contador desglosa exactas y aprox (2 exactas · 1 aprox). LogbookSection.astro serializa el índice completo; en LogbookEntry.astro, cada sección larga del caso lleva un identificador en el HTML para que el resaltado fuzzy caiga en el bloque correcto (investigación, solución o aprendizaje), no en cualquier párrafo.

    Filtro y resaltado deben compartir criterio o el usuario ve resultados fantasma. Un híbrido honesto: exacto primero (predecible), fuzzy como segunda capa con visual distinto (ámbar + ~aprox) y highlight de lo que Fuse encontró, no de lo que el usuario escribió. Mantener Fuse tiene sentido si la segunda capa está acoplada a matches; si no, es dependencia muerta. Para pocos artículos el literal basta; el híbrido escala mejor cuando crezca la bitácora o haya typos en errores de consola largos.

  • Angular lanzaba NG0200: SignalStore y Apollo crearon una dependencia circular

    El bootstrap fallaba por el ciclo AppStore → HostPending → Apollo → AppStore; rompí la inyección eager con Injector y resolución perezosa en la acción. Ver diagnóstico → Ocultar diagnóstico ↑

    En el front de Slowork, el sidebar del admin muestra un badge con el total de solicitudes pendientes (creadores vía REST y hosts vía GraphQL). Ese número vive en AppStore, un NgRx SignalStore global. Tras tocar la carga de contadores en el store, la aplicación dejó de arrancar en /login: antes de poder validar si el badge volvía a funcionar, el bootstrap ya fallaba con un error de dependencias.

    RuntimeError NG0200: dependencia circular detectada para `SignalStore`. Ruta: `_Auth` → `SignalStore` → `_HostPendingService` → `_Apollo` → `InjectionToken APOLLO_OPTIONS` → `SignalStore`. La app fallaba al cargar `/login` antes de que el sidebar pudiera pedir los contadores.

    Se trazó el ciclo completo en el grafo de DI. AppStore inyectaba HostPendingService en el bloque withMethods al **construir** el store. Ese servicio depende de Apollo. La factory de provideApollo (apollo.config.ts) a su vez hace inject(AppStore) para leer el token JWT y montar el authLink. Angular no puede instanciar dos tokens que se necesitan mutuamente durante la misma resolución. Se descartó mover la lógica a CollaborationReviewService (también inyecta AppStore). Como alternativa válida quedó usar HttpClient con POST GraphQL crudo, pero duplica la query ya definida en host-pending.queries.ts.

    Inyección perezosa con Injector. En withMethods solo se resuelven dependencias seguras (Router, HttpClient, Injector). HostPendingService se obtiene **dentro** de loadAdminPendingCounts() con injector.get(HostPendingService), cuando el store ya está creado. La llamada a Hosts sigue siendo getPendingHosts(1, 1) y se guarda pageInfo.totalItems; Creadores usa REST con HttpClient. Ambas peticiones van en forkJoin con catchError(() => of(0)) para no romper el badge si una API falla.

    Un store global no debe inyectar en su constructor servicios que dependan de infraestructura configurada con el propio store. El ciclo no aparece al ejecutar un método: aparece al **registrar** inject(HostPendingService) en withMethods. Regla práctica: estado central (token, usuario) ↔ cliente HTTP/GraphQL exige lazy resolution, un TokenService intermedio sin dependencias, o peticiones REST aisladas. En SignalStore, inject(Injector) + get() en la acción es el patrón mínimo para romper el ciclo sin duplicar queries.

  • La animación de salida del toast no se veía: Angular destruía el nodo antes del CSS

    El @for eliminaba el toast al instante y cortaba la transición; usé @starting-style en la entrada y animate.leave para retener el nodo hasta el fin del CSS. Ver diagnóstico → Ocultar diagnóstico ↑

    En el front de Slowork (Angular 21 zoneless) estaba puliendo las notificaciones toast del panel admin. Cada aviso se renderiza en un @for con temporizador de cierre: al expirar, Angular elimina el nodo al instante y la transición de salida no se llega a ver. El objetivo era animar entrada y salida con CSS nativo, sin añadir @angular/animations ni penalizar Lighthouse.

    Colisión de animaciones y destrucción prematura de nodos en el DOM. Al expirar el tiempo del Toast, Angular destruye el nodo instantáneamente por el flujo de control `@for`, impidiendo que las transiciones CSS de salida se ejecuten o provocando cortes abruptos en la interfaz gráfica.

    Análisis del ciclo de vida del DOM moderno y evaluación de alternativas. Cargar el paquete tradicional @angular/animations añade una penalización de rendimiento innecesaria en aplicaciones Zoneless y va en contra de la optimización del Core Web Vitals (Lighthouse 100/100). Históricamente se retenía el nodo con JS, pero los estándares web actuales permiten delegar la carga directamente al motor de renderizado del navegador.

    Implementación de una estrategia híbrida nativa. Para la entrada, se utiliza la propiedad de CSS moderno @starting-style para definir los estilos del elemento antes de su primer renderizado en el navegador (solucionando la animación desde display: none). Para la salida, se utiliza el hook de animación nativo del framework (animate.leave), deteniendo la destrucción física del nodo en el DOM hasta que la transición CSS declare su finalización.

    Menos JavaScript, más CSS nativo. Descargar la lógica de transiciones en las APIs modernas del navegador mantiene el hilo principal libre y la aplicación ultra fluida. La evolución de las herramientas modernas prioriza los estándares web sobre las abstracciones pesadas del framework. Además, se inmuniza el entorno de desarrollo actualizando las reglas globales de contexto (.cursor/rules / .mdc) para asegurar que todo el equipo y los agentes adopten esta solución nativa en futuros componentes interactivos (modales, menús, etc.), evitando deuda técnica.

  • Fuse.js no encontraba resultados porque estaba indexando el objeto equivocado

    JSON.stringify de CollectionEntry producía {}; serialicé objetos planos en el servidor antes de pasar el índice al Web Component. Ver diagnóstico → Ocultar diagnóstico ↑

    Estaba montando el buscador de la bitácora en mi CV (Astro 6). Necesitaba filtrar entradas del timeline en el cliente sin React ni re-render del HTML del servidor: un Web Component con Fuse.js y un índice JSON generado en build. El primer paso era hacer llegar los datos de getCollection('logbook') al navegador de forma fiable.

    Al transportar el resultado de `getCollection('logbook')` al cliente, el índice de búsqueda llegaba vacío. Los objetos `CollectionEntry` de Astro contienen metadatos internos (símbolos, referencias a `.render()`) que no son JSON-serializables. `JSON.stringify` sobre la colección completa producía `{}` o fallaba en silencio, y Fuse.js no encontraba coincidencias aunque las entradas estuvieran en el DOM.

    Se revisó la documentación de Astro sobre el boundary servidor→cliente en islas y scripts de componente. La serialización de props con client:* exige datos planos; el mismo criterio aplica al pasar datos vía data-* attributes en un Custom Element. Se evaluó @astrojs/react con client:load, pero la interacción solo filtra nodos del DOM, así que no justifica el peso extra del runtime. Se optó por un HTMLElement con Fuse.js y manipulación directa de [data-entry-id].

    En LogbookSection.astro se mapeó la colección a objetos planos antes de serializar: entries.map(e => ({ id: e.id, context: e.data.context, error: e.data.error, technologies: e.data.technologies, solution: e.data.solution })). El JSON viaja al cliente como data-index en <logbook-search>. El Web Component parsea el atributo en connectedCallback, inicializa Fuse.js y muestra u oculta los <li> del timeline con display: none, sin re-renderizar el HTML generado en el servidor.

    En Astro, cualquier dato que cruce el boundary servidor→cliente, ya sea como prop de client:* o como atributo data-*, debe ser JSON puro. Los CollectionEntry hay que destruirlos a plain objects en el servidor. Para interacciones puntuales (filtrar, toggles, contadores), un Web Component en el <script> del .astro suele ser suficiente y evita dependencias de UI innecesarias.

  • Producción mostraba cero vehículos porque la API seguía conectada a localhost

    El catálogo en prod salía vacío con la DB llena porque apiUrl apuntaba a localhost; unifiqué SPA y API bajo /api same-origin con Nginx. Ver diagnóstico → Ocultar diagnóstico ↑

    Estaba cerrando el primer deploy en producción de GTVMOTOR en AWS EC2: Angular servido por Nginx en el host, API NestJS y PostgreSQL 16 en Docker, dominios gtvmotor.es y gtvmotor.co.uk con SSL. La infra de proxy se montó el 12/12/2025; al día siguiente, con todo desplegado, la web cargaba pero el catálogo de coches aparecía vacío para cualquier visitante.

    Tras el deploy, la web cargaba pero el inventario mostraba 0 vehículos pese a tener datos en PostgreSQL. `environment.prod.ts` seguía apuntando a `http://localhost:3000/api`: en el navegador del usuario esa URL no existe. Además, con varios dominios (.es, .co.uk, www) un front que llamara a otro host habría forzado CORS, preflight y cookies cruzadas. La API en Docker estaba bien acotada (`127.0.0.1:3000`), pero el cliente no pasaba por el reverse proxy de Nginx.

    Se depuró por capas desde la EC2: curl -s http://127.0.0.1:3000/api devolvía JSON con datos, mientras https://gtvmotor.es mostraba el SPA vacío. Se revisó el Security Group (solo 80/443 públicos, 3000 cerrado), el vhost en /etc/nginx/sites-available/gtvmotor y la diferencia entre /api y /api/ en proxy_pass. Se contrastó el enfoque Docker (nginx-proxy.conf con upstreams) con la estrategia final documentada en SPECIFICATION_PROD.md: Nginx en el host, estáticos en /var/www/gtvmotor/, proxy de /api/ hacia el contenedor local. Para SSL inicial se usó nginx-proxy.conf.temp-no-ssl hasta que Certbot validó los dominios.

    Se unificó todo bajo same-origin. En environment.prod.ts: apiUrl: '/api' e imageUrl: '/api', de modo que el navegador siempre habla con gtvmotor.es y Nginx enruta internamente. Reglas de proxy: /api/127.0.0.1:3000/api/, /docs y /docs-json al backend, / con try_files $uri $uri/ /index.html para el SPA. Redirect explícito location = /api { return 301 /api/; }. Dominio canónico gtvmotor.es con 301 desde www y .co.uk. API expuesta solo en loopback (127.0.0.1:3000:3000 en docker-compose). Certbot en Nginx y HSTS en el vhost HTTPS.

    Un reverse proxy no es solo terminar SSL: es la fachada única que alinea SPA, API y documentación bajo un dominio. Si prod usa /api relativo, el front en dev debe tener un proxy equivalente o URLs coherentes; si no, funciona en un entorno y falla en otro. No exponer el puerto del backend: Nginx + binding localhost es defensa en profundidad. Los detalles de Nginx importan (barra final en proxy_pass, redirect /api/api/). Documentar la estrategia real en un runbook (SPECIFICATION_PROD.md) evita repetir la depuración en el siguiente deploy.