A brand can look like a design problem until two real applications sit behind it. At that point the logo is no longer merely an SVG, the language selector is no longer merely a control, and dark mode is no longer merely a local preference. Every one of those pieces starts crossing boundaries: routes, builds, domains, storage, caches and inherited code. That is what happened as we consolidated Blupoli as the umbrella platform while keeping Blupoli Puzzles as a product with its own technical life. Visually, we wanted one family. Technically, we still had separate surfaces with different histories and route conventions.

The obvious temptation was to solve the mismatch from the top down: choose one framework, move everything into a fresh application and assume consistency would arrive after the migration. We chose not to. The repository already contained the main site, the puzzle application, hundreds of generated routes, editorial content and a substantial build pipeline. Rewriting that system in order to share a header would have exchanged a bounded problem for a much larger migration. Instead, we treated inconsistency as a boundary problem. We needed to decide what belonged to Blupoli, what belonged to each product, and how to stop common pieces from being copied again the next time a new requirement appeared.

The symptom was not just a different header

At first, the failures could be read as unrelated details. One version of the logo had slightly different geometry. The Puzzles footer inherited old global CSS rules that could hide it on mobile. The main site and the product could expose language controls that did not behave in exactly the same way. An editorial page could remember dark mode while another surface fell back to light. Favicons could lag behind the current identity because of a path or caching policy. None of those defects justified a rewrite on its own. Together, however, they exposed a deeper issue: there was not yet a strong enough boundary between platform UI and product-specific UI.

That diagnosis changed the solution. Fixing each screen independently would have produced a set of patches that happened to look alike. Extracting a shared layer gave every future correction a canonical home. The work that put a shared header in packages/ui was the structural starting point. The same brand, base navigation, mobile-menu behavior and assets could feed both the main site and Puzzles. The important part was not identical HTML for its own sake. It was the existence of a source that could evolve without requiring two parallel implementations.

Shared architecture

A platform layer feeds two products without erasing their differences.

packages/uiLogo, header, footer, language, theme and shared contracts.
apps/webMain Blupoli site, editorial surfaces and its route model.
apps/puzzlesCatalog, games, categories and localized routes.
BuildCopies, normalizes and validates assets and markup.

Sharing markup is not enough

The first lesson arrived quickly: adding a shared component does not automatically remove the old one. When we introduced the common language selector, Puzzles still had inherited localization mechanisms. The result could be the opposite of what we wanted: two visible selectors, or two JavaScript layers trying to control the same interaction. The follow-up fix did not hide one with CSS. We removed the legacy injection and made the presence of exactly one shared selector something the build could validate. That distinction between “looks correct” and “the architecture rejects duplication” has shaped much of this work.

We also learned that shared UI needs configuration rather than blind uniformity. English routes are a useful example. The main site moved toward English as the real default language, with no prefix on its root, while Puzzles retains localization conventions that are not identical. A common selector could not assume that switching to English meant constructing the same URL on both products. It needed to understand its host surface, resolve the equivalent route and preserve the user’s intent. Sharing the control made sense; sharing an incorrect routing assumption did not.

That forced us to separate two ideas that are often conflated: a shared component and configurable behavior. The header can be common while receiving different destinations. The logo can be canonical while accepting an appropriate logoHref for each surface. The selector can share design and events while resolving locales according to the host application’s route conventions. The architecture improved when we stopped chasing “the same code everywhere” and started chasing “the same responsibility with explicit differences.”

The logo became a consistency test, not a drawing task

Identity unification exposed another form of drift. A logo sounds like one of the easiest assets to share, yet it can appear in a header, a footer, a favicon, a manifest, generated pages and metadata. If each destination keeps a copy, changing identity means remembering all of them. Recent work established a canonical Blupoli mark and added checks that can catch divergence. A generated-site review checked hundreds of HTML pages for the canonical favicon. The value is not the exact page count. The value is that correctness no longer depends on manually sampling a few routes.

Even after centralizing the asset, a small alignment issue in the B and a Puzzles footer path still needed correction. Those are useful reminders that “centralized” does not mean “finished.” A single source reduces the error surface, but integrations must still respect basePath, links and deployment context. We therefore added tests around those boundaries. If one asset is consumed by two applications, the abstraction is tested most seriously exactly where the applications stop being identical.

Dark mode made the browser part of the architecture

The theme preference looked simpler still: store light or dark and apply a class. The complication begins when somebody changes the theme on one surface and navigates to another. If each application uses a different localStorage key, the preference fragments. And local storage is not automatically shared between a parent domain and every subdomain. The experience we wanted — choose once, keep the choice — meant treating storage as platform infrastructure.

