Una marca puede parecer un problema de diseño hasta que tiene dos aplicaciones reales detrás. Entonces el logo deja de ser solo un SVG, el selector de idioma deja de ser solo un control y el tema oscuro deja de ser una preferencia local. Cada una de esas piezas empieza a cruzar límites de rutas, builds, dominios, almacenamiento, caché y código heredado. Eso es exactamente lo que nos ocurrió al consolidar Blupoli como plataforma y mantener Blupoli Puzzles como un producto con vida propia. Visualmente queríamos una sola familia. Técnicamente seguíamos teniendo superficies distintas, con historias distintas y con reglas de navegación que no coincidían en todos los detalles.

La tentación evidente era resolverlo desde arriba: escoger un framework común, mover todo a una aplicación nueva y declarar que la coherencia llegaría después de la migración. No fue el camino que tomamos. El repositorio ya contenía una web principal, una aplicación de puzzles, cientos de rutas generadas, contenido editorial y una cantidad importante de lógica de build. Reescribir ese conjunto para compartir una cabecera habría cambiado un problema concreto por una migración enorme. Preferimos tratar la incoherencia como lo que era: un problema de fronteras. Había que decidir qué pertenecía a Blupoli, qué pertenecía a cada producto y cómo hacer que las piezas comunes no se copiaran de nuevo en cuanto apareciera la siguiente necesidad.

El síntoma no era una cabecera distinta

Al principio los fallos podían verse como detalles aislados. Un logo tenía una geometría ligeramente diferente. El footer de Puzzles heredaba reglas CSS antiguas que podían ocultarlo en móvil. La web principal y el producto podían mostrar selectores de idioma que no se comportaban igual. Una página editorial podía recordar el tema oscuro mientras otra volvía al claro. Los favicons podían quedarse detrás de la identidad actual por una ruta o una política de caché. Ninguno de esos errores, por separado, justificaba una reescritura. Juntos sí señalaban algo más importante: no existía todavía una frontera suficientemente fuerte entre la UI de plataforma y la UI específica del producto.

Ese diagnóstico cambió el tipo de solución. Si corregíamos cada pantalla por separado, obtendríamos una colección de parches visualmente parecidos. Si extraíamos una capa compartida, cada corrección futura tendría un lugar canónico. El trabajo que culminó en una cabecera compartida dentro de packages/ui fue el primer paso estructural. La misma marca, la misma navegación base, el mismo menú móvil y los mismos recursos podían alimentar tanto la web principal como Puzzles. La diferencia importante no estaba en que el HTML fuese idéntico por estética, sino en que pasaba a existir una fuente que podía evolucionar sin exigir dos implementaciones paralelas.

Arquitectura compartida

Una capa de plataforma alimenta dos productos sin borrar sus diferencias.

packages/uiLogo, header, footer, idioma, tema y contratos compartidos.
apps/webBlupoli principal, editorial y rutas propias.
apps/puzzlesCatálogo, juegos, categorías y rutas localizadas.
BuildCopia, normaliza y valida assets y markup.

Compartir markup no basta

La primera lección apareció rápido: compartir una pieza no elimina automáticamente la versión antigua. Cuando introdujimos el selector de idioma común, Puzzles conservaba mecanismos heredados de localización. El resultado podía ser exactamente lo contrario de lo buscado: dos selectores visibles o dos capas de JavaScript intentando gobernar la misma interacción. La corrección posterior no consistió en retocar márgenes para que uno quedara escondido. Eliminamos la inyección heredada y convertimos la presencia de un único selector compartido en una condición que el build pudiera comprobar. Esa diferencia entre «parece correcto» y «la arquitectura impide la duplicación» ha sido una constante en esta etapa.

También descubrimos que una UI compartida necesita configuración, no uniformidad ciega. Las rutas inglesas son un buen ejemplo. La web principal evolucionó hacia inglés como idioma por defecto sin prefijo en su raíz, mientras que Puzzles mantiene convenciones de rutas localizadas que no son idénticas. Un selector común no podía asumir que cambiar a inglés significaba construir la misma URL en ambos productos. Debía conocer el contexto de la superficie, resolver la ruta equivalente y preservar la intención del usuario. Compartir el control tenía sentido; compartir una regla de routing incorrecta no.

Esto nos obligó a separar dos ideas que suelen mezclarse: componente compartido y comportamiento parametrizable. El header puede ser común y, al mismo tiempo, recibir destinos distintos. El logo puede ser canónico y aceptar un logoHref apropiado para cada superficie. El selector puede compartir diseño y eventos, pero resolver idiomas según la convención de rutas de la aplicación anfitriona. La arquitectura mejoró cuando dejamos de perseguir «el mismo código en todas partes» y empezamos a perseguir «una misma responsabilidad con diferencias explícitas».

