La arquitectura multiproducto de Blupoli era razonable mientras sólo tenía que explicar un producto. El repositorio ya separaba apps/web para la plataforma y apps/puzzles para Puzzles, Firebase Hosting tenía targets distintos, la Home consumía un registro canónico de productos y la navegación compartida estaba preparada para moverse entre superficies. Todo eso sonaba correcto. Pero una arquitectura no demuestra que puede crecer porque tenga carpetas con nombres genéricos. Lo demuestra cuando llega el segundo producto y obliga a reutilizar las fronteras sin introducir una colección de excepciones.
Ese segundo producto es Blupoli Cards. El trabajo quedó integrado en la PR #216, fusionada con el commit 817069e. Añadimos una aplicación independiente en apps/cards, un output propio en dist-cards, un tercer target de Firebase Hosting llamado cards que apunta al site blupoli-cards, cinco solitarios iniciales y los cambios necesarios para que blupoli.com reconozca Cards como producto real. La parte interesante no es la cantidad de archivos. Es qué cosas tuvimos que compartir y cuáles decidimos no compartir.
El problema real no era programar cinco solitarios
Podríamos haber empezado por las reglas de Klondike y no pensar en arquitectura hasta tener cinco tableros. Eso habría producido una demo, pero no habría respondido a la pregunta que nos interesaba: ¿cómo añade Blupoli un producto que comparte plataforma sin quedar absorbido por Puzzles? Las reglas de cartas eran sólo una parte del trabajo. Había que decidir dónde vive el código, cómo se construye, cómo se publica, cómo se descubre, cómo se enlaza desde la Home y qué responsabilidades siguen perteneciendo a la capa común.
El riesgo más evidente era aprovechar demasiado lo que ya existía. Puzzles tiene catálogo, shell, persistencia, i18n, analytics locales, engines, onboarding y una gran cantidad de infraestructura. Reutilizarlo todo habría reducido código inicial, pero habría convertido Cards en una categoría de Puzzles disfrazada de subdominio. El riesgo contrario era empezar un repositorio o stack completamente nuevo y duplicar marca, CI, deploy, metadata, navegación y decisiones de calidad. La solución tenía que vivir entre esos extremos.
Una app nueva dentro del mismo monorepo
La decisión principal fue crear apps/cards. Eso da a Cards una frontera de código igual de visible que apps/web y apps/puzzles. No necesita un repositorio independiente para ser un producto independiente. El monorepo sigue siendo útil porque la plataforma, los workflows y las piezas compartidas evolucionan juntas, pero la estructura evita que los conceptos específicos de cartas entren en los motores de Puzzles.
Esta decisión también conserva una propiedad que valoramos mucho: un cambio transversal puede revisarse como una sola unidad. La PR que introduce Cards puede modificar al mismo tiempo la nueva app, el registro de productos, la navegación, Firebase y los gates de build. Si Cards viviese en otro repositorio, el lanzamiento exigiría coordinar varias versiones y varios commits para conseguir un estado coherente. El monorepo reduce ese problema sin obligarnos a mezclar los dominios.
Web-first y sin añadir un framework sólo porque era un producto nuevo
Cards se implementó con HTML, CSS y JavaScript dentro de la arquitectura web existente. No añadimos React, Flutter, KMP ni otra capa sólo para marcar una separación tecnológica. La independencia del producto viene de sus límites de app, build, dominio y hosting, no de utilizar una herramienta distinta. Añadir un framework nuevo habría creado otro problema de dependencias, componentes y despliegue antes de saber qué necesita realmente la colección de cartas.
Esto no significa que Cards esté condenado a una estructura mínima. Significa que su primera versión puede evolucionar desde requisitos observables. Si más adelante una interacción, un sistema de estado o una expansión de catálogo justifica una librería concreta, podremos evaluarla con evidencia. Por ahora, la misma tecnología básica que nos permite mover rápido en la web es suficiente para comprobar el producto y mantener el coste conceptual bajo.
Un único runtime de Cards, cinco modelos de partida
La primera versión utiliza apps/cards/cards.js como runtime compartido. Ahí viven utilidades comunes como creación y barajado de mazos, representación de cartas, renderizado básico, contador de movimientos, persistencia local y dispatch de interacciones. Encima de esa base, cada juego mantiene su propio estado y sus reglas.
Klondike necesita tableau, stock, waste y cuatro fundaciones. Spider trabaja con diez columnas, stock de filas y secuencias completadas. FreeCell añade cuatro celdas libres y ocho cascadas. Pyramid modela veintiocho posiciones y relaciones de cobertura. TriPeaks mantiene otra geometría y un mapa de bloqueos entre cartas. Compartir utilidades no elimina esas diferencias. El runtime funciona porque acepta que el estado específico siga siendo específico.
Esta es una versión deliberadamente compacta del patrón que ya conocemos en Puzzles: contratos y herramientas comunes alrededor de mecánicas distintas. No intentamos crear un “solitaire engine universal” declarativo antes de saber si merece la pena. Los cinco juegos comparten lo que hoy sabemos que comparten; el resto permanece explícito.
Persistencia local como contrato mínimo de continuidad
Cada juego guarda su estado bajo una clave blupoli-cards:<slug>. La reanudación no depende de cuentas ni de un backend. El objetivo de esta primera capa es demostrar continuidad: abandonar una partida y volver al mismo navegador no debe convertir siempre la sesión en un reparto nuevo.
El modelo es sencillo, pero tiene una consecuencia arquitectónica útil. El estado serializado obliga a cada juego a distinguir qué información define realmente una partida. Klondike no necesita guardar el DOM; necesita piles, cartas, selección y movimientos. Pyramid necesita las posiciones retiradas, stock y waste. TriPeaks necesita sus cartas eliminadas y el descarte. Guardar modelo en lugar de representación nos deja una base más sana para una futura sincronización, si llega, sin fingir que esa sincronización ya existe.
El build de Cards se convierte en una fase explícita
Crear apps/cards no era suficiente. El monorepo necesitaba producir un artefacto desplegable de forma determinista. Añadimos scripts/build-cards.mjs, que limpia y genera dist-cards, copia la Home, estilos, runtime y manifest, crea las cinco páginas de juego a partir de una plantilla y genera sitemap.xml y robots.txt.
La decisión importante es que Cards entra en el comando de build general. npm run build no considera terminado el repositorio si Puzzles y blupoli.com se han generado pero Cards no. Eso evita una categoría nueva de fallo: que un desarrollador ejecute la validación habitual, obtenga verde y descubra después que el tercer producto ni siquiera formaba parte del artefacto comprobado.
También añadimos un comando cards:build para trabajar de forma aislada cuando sólo necesitamos ese output. La relación es intencionada: build específico para velocidad local, build general como contrato de publicación.
SEO básico generado desde el principio
El builder crea rutas estáticas para la Home y para /games/klondike/, /games/spider/, /games/freecell/, /games/pyramid/ y /games/tripeaks/. Cada página tiene canonical sobre cards.blupoli.com, description, Open Graph y datos estructurados VideoGame. El sitemap contiene sólo esas URLs canónicas y robots apunta al sitemap.
No montamos un sistema SEO paralelo complejo. El producto necesita un mínimo verificable antes de empezar a acumular contenido. Esa base permite que el artículo de lanzamiento del Blog enlace a páginas estables y que Search Console pueda descubrirlas cuando el dominio esté listo. También reduce el riesgo de que una futura migración tenga que corregir seis URLs públicas que nacieron sin una convención clara.
Firebase: un tercer target, no un segundo proyecto
El proyecto Firebase sigue siendo blupoli. La separación ocurre en Hosting. .firebaserc ya mapeaba los targets web y puzzles; Cards añade cards → blupoli-cards. En firebase.json, el nuevo target publica dist-cards con su propia configuración de headers, caché y seguridad.
Esta decisión mantiene una unidad operativa sin hacer que los tres sitios compartan output. Cada superficie se puede desplegar de forma independiente, recibir un dominio personalizado distinto y conservar una política de rutas adecuada. El dominio público es cards.blupoli.com, mientras que Firebase proporciona también el dominio automático blupoli-cards.web.app.
La verificación DNS y la emisión del certificado son una preocupación externa al código. El repositorio puede estar completamente listo antes de que Firebase termine de reconocer el subdominio. Esa separación fue útil durante el lanzamiento: no había razón para bloquear la implementación de build, SEO o juegos mientras el proveedor terminaba el proceso de dominio.
La asociación PWA nos obligó a revisar un detalle de Hosting
Blupoli ya utiliza una asociación cross-origin para que la aplicación principal pueda reconocer el origen de Puzzles. Cards añadió el mismo concepto mediante /.well-known/web-app-origin-association y una extensión de scope en el manifest de blupoli.com.
Durante la integración apareció un detalle pequeño pero importante: el target de Firebase no podía ignorar todos los dotfiles si esperábamos publicar .well-known. La primera configuración heredaba "**/.*" dentro de ignore, lo que habría dejado la asociación presente en dist-cards pero ausente en producción. Lo corregimos antes del merge y añadimos una validación para que esa contradicción no pueda volver silenciosamente.
Este tipo de fallo es exactamente la razón por la que preferimos gates sobre documentación. Una nota que diga “no ignores .well-known” depende de que alguien la recuerde. Un check que falla cuando aparece esa combinación convierte la decisión en contrato.
El registro de productos demuestra para qué existía
Cuando rediseñamos la Home de Blupoli, movimos la lista de productos a content/products.json. En ese momento sólo contenía Puzzles. Era fácil pensar que el archivo añadía una capa innecesaria para un único objeto. Cards justifica esa decisión: añadir el segundo producto significa añadir un registro con identidad, URL, estado, plataforma, clave de traducción y elementos de preview. La Home sigue siendo un renderer.
La consecuencia va más allá de evitar HTML duplicado. El registro obliga a que “ser un producto de Blupoli” tenga una forma explícita. Cards no aparece porque alguien copió una card visual. Aparece porque el sistema de productos ahora contiene una entidad publicada. Esa diferencia será más valiosa con un tercer o cuarto producto, cuando ya no queramos que cada nuevo lanzamiento requiera recordar manualmente todas las superficies donde debe existir.
La Home tuvo que dejar de hablar en singular
La PR #216 ya hacía que Cards apareciera en la sección de productos, pero el trabajo editorial posterior reveló otra capa: parte del copy seguía describiendo Puzzles como “el primer producto” y el hero todavía utilizaba Puzzles como único destino jugable destacado. Técnicamente Cards estaba integrado; semánticamente la Home seguía perteneciendo a una fase anterior.
Por eso la integración completa añade un CTA de Cards al hero, convierte el antiguo nodo “future” del ecosistema en un nodo real de Blupoli Cards, incorpora Cards a la navegación compartida y al footer, actualiza descripciones SEO y cambia los textos de plataforma para hablar de dos productos. Es una buena lección: las fuentes de verdad estructurales resuelven gran parte del crecimiento, pero las frases también contienen arquitectura. Un “primer producto” puede convertirse en deuda igual que una ruta hardcodeada.
La navegación compartida crece sin hacer que todos los productos sean iguales
renderBlupoliHeader ya acepta una lista de destinos. No necesitó aprender reglas específicas de Cards. El builder de la web añade un item con el destino https://cards.blupoli.com/ y copy localizado para los seis idiomas actuales de la plataforma. Puzzles conserva el CTA principal porque sigue siendo una superficie con navegación localizada propia; Cards entra como otro destino de producto.
El footer compartido recibe el mismo tratamiento. Además del enlace de navegación, la zona inferior puede mostrar ambos dominios de producto. Esto evita que una página editorial escrita hoy siga presentando sólo puzzles.blupoli.com después de haber publicado Cards. Como header y footer se renderizan desde componentes compartidos durante el build, la actualización se propaga por la Home, Blog y Devlog en lugar de copiar cambios en decenas de HTML.
La i18n de la Home tenía que reconocer el segundo producto
Cards V1 se publica en inglés, pero la Home de Blupoli existe en seis locales. El registro de productos utiliza translationKey, así que añadir Cards obliga a que EN, ES, IT, PT, FR y DE definan categoría, resumen y tres rasgos de producto. También actualizamos el copy general de la Home para que las descripciones no hablen de una plataforma con un solo producto.
Este punto ilustra otra ventaja de las claves semánticas que introdujimos en el rediseño anterior. No hay que localizar una card escrita dentro del template. products.cards.summary es una responsabilidad explícita de cada paquete. Si falta, el builder puede fallar en vez de publicar una mezcla de idiomas. Añadir productos reales es una prueba mucho mejor para un sistema de i18n que traducir una página que nunca cambia de estructura.
CI: Cards no obtiene un carril de calidad más débil
El workflow Quality and Firebase Preview se amplió para desplegar una preview del target Cards en las pull requests. La misma PR tiene que superar los tests, checks, build y E2E existentes antes de que consideremos el cambio listo. Después, las previews de blupoli.com, Puzzles y Cards prueban que los tres targets son aceptados por Firebase.
El primer intento de E2E de la rama terminó rojo por una prueba de Numberlink: el diálogo de onboarding interceptó el clic sobre “Play another”. Era una prueba existente de Puzzles y ninguno de los archivos de Cards tocaba ese motor. El build completo había pasado. Repetimos exactamente el mismo commit; la segunda ejecución completó las 119 pruebas E2E y las tres previews. No tratamos el rojo como “irrelevante” y fusionamos de todos modos; confirmamos que era el comportamiento intermitente conocido antes de continuar.
Este detalle no hace más heroico el lanzamiento, pero sí describe mejor el proceso. Una suite compartida puede fallar por otra parte del producto. El trabajo del gate no es culpar automáticamente al diff, sino impedir que el merge ocurra hasta que tengamos evidencia suficiente para entender el resultado.
Nuevos checks para que Cards no desaparezca del build
scripts/check-output.mjs ya revisaba Puzzles y blupoli.com. Cards pasa a ser un tercer target validado. El check exige que exista output, que el manifest tenga la identidad correcta, que robots anuncie el sitemap, que estén las cinco rutas de lanzamiento, que sus canonicals coincidan con cards.blupoli.com, que el sitemap incluya esas URLs y que las páginas expongan schema VideoGame.
También valida la configuración de Firebase y la asociación .well-known. Además añadimos tests de fuente que comprueban que cards.js parsea como JavaScript válido, que el builder declara los cinco slugs y que la Home enlaza a todos. No sustituyen pruebas de juego profundas, pero aseguran que un refactor accidental no pueda convertir Cards en un producto vacío sin que la pipeline lo detecte.
Qué compartimos hoy y qué seguimos manteniendo separado
Cards comparte repositorio, marca, registro de productos, navegación global, footer, CI, proyecto Firebase, filosofía SEO y parte de la integración PWA. Mantiene separados sus HTML, CSS, runtime, modelos de juego, build output, site de Hosting, dominio y sitemap. Esa lista es más útil que decir simplemente “monorepo”. Explica qué tipo de acoplamiento estamos aceptando.
La frontera puede cambiar con evidencia. Si Puzzles y Cards terminan necesitando el mismo módulo de estadísticas, quizás tenga sentido extraerlo a packages/. Si sus modelos de partida divergen demasiado, forzar una interfaz universal puede ser peor que dos implementaciones. La regla no es compartir por defecto ni aislar por defecto. Es compartir cuando la semántica coincide y mantener separado cuando sólo se parece la tecnología.
La primera implementación de juegos también tiene límites conscientes
Los cinco solitarios son jugables, pero la V1 no pretende competir en profundidad de opciones con aplicaciones especializadas que llevan años refinando cada variante. Klondike, Spider y FreeCell pueden ampliar movimientos de secuencias, opciones de reparto y ergonomía. Drag and drop, teclado, animaciones y ayudas más inteligentes también son áreas futuras. La prioridad del lanzamiento era demostrar producto, arquitectura y un ciclo completo de publicación.
Esto afecta a cómo interpretamos “reutilización”. Extraer un framework de cartas demasiado pronto habría cristalizado decisiones tomadas para cinco implementaciones iniciales. Preferimos dejar que la siguiente ronda de profundidad revele qué patrones son realmente comunes. Un componente compartido que aparece después de varias necesidades concretas suele ser más estable que uno diseñado anticipando un catálogo imaginario.
Blog y Devlog completan el circuito de producto
El lanzamiento no termina en código y Hosting. Cards incorpora enlaces desde su Home al artículo de lanzamiento y a este Devlog. El Blog explica qué puede jugar una persona y por qué hemos empezado con esas cinco familias. Este texto explica las fronteras de arquitectura y despliegue. A su vez, ambos enlazan a Cards y a sus páginas de juego.
Ese circuito mejora navegación y SEO, pero sobre todo evita que la documentación del producto viva únicamente dentro del repositorio. Una persona que descubre Cards puede entender qué es. Una persona interesada en el desarrollo puede reconstruir decisiones sin leer veintiún commits. Y el propio equipo gana una memoria editorial que complementa a Git: el diff conserva qué cambió; el Devlog conserva por qué esas fronteras eran importantes.
Qué nos enseñó añadir el segundo producto
La primera lección es que una buena arquitectura multiproducto se nota más en los pequeños cambios que no hacen falta. No tuvimos que mover Puzzles, crear un nuevo proyecto Firebase ni reescribir la Home. El registro aceptó otra entrada. El builder general aceptó otra fase. La navegación aceptó otro item. Firebase aceptó otro target. Esa extensibilidad era el objetivo.
La segunda lección es que los textos y los detalles de infraestructura también forman parte de la arquitectura. El nodo “future” del hero, el footer con un solo dominio y el ignore de .well-known eran supuestos pequeños que sólo se hicieron visibles cuando Cards llegó. El segundo producto funciona como una prueba de integración mucho más honesta que cualquier diagrama previo.
La tercera es que no necesitamos uniformidad tecnológica para tener coherencia, ni diversidad tecnológica para tener independencia. Cards puede usar HTML, CSS y JavaScript como el resto de la web y seguir siendo un dominio separado. La estructura de responsabilidades importa más que la novedad del stack.
Qué queda después del merge
El merge de la PR #216 deja la base publicada, pero no convierte Cards en un producto terminado para siempre. La siguiente fase debería profundizar los cinco juegos, mejorar interacción táctil, incorporar drag and drop cuando aporte valor, reforzar accesibilidad, decidir estadísticas útiles y estudiar más variantes. También queda trabajo de localización dentro de Cards si queremos que el producto alcance los mismos seis idiomas que la plataforma principal.
En paralelo, Search Console y el sitemap de Cards permitirán observar descubrimiento e indexación cuando el dominio acumule datos. No tiene sentido inventar una estrategia de contenido enorme sin ver primero qué páginas reciben impresiones y qué consultas aparecen. La base técnica está para que podamos medir y ajustar, no para declarar que el SEO está resuelto.
De una arquitectura preparada a una arquitectura utilizada
Cuando escribimos sobre rediseñar la Home de Blupoli como plataforma, el segundo producto era deliberadamente un espacio sin nombre. No queríamos inventar una aplicación para justificar la composición. Cards ocupa ahora ese lugar y nos permite revisar aquella decisión con evidencia. El registro de productos tenía sentido. La separación por apps tenía sentido. Los targets de Hosting podían crecer. La navegación compartida necesitaba un pequeño ajuste, no una reescritura.
Eso no demuestra que la arquitectura vaya a escalar indefinidamente. Ninguna PR puede probarlo. Sí demuestra algo más concreto: pasar de uno a dos productos no obligó a romper el modelo. Y esa es una señal mucho más útil que decir que el monorepo “está preparado para el futuro”. Hoy Blupoli tiene una plataforma, Puzzles y Cards como superficies distintas. La arquitectura ya no describe una posibilidad; está siendo usada.