The recent implementation consolidated the canonical preference under blupoli-theme, kept migration and synchronization support for the older puzzlehub-theme key, and used a domain cookie alongside local storage to carry the decision across Blupoli surfaces. We did not need an identity service for a visual preference. We did need to acknowledge the browser’s origin boundaries. The cookie provides a cross-domain bridge, while each surface can retain fast local access compatible with its existing code.

Migrating the old key mattered as well. A brand change should not silently erase a preference a person already expressed. Preserving compatibility during the transition prevents an architectural improvement from becoming a user-facing regression. The same principle appears elsewhere in the repository: internal legacy names can survive for a while when removing them creates more risk than value, while public identity and new contracts move toward Blupoli.

Shared state

Language and theme cross product boundaries while each app keeps its own routing and rendering.

ChoiceThe user changes language or theme on one surface.
Common preferenceCanonical key, domain cookie and shared contract.
Local resolutionEach app computes its route and presentation.
ValidationThe build detects duplicates, stale assets and drift.

Language consistency is harder than translating strings

The language selector forced us to adopt a stricter definition of localization. Having translations on a page is not enough. If a user chooses English on Blupoli and lands in Spanish after moving to Puzzles, the platform has contradicted an explicit choice. If no choice exists, browser-language detection is sensible; if that language is unsupported, the fallback must be stable. That policy should be coherent across the family even if route construction differs between applications.

The English-first work added another layer. The main Blupoli site started treating English as a real default experience, localized home pages were added, and the shared header exposed EN, ES, IT, PT, FR and DE. Puzzles already had a broader localization history across its games. Bringing the two surfaces together meant separating “authoring language” from “public default language.” Editorial content can still originate in Spanish and receive a full English transcreation while the platform resolves English as the fallback public locale. Those are different decisions, and the code should represent them as different decisions.

Consistency also extends beyond navigation. Metadata, SEO, generated copy, cards, category pages and game states need to respect locale. That is why the recent language work did not stop at the selector. Localization scripts and build rules became part of the contract. When an editorial surface lacks an equivalent translation, fallback behavior needs to be deliberate and visible in the architecture rather than an accident of whichever file happened to be found first.

Shared CSS: easy until it inherits years of context

The mobile footer exposed a classic problem. A shared component can be correctly implemented and still be modified by old global rules in the host product. Puzzles contained inherited styles that could interfere with footer visibility. The fix was not simply to increase specificity until the new CSS won a selector war. We isolated the shared footer styling more carefully so it depended less on global product conventions. It is a small file-level change with a large architectural intention: a platform component should be able to enter an application without being at the mercy of CSS rules it does not know.

The same thinking informed responsive work. Mobile navigation, the hamburger menu, touch targets and behavior at narrow widths were reviewed as a system. The menu tracks accessible state through aria-expanded, closes with Escape and outside clicks, and controls body scrolling where appropriate. The home page and game controls have different needs, but they share an expectation: a narrow screen must not be a shrunken desktop. Improvements down to roughly 320–340 pixels and touch targets around 44 pixels belong to the same product-coherence story even when not every rule lives in the shared package.

Caching can break a shared identity too

Once markup and assets were corrected, a less visible problem remained: a browser can receive new HTML while holding old CSS, JavaScript or SVG assets. During an interface migration that creates combinations developers do not see in a clean build. We adjusted caching so mutable assets such as CSS, JS, SVG and ICO revalidate, while resources that genuinely benefit from long-lived caching, such as appropriate raster images or fonts, can keep it. Visual coherence depends on compatible pieces of a release arriving together.

This mattered around the language selector and identity assets in particular. When a legacy implementation is removed but one of its resources remains cached, a caching problem can be misread as a defect in current source. Making the policy explicit reduces that ambiguity. It does not make every deployment perfectly atomic, but it shortens the period in which a client can combine incompatible contracts.

Why we did not merge the applications

After all this work, it might seem that the logical conclusion is to eliminate two applications and create one. Not necessarily. Main Blupoli and Blupoli Puzzles have different responsibilities. The main site carries identity, discovery and editorial content; Puzzles carries the catalog, game engines, state, categories and a much heavier generated-route model. Forcing both sets of needs into one runtime merely to share a header and theme would create more coupling than the original problem.