El logo fue una prueba de consistencia, no de dibujo

La unificación de identidad hizo visible otra clase de deriva. Un logo parece uno de los assets más fáciles de compartir, pero puede aparecer en la cabecera, el footer, el favicon, un manifest, páginas generadas y metadatos. Si cada destino conserva una copia, actualizar la identidad significa recordar todos esos lugares. El trabajo reciente convirtió la marca de Blupoli en un recurso canónico y añadió comprobaciones para detectar divergencias. En una revisión del HTML generado se comprobaron cientos de páginas para asegurar que el favicon canónico estaba presente. El valor de esa comprobación no está en el número concreto de páginas, sino en que el problema dejó de depender de revisar visualmente rutas al azar.

Incluso después de unificar el recurso, apareció un detalle de alineación en la B y una ruta del footer de Puzzles que necesitaba respetar correctamente el contexto. Son buenos ejemplos de por qué «centralizar» no significa «terminar». Una fuente única reduce la superficie de error, pero las integraciones siguen teniendo que respetar basePath, enlaces y contextos de despliegue. Por eso añadimos pruebas orientadas a esas fronteras. Si el mismo recurso se utiliza desde dos aplicaciones, la calidad de la abstracción se mide precisamente en los lugares donde las aplicaciones dejan de ser iguales.

El tema oscuro convirtió el navegador en parte de la arquitectura

La preferencia de tema parecía todavía más sencilla: guardar light o dark y aplicar una clase. El problema aparece cuando una persona cambia el tema en una superficie y navega a otra. Si cada aplicación usa una clave distinta de localStorage, la preferencia se fragmenta. Y localStorage no se comparte automáticamente entre un dominio principal y todos sus subdominios. La experiencia que queríamos —elegir una vez y mantener la decisión— exigía pensar en almacenamiento como una pieza de plataforma.

La solución reciente consolidó la preferencia bajo blupoli-theme, mantuvo migración y sincronización con la clave heredada blupoli-theme y utilizó una cookie de dominio junto con almacenamiento local para propagar la decisión entre Blupoli y sus subdominios. No era necesario construir un servicio de identidad para una preferencia visual; sí era necesario reconocer que la frontera del navegador importaba. La cookie ofrece un puente de dominio y cada superficie puede seguir usando almacenamiento local para una lectura rápida y compatible con su código.

La migración de la clave antigua también era importante. Cambiar de marca no debería borrar silenciosamente una preferencia que el usuario ya había expresado. Mantener compatibilidad durante la transición evita que una mejora arquitectónica se perciba como una regresión. Ese principio se repite en otras partes del repositorio: los nombres internos heredados pueden sobrevivir un tiempo cuando romperlos no aporta valor, mientras la identidad pública y los nuevos contratos avanzan hacia Blupoli.

Estado compartido

Idioma y tema atraviesan superficies distintas, pero cada una conserva su routing y renderizado.

ElecciónEl usuario cambia idioma o tema en una superficie.
Preferencia comúnClave canónica, cookie de dominio y contrato compartido.
Resolución localCada app calcula su ruta y aplica su presentación.
ValidaciónEl build detecta duplicados, assets obsoletos y divergencias.

La coherencia de idioma es más difícil que traducir textos

El selector de idioma puso sobre la mesa una definición más exigente de internacionalización. No basta con que una página tenga traducciones. Si el usuario elige inglés en Blupoli y al entrar en Puzzles encuentra español, la plataforma contradice una decisión explícita. Si no ha elegido nada, tiene sentido detectar el idioma del navegador; si no lo soportamos, necesitamos un fallback estable. Esa política debe ser igual en toda la familia, aunque la forma de construir las rutas cambie entre aplicaciones.

El trabajo English-first añadió otra capa. Blupoli principal pasó a tratar inglés como experiencia real por defecto, se añadieron home localizadas y el header compartido expuso EN, ES, IT, PT, FR y DE. Puzzles ya tenía una historia de localización más amplia en sus juegos. Unificar ambas superficies significaba no confundir «idioma de autoría» con «idioma público por defecto». El contenido editorial puede seguir naciendo en español y tener su transcreación inglesa; la plataforma puede, al mismo tiempo, resolver inglés como fallback público. Son decisiones diferentes y conviene que el código las represente como tales.

Además, la coherencia no termina en la navegación. Metadatos, SEO, textos generados, tarjetas, páginas de categorías y estados de los juegos deben respetar el locale. Por eso las mejoras recientes de idioma no se limitaron al selector. Los builds y scripts de localización se convirtieron en parte del contrato. Cuando una superficie no tiene una traducción editorial equivalente, el fallback debe ser deliberado y visible en la arquitectura, no una casualidad de qué archivo se encontró primero.

