Files
bootnode/web/lib/brand.ts
zooqueenandHanzo Dev 95d9ac6fa9 web: decomplect brand into runtime white-label config
Remove the hardcoded 5-brand catalog from web/lib/brand.ts; resolve the
active brand at runtime by hostname from an external catalog
(window.__BRAND_CATALOG__ / /config.json), mirroring the @hanzo/id SPA.
Ship one neutral built-in bootnode fallback. Add BrandProvider that loads
the per-deploy catalog client-side after hydration.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-17 00:10:05 -07:00

199 lines
6.7 KiB
TypeScript

// White-label branding — runtime-resolved, brand-neutral source.
//
// This module knows ONE brand: bootnode's own neutral defaults (the built-in
// fallback). Every real brand (lux / zoo / pars / hanzo / …) is supplied at
// RUNTIME from an external catalog injected per-deploy — bootnode the app ships
// no brand names, domains, or links in source.
//
// Mechanism (mirrors the @hanzo/id SPA's runtime config loader):
// 1. The deploy serves `/config.json` and/or injects a `window.__BRAND_CATALOG__`
// global (a host->BrandConfig map, or a string of one) into the page.
// 2. `applyBrandCatalog()` ingests it; `getBrandKey()` resolves the active
// brand by `window.location.hostname` against the catalog.
// 3. `getBrand()` returns the resolved BrandConfig, falling back to the
// built-in neutral `bootnode` defaults when no catalog entry matches.
//
// The full multi-brand catalog lives in the DEPLOY config (universe / CF-Pages),
// NOT in this repo. See `public/config.json` for the contract + the README note
// in the PR for where the per-host catalog is provisioned.
export type BrandConfig = {
name: string
tagline: string
description: string
logo: string
logoIcon: string
logoWhite: string
favicon: string
domain: string
apiUrl: string
wsUrl: string
statusUrl: string
defaultTheme: "light" | "dark"
colors: {
primary: string
primaryForeground: string
accent: string // Hero gradient start
accentEnd: string // Hero gradient end
}
social: {
twitter?: string
github?: string
discord?: string
}
iam: {
url: string
clientId: string
domain: string // e.g., hanzo.id, zoo.id
}
footerLinks: {
name: string
href: string
external?: boolean
}[]
// Ecosystem apps — retail-facing services linked from dashboard
ecosystemApps: {
name: string
href: string
description: string
icon: string // lucide icon name
category: "explore" | "defi" | "tools" | "governance"
}[]
}
// Runtime catalog: host substring -> brand key, plus key -> BrandConfig.
// Both are empty in source. Populated at runtime by `applyBrandCatalog()` from
// the deploy-injected config (see public/config.json for the shape).
export type BrandCatalog = {
// Ordered host matchers. First whose `match` is a substring of the hostname wins.
hosts?: { match: string; key: string }[]
// Brand definitions, keyed by brand key (e.g. "lux", "zoo").
brands?: Record<string, BrandConfig>
// Default brand key for unknown hosts (defaults to "bootnode").
defaultKey?: string
}
// The ONE built-in brand: bootnode's neutral defaults. Used for dev, SSG, and
// any host with no catalog entry. Intentionally generic — no third-party brand.
const BOOTNODE_DEFAULT: BrandConfig = {
name: "Bootnode",
tagline: "Blockchain Infrastructure for Developers",
description:
"The complete blockchain development platform. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, Webhooks, and more.",
logo: "/logo.svg",
logoIcon: "/logo.svg",
logoWhite: "/logo.svg",
favicon: "/logo.svg",
domain: "bootno.de",
apiUrl: "https://api.bootno.de",
wsUrl: "wss://ws.bootno.de",
statusUrl: "https://status.bootno.de",
defaultTheme: "light",
colors: {
primary: "#000000",
primaryForeground: "#ffffff",
accent: "#3b82f6",
accentEnd: "#06b6d4",
},
social: {},
iam: {
url: "https://hanzo.id",
clientId: "bootnode-web",
domain: "hanzo.id",
},
footerLinks: [],
ecosystemApps: [],
}
const DEFAULT_KEY = "bootnode"
// Active runtime catalog. Starts empty (source ships no brands). The deploy
// fills it via `applyBrandCatalog()` before/at hydration.
let catalog: BrandCatalog = {}
/**
* Parse a raw catalog value (object or JSON string) into a BrandCatalog.
* Returns {} on any error — never throws (mirrors id SPA's `parseCatalog`).
*/
export function parseBrandCatalog(raw: unknown): BrandCatalog {
if (!raw) return {}
try {
const obj = typeof raw === "string" ? JSON.parse(raw) : raw
if (!obj || typeof obj !== "object") return {}
return obj as BrandCatalog
} catch {
return {}
}
}
/**
* Install the runtime brand catalog. Idempotent — safe to call repeatedly
* (e.g. once from the inline boot script, once after the /config.json fetch).
* Accepts either a parsed object or a raw JSON string.
*/
export function applyBrandCatalog(raw: unknown): void {
const next = parseBrandCatalog(raw)
if (Object.keys(next).length > 0) catalog = next
}
/**
* Read the catalog injected as a global by the deploy
* (`window.__BRAND_CATALOG__`). Synchronous — available before the first
* render when the deploy inlines it in the document head.
*/
function readInjectedCatalog(): BrandCatalog {
if (typeof window === "undefined") return {}
const w = window as unknown as { __BRAND_CATALOG__?: unknown }
return parseBrandCatalog(w.__BRAND_CATALOG__)
}
function activeCatalog(): BrandCatalog {
if (catalog.brands || catalog.hosts) return catalog
return readInjectedCatalog()
}
/**
* Resolve the active brand key from the hostname against the runtime catalog.
* No hardcoded host switches — the host->key table is data, supplied at deploy.
*/
function getBrandKey(): string {
const cat = activeCatalog()
const fallback = cat.defaultKey || DEFAULT_KEY
if (typeof window === "undefined") return fallback
const hostname = window.location.hostname.toLowerCase()
for (const rule of cat.hosts ?? []) {
if (rule.match && hostname.includes(rule.match.toLowerCase())) return rule.key
}
return fallback
}
// Export the active brand configuration. Synchronous so module-level callers
// (`const brand = getBrand()`) keep working; resolves against the injected
// catalog when present, else the built-in neutral default.
export function getBrand(): BrandConfig {
const cat = activeCatalog()
const key = getBrandKey()
return cat.brands?.[key] ?? cat.brands?.[DEFAULT_KEY] ?? BOOTNODE_DEFAULT
}
// Export brand key for conditional rendering / per-brand local maps
// (SDK package names, region data, localStorage namespacing).
export function getBrandKey_(): string {
return getBrandKey()
}
// Check if the active brand is the Hanzo brand.
export function isHanzoBrand(): boolean {
return getBrandKey() === "hanzo"
}
// Get all brands currently known at runtime (for admin/testing). Empty until a
// catalog is injected — source ships no brands.
export function getAllBrands(): Record<string, BrandConfig> {
return activeCatalog().brands ?? { [DEFAULT_KEY]: BOOTNODE_DEFAULT }
}
// Default export for convenience (neutral default at module-eval / SSG time;
// the BrandProvider re-resolves client-side once the runtime catalog loads).
export const brand = getBrand()