The monorepo gives us a more useful option: share packages and contracts where responsibility is genuinely common, and keep separate applications where development cycles differ. That also creates a sensible base for future products. If another Blupoli application appears, it should not copy the Puzzles header or depend on game runtime code. It should consume the platform layer and declare its own differences. The recent work is therefore more than visual cleanup; it is a practical test of the kind of monorepo we want to maintain.

Build gates turn an intention into a system property

One of the most consequential choices was to stop relying exclusively on documentation such as “remember to use the new logo.” The build now checks conditions that used to depend on memory: absence of legacy selectors, presence of canonical identity assets, valid routes and other invariants. Automation cannot cover everything, but anything it can cover should free human attention for problems that require judgment.

This changes the economics of regressions. Without a gate, a duplicated selector can reach production and only be discovered on a particular route. With a gate, the architecture declares that exactly one implementation is valid. Without asset checks, an old logo copy can survive for months. With build-time comparison or normalization, divergence becomes an observable failure. This is not perfection. It is the removal of silence from a class of errors.

We have also learned not to confuse gates with judgment. A test can prove there is one selector, but not that its interaction feels good. It can validate an href, but not whether a locale transition is understandable. It can find a favicon, but not decide whether the B is optically centered. Shared architecture works best when automation and visual review divide the work according to what each can actually measure.

What changed for people using Blupoli

Most of this engineering should be boring from the outside, and that is a positive outcome. A visitor does not need to know that packages/ui exists. They need the logo to lead somewhere sensible, the menu not to change personality when they cross into Puzzles, their theme to persist, their chosen language to remain the language of the experience, and the footer not to disappear on mobile. When a platform layer works, its success is often visible as the absence of small contradictions.

It also improves our ability to evolve. A navigation correction can propagate from a shared source. A future application can adopt the identity without rebuilding it. A brand adjustment does not need to chase scattered copies. And a regression in canonical pieces is more likely to break a build before it breaks the public experience. For a project growing from a puzzle collection toward a platform with differentiated products, reducing drift matters more than saving a few lines of HTML.

What changed for maintainers

Internally, it is easier to answer a basic question: “where should this change live?” If it affects common identity and navigation, the shared layer is the first candidate. If it affects the catalog or an engine, it belongs to Puzzles. Editorial work follows its own pipeline. If route differences require a component parameter, the difference is expressed in the contract rather than materialized as a copy. Having that answer reduces the cost of future decisions.

We also have a more precise way to discuss debt. Legacy names and mechanisms still exist where compatibility requires them; deleting all of them immediately is not a goal in itself. What matters is that they no longer govern the public experience or force new code to repeat the past. The move from puzzlehub-theme to blupoli-theme captures that philosophy well: preserve the user’s state, change the canonical contract, and provide a transition path.

What remains unfinished

Unification is not permanently complete. Every new surface can reveal another hidden assumption: a route that resolves differently, a locale policy that needs more precision, an editorial component that still owns private styling, or a preference that should cross products. The goal is not to claim all UI has been abstracted. Over-abstraction would become another form of debt. We want the shared layer to grow when a responsibility is truly common and allow applications to remain independent where their needs are specific.

There is also room to strengthen real navigation tests across domains and locales. Build gates are excellent at structural invariants, while transitions involving storage, cookies and browser behavior deserve end-to-end validation when the cost is justified. Accessibility follows the same pattern: attributes can be checked automatically, but the complete flow still benefits from keyboard and assistive-technology inspection.

The lesson we are carrying forward

The most useful result was not discovering how to reuse a header. It was learning that platform consistency becomes an architecture problem as soon as an experience crosses applications. Brand, language, theme, navigation, caching and accessibility look like separate layers until a person encounters them in one journey. If every surface resolves those decisions locally, fragmentation appears even when the design files look identical.

The answer is not a universal component for everything either. A good platform shares decisions without erasing context. At Blupoli that now means canonical identity, common contracts for preferences and navigation, explicit configuration for route differences, and gates that watch for drift. Puzzles can remain Puzzles; the main site can keep its own cycle; future products can add new needs. Unity lives in the rules that should feel the same.

This work builds on the ideas in our earlier shared UI Devlog and the editorial architecture described in the move from Journal to Blog and Devlog. The public-facing side of the identity transition is covered in Blupoli changes its skin. The technical conclusion here is narrower: when two applications are supposed to feel like one platform, the decisive work happens at boundaries that design alone cannot see.

We now have a stronger base for growth, not because every difference has disappeared, but because we can distinguish differences that belong from differences that are regressions. That turns consistency from something inspected by eye into something architecture can help preserve. For a project that wants to add products without multiplying identities, that may be the most valuable property we could have gained from this phase.