Skip to main content

ADR 006: Unified Perimeter — Storage Namespace & Blog Locale Sovereignty

Status: Active Decider: Architecture Lead Date: 2026-04-27 (v0.7.0 sprint, CEO 051, commit 3188387)


Context

This ADR is specific to the zenzic.dev documentation site (this repository), not to the Zenzic CLI core. It documents two independent locale-bleed bugs that were introduced when future.v4: true was activated in docusaurus.config.ts.

Bug 1 — The Theme Flip

With future.v4: true, Docusaurus enables siteStorageNamespacing: it auto-generates a per-locale localStorage key by hashing url + baseUrl + locale. This produced:

LocalelocalStorage key
English (/)theme-926
Italian (/it/)theme-3d7

When a user switched from the English to the Italian documentation, their browser loaded a different localStorage key. Since the Italian key had no stored preference, Docusaurus fell back to defaultMode: 'dark'. If the user had previously switched to light mode in English, the switch caused an instant dark mode revert — a visible FOUC (Flash of Unstyled Content) on every locale switch.

Bug 2 — The Blog Locale Bleed

The Blog link in the navbar pointed to the blog using a standard Docusaurus navbar item:

// docusaurus.config.ts — original, broken
{ to: '/blog', label: 'Journal', position: 'left' }

Docusaurus's static build pipeline rewrites both to: and href: values in navbar items for each locale's HTML output. In the Italian static build, this became:

<!-- build/it/docs/*/index.html -->
<a href="/it/blog">Journal</a>

When a user navigated from Italian documentation to the Journal via that link, they landed on /it/blog — which loaded the blog with the Italian locale UI: dates rendered as "25 aprile 2026", labels appeared as "Etichette", the reading time showed "9 minuti di lettura". The Blog is an English-only content space and must never be locale-translated.

Switching from to: to href: did not fix the issue: href: values in standard navbar items are also rewritten by the Docusaurus i18n build pipeline.


Decision

Two independent fixes were applied to docusaurus.config.ts:

Fix 1 — Unified Storage Namespace

// docusaurus.config.ts
storage: {
namespace: false,
},

The top-level storage.namespace: false overrides the future.v4 namespacing behaviour. Both locales now share the single key "theme" in localStorage. Dark mode preference persists across all locale switches.

Verified in build output: The anti-FOUC inline script in both build/index.html and build/it/index.html reads:

localStorage.getItem("theme")
// docusaurus.config.ts — Blog navbar item
{
type: 'html',
value: '<a class="navbar__item navbar__link" href="/blog">Blog</a>',
position: 'left',
},

Docusaurus does not process the innerHTML of type: 'html' navbar items through the i18n rewrite pipeline. The raw href="/blog" is preserved verbatim in every locale's static HTML output.

Verified in build output: The Italian locale HTML contains:

href=/blog>Blog</a>

Not /it/blog — locale-sovereign.


Rejected Approaches

themeConfig.siteStorage.themeKey

Proposed in the CEO directive as a way to control the storage key. This property does not exist in Docusaurus 3.x. There is no themeConfig.siteStorage namespace. The correct API is the top-level storage object.

respectPrefersColorScheme: true

Also proposed in the CEO directive. This would instruct Docusaurus to follow the OS-level color scheme preference on every page load — overriding the user's explicit in-app preference. This directly reverts the CEO 149 invariant (respectPrefersColorScheme: false) which was established as a permanent protection against OS-preference-driven theme resets. It was not applied.


Invariants (Non-Negotiable)

  • storage: { namespace: false } must remain in docusaurus.config.ts for as

    long as future.v4: true is active and the Italian locale is supported. Removing it silently re-introduces per-locale storage key fragmentation.

  • colorMode.respectPrefersColorScheme must remain false. This is an

    immutable invariant (CEO 149). Any PR that sets it to true is an automatic revert candidate.

  • The Blog navbar item must remain type: 'html'. Converting it back to a

    standard to: or href: item will re-introduce locale bleed in the next build. This is not immediately visible in development mode (npm run start) because npm run start serves a single locale without the rewrite pipeline. Bugs of this class are only visible in just build output.


Consequences

  • Dark mode preference is now fully locale-independent. A user who sets dark mode

    in English documentation retains dark mode when switching to Italian.

  • The Blog (blog) always loads at /blog regardless of which locale the user

    navigated from.

  • The type: 'html' navbar item does not participate in Docusaurus's i18n

    translation pipeline (i.e., it does not appear in code.json translation keys). The label "Blog" is therefore hardcoded in the HTML value — this is intentional, as the blog is English-only and the label does not require translation.