CSS compartido: la parte que parece fácil hasta que hereda diez años de contexto

El footer móvil mostró un problema clásico. Una pieza compartida puede estar correctamente implementada y aun así ser modificada por reglas globales antiguas del producto que la hospeda. En Puzzles había estilos heredados capaces de interferir con la visibilidad del footer. El arreglo no fue simplemente aumentar la especificidad hasta ganar una guerra de selectores. Aislamos mejor el CSS de la pieza compartida para reducir el acoplamiento con reglas globales. Es un cambio pequeño en archivos y grande en intención: un componente de plataforma debe poder entrar en una aplicación sin quedar a merced de convenciones CSS que no conoce.

El mismo razonamiento se aplicó al responsive. La navegación móvil, el menú hamburguesa, los objetivos táctiles y el comportamiento desde anchuras pequeñas se revisaron como sistema. El menú incorpora estado accesible con aria-expanded, cierre con Escape y clic exterior, además de bloqueo de scroll cuando corresponde. La home y los controles de juego tienen necesidades diferentes, pero comparten una expectativa: una pantalla estrecha no debe ser una versión encogida del escritorio. Las mejoras de 320–340 píxeles y objetivos cercanos a 44 píxeles forman parte de esa coherencia de producto, aunque no todo viva en el mismo componente.

El caché también puede romper una identidad compartida

Después de corregir markup y assets apareció un enemigo menos visible: una persona puede recibir HTML nuevo y conservar CSS, JavaScript o SVG antiguos. En una migración de interfaz eso produce combinaciones que el desarrollador no ve en un build limpio. La política de caché se ajustó para que assets mutables como CSS, JS, SVG e ICO se revaliden, mientras recursos realmente apropiados para caché larga, como imágenes raster o fuentes versionables, pueden conservarla. La coherencia visual depende también de que las piezas de una versión lleguen juntas.

Este punto fue especialmente relevante alrededor del selector de idioma y la identidad. Cuando se elimina una implementación heredada pero el navegador conserva uno de sus assets, es posible interpretar un fallo de caché como un fallo de código actual. Hacer explícita la estrategia reduce esa ambigüedad. No convierte cada despliegue en atómico, pero disminuye el tiempo durante el que un cliente puede mezclar contratos incompatibles.

Por qué no fusionamos las aplicaciones

Con todo este trabajo podría parecer que la conclusión natural es acabar con dos aplicaciones y crear una sola. No necesariamente. Blupoli principal y Blupoli Puzzles tienen responsabilidades distintas. La web principal aloja identidad, descubrimiento y contenido editorial; Puzzles tiene catálogo, motores de juego, estados, categorías y una generación de rutas mucho más intensa. Forzar ambas necesidades dentro de un único runtime solo para compartir cabecera y tema introduciría un acoplamiento mayor que el problema original.

El monorepo nos permite una alternativa más útil: compartir paquetes y contratos donde hay una responsabilidad común y mantener aplicaciones separadas donde los ciclos de desarrollo difieren. Esa es también una base razonable para futuros productos. Si mañana aparece otra aplicación bajo Blupoli, no debería copiar el header de Puzzles ni depender del runtime de los juegos. Debería consumir la capa de plataforma y declarar sus propias diferencias. El trabajo de estas semanas no es únicamente una limpieza visual; es una prueba de qué tipo de monorepo queremos tener.

Los gates convierten una intención en una propiedad del sistema

Una de las decisiones más importantes fue dejar de confiar exclusivamente en documentación del tipo «recuerda usar el logo nuevo». El build comprueba condiciones que antes dependían de memoria: ausencia de selectores heredados, presencia del favicon canónico, rutas válidas, assets correctos y otras invariantes. No todo puede automatizarse, pero todo lo que sí puede debería liberar atención humana para problemas más interesantes.

Esto cambia la economía de las regresiones. Sin un gate, una duplicación de selector puede llegar a producción y descubrirse navegando una ruta concreta. Con un gate, la arquitectura declara que exactamente una implementación es válida. Sin una comprobación de assets, una copia antigua del logo puede sobrevivir durante meses. Con una comparación o normalización en build, la divergencia se convierte en un fallo observable. La diferencia no es perfección; es hacer que ciertos errores dejen de ser silenciosos.

También hemos aprendido a no usar gates como sustituto de criterio. Un test puede demostrar que existe un único selector, pero no que su diseño sea cómodo. Puede verificar un href, pero no que el cambio de idioma resulte comprensible. Puede encontrar el favicon, pero no decidir si la B está visualmente centrada. La arquitectura compartida funciona mejor cuando automatización y revisión visual se reparten el trabajo según lo que cada una sabe medir.

Lo que cambió para quien usa Blupoli

