mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
web: decomplect brand into runtime white-label config (mirror @hanzo/id) (#5)
* fix(web): use real Casdoor client zoo-console for Zoo brand IAM The zoo brand preset used clientId "zoo-app-client-id", which is not a registered Casdoor application — Casdoor's get-app-login returns "Invalid client_id" for it, so login from zoo.cloud / web3.zoo.ngo via zoolabs.id never completed. Switch to "zoo-console" (org "zoo"), the actual registered app whose redirectUris already include https://zoo.cloud/* and https://zoolabs.id/callback, so the OAuth round-trip resolves with no IAM-side changes required. * web: static-export build for lux.cloud (Lux Cloud) Cloudflare Pages deploy Deploy the web3 console as lux.cloud (Lux brand). Convert from SSR (output: standalone) to static export so it serves as static assets on Cloudflare Pages with no edge/Node runtime. - next.config.ts: output "export" + images.unoptimized; drop headers()/ redirects() (unsupported in export) — moved to public/_headers and public/_redirects for Pages. - app/layout.tsx: bake brand from NEXT_PUBLIC_BRAND/BRAND; remove force-dynamic + headers() host detection (client still auto-detects brand from window.location.hostname via lib/brand.ts). - app/api/health, app/llms.txt: mark route handlers force-static. - Remove app/dashboard/api/keys route handler (backend proxy unsupported in export; the dashboard talks to brand.apiUrl directly). - nodes/[id]: split into a server page.tsx (generateStaticParams) + client node-detail-client.tsx so the dynamic segment is export-compatible. - unified-navigation.tsx: fix API Keys link to the real /dashboard/api-keys page (was pointing at the removed route handler). - lib/brand.ts: point lux IAM clientId at the real Casdoor app "lux-cloud" (clientId lux-app-client-id does not exist in IAM). Built with NEXT_PUBLIC_BRAND=lux, IAM lux.id / client lux-cloud; deployed to CF Pages project lux-cloud, custom domain lux.cloud. * 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: Antje Worring <[email protected]> Co-authored-by: hanzo-dev <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Hanzo Dev
parent
352fcf8100
commit
422f8f11b4
+23
-25
@@ -5,23 +5,19 @@ import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { AuthProvider } from "@/lib/auth"
|
||||
import { Providers } from "@/app/providers"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { BrandProvider } from "@/components/brand-provider"
|
||||
import { getBrand } from "@/lib/brand"
|
||||
import "@/app/globals.css"
|
||||
|
||||
// Brand baked at build time from NEXT_PUBLIC_BRAND/BRAND (default lux for this
|
||||
// deployment). Static export: no request-time host detection. The client still
|
||||
// auto-detects the brand from window.location.hostname (see lib/brand.ts), so
|
||||
// runtime CSS/logos remain host-correct; only SSR metadata is baked here.
|
||||
const brands: Record<string, { name: string; tagline: string; domain: string; icon: string; defaultTheme: string }> = {
|
||||
bootnode: { name: "Bootnode", tagline: "Blockchain Infrastructure", domain: "bootno.de", icon: "/logo/bootnode-icon.svg", defaultTheme: "light" },
|
||||
hanzo: { name: "Hanzo Web3", tagline: "Web3 Infrastructure", domain: "web3.hanzo.ai", icon: "/logo/hanzo-icon.svg", defaultTheme: "dark" },
|
||||
lux: { name: "Lux Cloud", tagline: "Next-Gen Blockchain Infrastructure", domain: "cloud.lux.network", icon: "/logo/lux-icon.svg", defaultTheme: "dark" },
|
||||
pars: { name: "Pars Cloud", tagline: "Sovereign L1 Infrastructure", domain: "cloud.pars.network", icon: "/logo/pars-icon.svg", defaultTheme: "dark" },
|
||||
zoo: { name: "Zoo Labs", tagline: "Decentralized AI Infrastructure", domain: "web3.zoo.ngo", icon: "/logo/zoo-icon.svg", defaultTheme: "dark" },
|
||||
}
|
||||
|
||||
// Static export: the prerendered HTML is brand-neutral. There is no
|
||||
// request-time host detection here, and source carries no brand catalog — so
|
||||
// SSG metadata uses bootnode's built-in neutral default. The real brand is
|
||||
// resolved CLIENT-SIDE after hydration by BrandProvider, which loads the
|
||||
// per-deploy catalog (window.__BRAND_CATALOG__ / /config.json) and re-resolves
|
||||
// title/favicon/logos by hostname. Mirrors the @hanzo/id SPA.
|
||||
function getServerBrand() {
|
||||
const key = (process.env.BRAND || process.env.NEXT_PUBLIC_BRAND || "bootnode").toLowerCase()
|
||||
return brands[key] || brands.bootnode
|
||||
const b = getBrand()
|
||||
return { name: b.name, tagline: b.tagline, domain: b.domain, icon: b.favicon, defaultTheme: b.defaultTheme }
|
||||
}
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -69,17 +65,19 @@ export default function RootLayout({
|
||||
>
|
||||
<body className="min-h-screen bg-background font-sans antialiased">
|
||||
<Providers>
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={b.defaultTheme}
|
||||
enableSystem={false}
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
<BrandProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={b.defaultTheme}
|
||||
enableSystem={false}
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</BrandProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,19 +3,12 @@
|
||||
import Image from "next/image"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { getBrand, type BrandConfig } from "@/lib/brand"
|
||||
import { getBrand } from "@/lib/brand"
|
||||
import { useBrand } from "@/components/brand-provider"
|
||||
|
||||
// Hook to get brand configuration with client-side detection
|
||||
export function useBrand() {
|
||||
const [brand, setBrand] = useState<BrandConfig>(getBrand())
|
||||
|
||||
useEffect(() => {
|
||||
// Re-check brand on client side (for domain detection)
|
||||
setBrand(getBrand())
|
||||
}, [])
|
||||
|
||||
return brand
|
||||
}
|
||||
// Re-export the brand hook so existing importers (`@/components/brand-logo`)
|
||||
// keep working. Single source of truth lives in brand-provider.tsx.
|
||||
export { useBrand }
|
||||
|
||||
// Brand Logo Component - theme-aware and white-label ready
|
||||
export function BrandLogo({
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
// BrandProvider — loads the per-deploy brand catalog at runtime and exposes the
|
||||
// resolved BrandConfig to the tree. Mirrors the @hanzo/id SPA's App-level load:
|
||||
// a `useEffect` reads the injected `window.__BRAND_CATALOG__` global and/or
|
||||
// fetches `/config.json`, applies it, then re-resolves the active brand by host.
|
||||
//
|
||||
// Static export note: the prerendered HTML is brand-neutral (built-in bootnode
|
||||
// defaults). Brand applies CLIENT-SIDE after hydration once the runtime catalog
|
||||
// is available — exactly the id SPA behaviour.
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
import { applyBrandCatalog, getBrand, type BrandConfig } from "@/lib/brand"
|
||||
|
||||
const BrandContext = createContext<BrandConfig>(getBrand())
|
||||
|
||||
export function BrandProvider({ children }: { children: React.ReactNode }) {
|
||||
// Start from the synchronous resolve (injected global if the deploy inlined
|
||||
// it before hydration; else the neutral built-in default).
|
||||
const [brand, setBrand] = useState<BrandConfig>(() => getBrand())
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
// 1) Ingest any deploy-injected global synchronously (no flash if present).
|
||||
const injected = (window as unknown as { __BRAND_CATALOG__?: unknown }).__BRAND_CATALOG__
|
||||
if (injected) {
|
||||
applyBrandCatalog(injected)
|
||||
setBrand(getBrand())
|
||||
}
|
||||
|
||||
// 2) Also fetch /config.json (the deploy-provisioned catalog) so the app
|
||||
// works when the catalog is served as a file rather than inlined.
|
||||
fetch("/config.json", { cache: "no-store" })
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((cat) => {
|
||||
if (cancelled || !cat) return
|
||||
applyBrandCatalog(cat)
|
||||
setBrand(getBrand())
|
||||
})
|
||||
.catch(() => {
|
||||
/* no catalog provisioned — keep the built-in neutral default */
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <BrandContext.Provider value={brand}>{children}</BrandContext.Provider>
|
||||
}
|
||||
|
||||
// Brand hook — context-backed so consumers re-render when the runtime catalog
|
||||
// loads. Same return shape as before (a BrandConfig).
|
||||
export function useBrand(): BrandConfig {
|
||||
return useContext(BrandContext)
|
||||
}
|
||||
+133
-252
@@ -1,5 +1,21 @@
|
||||
// White-label branding configuration
|
||||
// Configure via environment variables for different deployments
|
||||
// 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
|
||||
@@ -46,274 +62,139 @@ export type BrandConfig = {
|
||||
}[]
|
||||
}
|
||||
|
||||
// Brand presets
|
||||
const brands: Record<string, BrandConfig> = {
|
||||
bootnode: {
|
||||
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/bootnode-logo.svg",
|
||||
logoIcon: "/logo/bootnode-icon.svg",
|
||||
logoWhite: "/logo/bootnode-logo-white.svg",
|
||||
favicon: "/logo/bootnode-icon.svg",
|
||||
domain: "bootno.de",
|
||||
apiUrl: "https://api.bootno.de",
|
||||
wsUrl: "wss://ws.bootno.de",
|
||||
statusUrl: "https://status.bootno.de",
|
||||
skillsOrg: "bootnode",
|
||||
defaultTheme: "light",
|
||||
colors: {
|
||||
primary: "#000000",
|
||||
primaryForeground: "#ffffff",
|
||||
accent: "#3b82f6", // blue-500
|
||||
accentEnd: "#06b6d4", // cyan-500
|
||||
},
|
||||
social: {
|
||||
twitter: "https://twitter.com/bootnode",
|
||||
github: "https://github.com/bootnode",
|
||||
},
|
||||
iam: {
|
||||
url: "https://hanzo.id",
|
||||
clientId: "bootnode-platform",
|
||||
domain: "hanzo.id",
|
||||
},
|
||||
footerLinks: [
|
||||
{ name: "GitHub", href: "https://github.com/bootnode", external: true },
|
||||
{ name: "Twitter", href: "https://twitter.com/bootnode", external: true },
|
||||
],
|
||||
ecosystemApps: [],
|
||||
},
|
||||
hanzo: {
|
||||
name: "Hanzo Web3",
|
||||
tagline: "Enterprise Blockchain Infrastructure",
|
||||
description: "Enterprise blockchain infrastructure powered by Hanzo AI. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, and more.",
|
||||
logo: "/logo/hanzo-logo.svg",
|
||||
logoIcon: "/logo/hanzo-icon.svg",
|
||||
logoWhite: "/logo/hanzo-logo-white.svg",
|
||||
favicon: "/logo/hanzo-icon.svg",
|
||||
domain: "web3.hanzo.ai",
|
||||
apiUrl: "https://api.cloud.hanzo.ai",
|
||||
wsUrl: "wss://ws.cloud.hanzo.ai",
|
||||
statusUrl: "https://status.hanzo.ai",
|
||||
skillsOrg: "hanzoai",
|
||||
defaultTheme: "dark",
|
||||
colors: {
|
||||
primary: "#fd4444",
|
||||
primaryForeground: "#ffffff",
|
||||
accent: "#fd4444", // hanzo red
|
||||
accentEnd: "#ff6b6b", // lighter red
|
||||
},
|
||||
social: {
|
||||
twitter: "https://twitter.com/hanaboratory",
|
||||
github: "https://github.com/hanzoai",
|
||||
discord: "https://discord.gg/hanzo",
|
||||
},
|
||||
iam: {
|
||||
url: "https://hanzo.id",
|
||||
clientId: "hanzo-web3",
|
||||
domain: "hanzo.id",
|
||||
},
|
||||
footerLinks: [
|
||||
{ name: "Hanzo AI", href: "https://hanzo.ai", external: true },
|
||||
{ name: "Hanzo Cloud", href: "https://cloud.hanzo.ai", external: true },
|
||||
{ name: "Hanzo Chat", href: "https://chat.hanzo.ai", external: true },
|
||||
{ name: "Hanzo Network", href: "https://hanzo.network", external: true },
|
||||
{ name: "GitHub", href: "https://github.com/hanzoai", external: true },
|
||||
],
|
||||
ecosystemApps: [
|
||||
{ name: "Hanzo Chat", href: "https://chat.hanzo.ai", description: "AI chat with 100+ models", icon: "MessageSquare", category: "tools" },
|
||||
{ name: "Hanzo Console", href: "https://console.hanzo.ai", description: "LLM observability & evaluation", icon: "BarChart3", category: "tools" },
|
||||
],
|
||||
},
|
||||
lux: {
|
||||
name: "Lux Cloud",
|
||||
tagline: "Next-Gen Blockchain Infrastructure",
|
||||
description: "High-performance blockchain infrastructure for the Lux Network ecosystem.",
|
||||
logo: "/logo/lux-wordmark-dark.svg",
|
||||
logoIcon: "/logo/lux-icon.svg",
|
||||
logoWhite: "/logo/lux-wordmark-white.svg",
|
||||
favicon: "/logo/lux-icon.svg",
|
||||
domain: "cloud.lux.network",
|
||||
apiUrl: "https://api.cloud.lux.network",
|
||||
wsUrl: "wss://ws.cloud.lux.network",
|
||||
statusUrl: "https://status.lux.network",
|
||||
skillsOrg: "luxfi",
|
||||
defaultTheme: "dark",
|
||||
colors: {
|
||||
primary: "#ffffff",
|
||||
primaryForeground: "#000000",
|
||||
accent: "#a8a8a8", // monochrome silver
|
||||
accentEnd: "#ffffff", // white
|
||||
},
|
||||
social: {
|
||||
twitter: "https://twitter.com/luxdefi",
|
||||
github: "https://github.com/luxfi",
|
||||
},
|
||||
iam: {
|
||||
url: "https://lux.id",
|
||||
clientId: "lux-cloud",
|
||||
domain: "lux.id",
|
||||
},
|
||||
footerLinks: [
|
||||
{ name: "Lux Network", href: "https://lux.network", external: true },
|
||||
{ name: "Lux Explorer", href: "https://explore.lux.network", external: true },
|
||||
{ name: "Lux Bridge", href: "https://bridge.lux.network", external: true },
|
||||
{ name: "Lux Wallet", href: "https://wallet.lux.network", external: true },
|
||||
{ name: "Lux Exchange", href: "https://exchange.lux.network", external: true },
|
||||
{ name: "GitHub", href: "https://github.com/luxfi", external: true },
|
||||
],
|
||||
ecosystemApps: [
|
||||
{ name: "Explorer", href: "https://explore.lux.network", description: "Block explorer for all Lux chains", icon: "Search", category: "explore" },
|
||||
{ name: "Bridge", href: "https://bridge.lux.network", description: "Cross-chain asset bridge", icon: "ArrowLeftRight", category: "defi" },
|
||||
{ name: "Exchange", href: "https://exchange.lux.network", description: "Decentralized exchange (DEX)", icon: "ArrowUpDown", category: "defi" },
|
||||
{ name: "Markets", href: "https://markets.lux.network", description: "NFT marketplace", icon: "ShoppingBag", category: "defi" },
|
||||
{ name: "Wallet", href: "https://wallet.lux.network", description: "Multi-chain wallet", icon: "Wallet", category: "tools" },
|
||||
{ name: "Safe", href: "https://safe.lux.network", description: "Multisig wallet (FROST)", icon: "Shield", category: "tools" },
|
||||
{ name: "Faucet", href: "https://faucet.lux.network", description: "Testnet token faucet", icon: "Droplets", category: "tools" },
|
||||
{ name: "Staking", href: "https://stake.lux.network", description: "Validator staking", icon: "Coins", category: "defi" },
|
||||
{ name: "Governance", href: "https://vote.lux.network", description: "On-chain governance", icon: "Vote", category: "governance" },
|
||||
{ name: "Status", href: "https://status.lux.network", description: "Network status & uptime", icon: "Activity", category: "explore" },
|
||||
{ name: "Lux Build", href: "https://lux.build", description: "Developer portal & docs", icon: "Code", category: "tools" },
|
||||
],
|
||||
},
|
||||
pars: {
|
||||
name: "Pars Cloud",
|
||||
tagline: "Sovereign L1 Infrastructure",
|
||||
description: "Sovereign L1 blockchain infrastructure for the Pars Network. Post-quantum secure, SessionVM messaging, native DEX, and more.",
|
||||
logo: "/logo/pars-logo.svg",
|
||||
logoIcon: "/logo/pars-icon.svg",
|
||||
logoWhite: "/logo/pars-logo-white.svg",
|
||||
favicon: "/logo/pars-icon.svg",
|
||||
domain: "cloud.pars.network",
|
||||
apiUrl: "https://api.cloud.pars.network",
|
||||
wsUrl: "wss://ws.cloud.pars.network",
|
||||
statusUrl: "https://status.pars.network",
|
||||
skillsOrg: "parsdao",
|
||||
defaultTheme: "dark",
|
||||
colors: {
|
||||
primary: "#00abff",
|
||||
primaryForeground: "#ffffff",
|
||||
accent: "#00abff", // persian blue
|
||||
accentEnd: "#F59E0B", // gold accent
|
||||
},
|
||||
social: {
|
||||
twitter: "https://twitter.com/parsdao",
|
||||
github: "https://github.com/pars-network",
|
||||
},
|
||||
iam: {
|
||||
url: "https://pars.id",
|
||||
clientId: "pars-app-client-id",
|
||||
domain: "pars.id",
|
||||
},
|
||||
footerLinks: [
|
||||
{ name: "Pars Network", href: "https://pars.network", external: true },
|
||||
{ name: "Pars Explorer", href: "https://explore.pars.network", external: true },
|
||||
{ name: "Pars Wallet", href: "https://wallet.pars.network", external: true },
|
||||
{ name: "SessionVM", href: "https://session.pars.network", external: true },
|
||||
{ name: "GitHub", href: "https://github.com/pars-network", external: true },
|
||||
],
|
||||
ecosystemApps: [
|
||||
{ name: "Explorer", href: "https://explore.pars.network", description: "Pars block explorer", icon: "Search", category: "explore" },
|
||||
{ name: "Wallet", href: "https://wallet.pars.network", description: "Pars wallet", icon: "Wallet", category: "tools" },
|
||||
{ name: "SessionVM", href: "https://session.pars.network", description: "Private messaging VM", icon: "MessageCircle", category: "tools" },
|
||||
{ name: "Faucet", href: "https://faucet.pars.network", description: "Testnet tokens", icon: "Droplets", category: "tools" },
|
||||
],
|
||||
},
|
||||
zoo: {
|
||||
name: "Zoo Labs",
|
||||
tagline: "Decentralized AI Infrastructure",
|
||||
description: "Blockchain infrastructure for decentralized AI and science research.",
|
||||
logo: "/logo/zoo-logo.svg",
|
||||
logoIcon: "/logo/zoo-icon.svg",
|
||||
logoWhite: "/logo/zoo-logo-white.svg",
|
||||
favicon: "/logo/zoo-icon.svg",
|
||||
domain: "web3.zoo.ngo",
|
||||
apiUrl: "https://api.web3.zoo.ngo",
|
||||
wsUrl: "wss://ws.web3.zoo.ngo",
|
||||
statusUrl: "https://status.zoo.ngo",
|
||||
skillsOrg: "zooai",
|
||||
defaultTheme: "dark",
|
||||
colors: {
|
||||
primary: "#00cc66",
|
||||
primaryForeground: "#ffffff",
|
||||
accent: "#00cc66", // emerald green
|
||||
accentEnd: "#10b981", // lighter green
|
||||
},
|
||||
social: {
|
||||
twitter: "https://twitter.com/zoolabs",
|
||||
github: "https://github.com/zoolabs",
|
||||
},
|
||||
iam: {
|
||||
// Casdoor app for Zoo (org "zoo"). Resolves via get-app-login and its
|
||||
// redirectUris already include https://zoo.cloud/* and zoolabs.id/callback.
|
||||
// The previous value "zoo-app-client-id" is not a registered Casdoor
|
||||
// client and returns "Invalid client_id", breaking the zoo.cloud login.
|
||||
url: "https://zoolabs.id",
|
||||
clientId: "zoo-console",
|
||||
domain: "zoolabs.id",
|
||||
},
|
||||
footerLinks: [
|
||||
{ name: "Zoo Labs", href: "https://zoo.ngo", external: true },
|
||||
{ name: "Zoo AI Chat", href: "https://ai.zoo.ngo", external: true },
|
||||
{ name: "Zoo Network", href: "https://zoo.network", external: true },
|
||||
{ name: "Zen LM", href: "https://zenlm.ai", external: true },
|
||||
{ name: "GitHub", href: "https://github.com/zooai", external: true },
|
||||
],
|
||||
ecosystemApps: [
|
||||
{ name: "Zoo AI Chat", href: "https://ai.zoo.ngo", description: "Decentralized AI chat", icon: "Bot", category: "tools" },
|
||||
{ name: "Zen LM", href: "https://zenlm.ai", description: "Open frontier models", icon: "Brain", category: "tools" },
|
||||
{ name: "Explorer", href: "https://explore.zoo.ngo", description: "Zoo chain explorer", icon: "Search", category: "explore" },
|
||||
{ name: "Faucet", href: "https://faucet.zoo.ngo", description: "Testnet tokens", icon: "Droplets", category: "tools" },
|
||||
],
|
||||
},
|
||||
// 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
|
||||
}
|
||||
|
||||
// Get brand from domain auto-detection or environment fallback
|
||||
// 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",
|
||||
skillsOrg: "bootnode",
|
||||
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 {
|
||||
// Client-side: detect from domain (highest priority)
|
||||
if (typeof window !== "undefined") {
|
||||
const hostname = window.location.hostname
|
||||
if (hostname.includes("lux.cloud") || hostname.includes("lux.network")) return "lux"
|
||||
if (hostname.includes("pars.network") || hostname.includes("pars.id")) return "pars"
|
||||
if (hostname.includes("zoo.ngo") || hostname.includes("zoo.network") || hostname.includes("zoo.cloud")) return "zoo"
|
||||
if (hostname.includes("hanzo.ai")) return "hanzo"
|
||||
if (hostname.includes("bootno.de") || hostname.includes("bootnode.io")) return "bootnode"
|
||||
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
|
||||
}
|
||||
|
||||
// Server-side: BRAND env var (runtime, not baked by Next.js unlike NEXT_PUBLIC_*)
|
||||
const runtimeBrand = process.env.BRAND?.toLowerCase()
|
||||
if (runtimeBrand && brands[runtimeBrand]) {
|
||||
return runtimeBrand
|
||||
}
|
||||
|
||||
// Fall back to build-time NEXT_PUBLIC_BRAND
|
||||
const envBrand = process.env.NEXT_PUBLIC_BRAND?.toLowerCase()
|
||||
if (envBrand && brands[envBrand]) {
|
||||
return envBrand
|
||||
}
|
||||
|
||||
return "bootnode"
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Export the active brand configuration
|
||||
// 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 {
|
||||
return brands[getBrandKey()]
|
||||
const cat = activeCatalog()
|
||||
const key = getBrandKey()
|
||||
return cat.brands?.[key] ?? cat.brands?.[DEFAULT_KEY] ?? BOOTNODE_DEFAULT
|
||||
}
|
||||
|
||||
// Export brand key for conditional rendering
|
||||
// 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 using Hanzo branding
|
||||
// Check if the active brand is the Hanzo brand.
|
||||
export function isHanzoBrand(): boolean {
|
||||
return getBrandKey() === "hanzo"
|
||||
}
|
||||
|
||||
// Get all available brands (for admin/testing)
|
||||
// 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 brands
|
||||
return activeCatalog().brands ?? { [DEFAULT_KEY]: BOOTNODE_DEFAULT }
|
||||
}
|
||||
|
||||
// Default export for convenience
|
||||
// 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()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
sharp: true
|
||||
@@ -6,3 +6,6 @@
|
||||
|
||||
/_next/static/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/config.json
|
||||
Cache-Control: no-store, must-revalidate
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"_comment": "Runtime brand catalog — NEUTRAL PLACEHOLDER. The deploy (universe / CF-Pages) OVERWRITES this file (and/or injects the same JSON as window.__BRAND_CATALOG__) with the full multi-brand catalog. Shipping empty `brands` makes the app fall back to bootnode's built-in neutral default. Shape mirrors the @hanzo/id SPA's /config.json contract. See lib/brand.ts (BrandCatalog / BrandConfig).",
|
||||
"defaultKey": "bootnode",
|
||||
"hosts": [],
|
||||
"brands": {}
|
||||
}
|
||||
Reference in New Issue
Block a user