fix(brand): RootLayout brand also detected from Host header

generateMetadata() already used the host header, but RootLayout itself
read only process.env.BRAND with a hard-coded "bootnode" fallback. That
made the body theme/SSR brand drift from the metadata brand on per-host
deploys. Make RootLayout async and reuse the host detection so both
paths agree.
This commit is contained in:
2026-05-01 20:29:24 -07:00
parent 912d4a6475
commit 8d5fe3bfe7
+19 -11
View File
@@ -17,10 +17,12 @@ const brands: Record<string, { name: string; tagline: string; domain: string; ic
zoo: { name: "Zoo Labs", tagline: "Decentralized AI Infrastructure", domain: "web3.zoo.ngo", icon: "/logo/zoo-icon.svg", defaultTheme: "dark" },
}
// Detect brand from Host header first, then BRAND env var, then fallback.
// IMPORTANT: Do NOT use NEXT_PUBLIC_BRAND here -- it gets inlined at build time
// by Next.js and would always resolve to the Dockerfile default ("bootnode").
function getServerBrandKeyFromHost(host: string | null): string {
// Detect brand from Host header. Returns null when no host match (caller
// then falls back to BRAND/NEXT_PUBLIC_BRAND env vars).
// IMPORTANT: Do NOT bake the env-var fallback in here — `dynamic = "force-dynamic"`
// makes layout requests runtime, so callers should read process.env directly
// at request time and avoid relying on this helper for the env-var path.
function getServerBrandKeyFromHost(host: string | null): string | null {
if (host) {
if (host.includes("lux.network") || host.includes("lux.cloud")) return "lux"
if (host.includes("pars.network") || host.includes("pars.id")) return "pars"
@@ -28,12 +30,17 @@ function getServerBrandKeyFromHost(host: string | null): string {
if (host.includes("hanzo.ai")) return "hanzo"
if (host.includes("bootno.de") || host.includes("bootnode.io")) return "bootnode"
}
return (process.env.BRAND || "bootnode").toLowerCase()
return null
}
function getServerBrand() {
// Used only by RootLayout (non-async). Falls back to BRAND env var.
const key = (process.env.BRAND || "bootnode").toLowerCase()
async function getServerBrand() {
// Prefer Host header; fall back to BRAND or NEXT_PUBLIC_BRAND env vars.
// dynamic = "force-dynamic" plus headers() forces request-time evaluation,
// so process.env reads pick up runtime values, not the Dockerfile default.
const hdrs = await headers()
const host = hdrs.get("host")
const key = getServerBrandKeyFromHost(host) ||
(process.env.BRAND || process.env.NEXT_PUBLIC_BRAND || "bootnode").toLowerCase()
return brands[key] || brands.bootnode
}
@@ -44,7 +51,8 @@ export async function generateMetadata(): Promise<Metadata> {
// Calling headers() forces Next.js to evaluate this at request time, never at build time.
const hdrs = await headers()
const host = hdrs.get("host")
const key = getServerBrandKeyFromHost(host)
const key = getServerBrandKeyFromHost(host) ||
(process.env.BRAND || process.env.NEXT_PUBLIC_BRAND || "bootnode").toLowerCase()
const b = brands[key] || brands.bootnode
return {
title: {
@@ -75,12 +83,12 @@ export async function generateMetadata(): Promise<Metadata> {
}
}
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const b = getServerBrand()
const b = await getServerBrand()
return (
<html
lang="en"