La mayor parte de esta ingeniería debería ser aburrida desde fuera, y eso es una buena señal. Una persona no necesita saber que packages/ui existe. Solo necesita que el logo lleve al lugar correcto, que el menú no cambie de personalidad al cruzar a Puzzles, que su tema se mantenga, que el idioma elegido siga siendo el idioma de la experiencia y que el footer no desaparezca en móvil. Cuando una capa de plataforma funciona, se nota sobre todo por la ausencia de pequeñas contradicciones.

También mejora la capacidad de evolución. Una corrección de navegación puede propagarse desde un lugar común. Una nueva aplicación puede adoptar la identidad sin reconstruirla. Un cambio de marca no necesita perseguir copias dispersas. Y una regresión en piezas canónicas tiene más probabilidades de romper el build antes que la experiencia pública. Para un proyecto que está creciendo desde una colección de puzzles hacia una plataforma con productos diferenciados, esa reducción de deriva es más valiosa que ahorrar unas líneas de HTML.

Lo que cambió para mantener el repositorio

Internamente, ahora resulta más fácil responder a una pregunta básica: «¿dónde debería vivir este cambio?». Si afecta a la identidad y navegación común, la primera candidata es la capa compartida. Si afecta al catálogo o a un motor, pertenece a Puzzles. Si es editorial, sigue su pipeline. Si una diferencia de rutas obliga a parametrizar un componente, esa diferencia queda expresada en el contrato en lugar de materializarse como una copia. Tener esa respuesta reduce el coste de decisiones futuras.

También hemos ganado una forma más precisa de hablar de deuda. Todavía existen nombres y mecanismos heredados por compatibilidad; eliminarlos todos de golpe no es un objetivo en sí mismo. Lo importante es que no gobiernen la experiencia pública ni obliguen a nuevas piezas a repetir el pasado. La migración de blupoli-theme a blupoli-theme resume bien esa filosofía: preservar al usuario, cambiar el contrato canónico y dejar una ruta de transición.

Qué queda por hacer

La unificación no está cerrada para siempre. Cada nueva superficie puede revelar otro supuesto oculto: una ruta que se resuelve distinto, una política de locale que necesita más precisión, un componente editorial que todavía usa estilos propios o una preferencia que debería cruzar productos. El objetivo no es declarar que toda la UI está abstraída. De hecho, abstraer demasiado pronto sería otra forma de deuda. Queremos que la capa compartida crezca cuando aparece una responsabilidad realmente común y que las aplicaciones conserven libertad donde sus necesidades son específicas.

También queda seguir fortaleciendo pruebas de navegación real entre dominios y locales. Los gates de build son excelentes para estructura estática, pero las transiciones que dependen de almacenamiento, cookies y comportamiento del navegador merecen validación de extremo a extremo cuando el coste lo justifique. Lo mismo ocurre con accesibilidad: los atributos pueden comprobarse automáticamente, mientras el flujo completo necesita inspección con teclado y tecnologías de asistencia.

La lección que nos llevamos

La parte más útil de este trabajo no fue encontrar una manera de reutilizar un header. Fue aprender que la coherencia de una plataforma es un problema de arquitectura cuando la experiencia cruza aplicaciones. Marca, idioma, tema, navegación, caché y accesibilidad parecen capas distintas hasta que el usuario las experimenta en un solo recorrido. Si cada equipo o superficie resuelve esas decisiones localmente, la fragmentación aparece incluso con un diseño idéntico.

La respuesta tampoco es convertir todo en un componente universal. Una buena plataforma comparte decisiones, no borra contextos. En Blupoli eso significa una identidad canónica, contratos comunes para preferencias y navegación, configuración explícita para las diferencias de routing y gates que vigilan la deriva. Puzzles puede seguir siendo Puzzles; la web principal puede seguir teniendo su propio ciclo; futuros productos podrán añadir necesidades nuevas. La unidad está en las reglas que deben sentirse iguales.

Este enfoque conecta con el trabajo descrito en nuestro Devlog sobre el sistema de UI compartido y con la evolución editorial explicada en la separación entre Blog y Devlog. La cara pública de la transición de marca también se cuenta en Blupoli cambia de piel. Aquí la conclusión es más técnica: cuando dos aplicaciones deben sentirse como una plataforma, el trabajo decisivo ocurre en las fronteras que el diseño por sí solo no puede ver.

Ahora tenemos una base mejor para seguir creciendo. No porque hayamos eliminado todas las diferencias, sino porque sabemos cuáles deben existir y cuáles son regresiones. Esa distinción convierte la coherencia de algo que se revisa a ojo en algo que la arquitectura ayuda a conservar. Y para un proyecto que quiere sumar productos sin multiplicar identidades, esa es probablemente la propiedad más importante que podíamos obtener de esta etapa.