/** * Shared site definitions and host-to-site resolver. * * Near-pure module — reads `import.meta.env.VITE_DOMAIN` (a Vite build-time * var available on both client and server) to derive `BASE_DOMAIN`, but * imports NO server-only code so it remains safe to import from client, * server, and unit tests (with a fallback when the env var is absent). * * This is the keystone of the subdomain-routing feature. Every * content module consumes `SITE_CONFIG` metadata via `useSite()`, * and the server-side host detection in * `src/server/site-context-server.ts` builds on `resolveSiteFromHost`. */ export type SiteId = "main" | "nessa" | "lineage" | "gaze" | "inputhalo"; export interface Site { /** Canonical id, also serialized into `` and `window.__SITE__`. */ id: SiteId; /** Subdomain label, e.g. `"nessa"`. Empty string for the main site. */ subdomain: string; /** Fully-qualified domain, e.g. `"nessa.freno.me"`. `"freno.me"` for main. */ domain: string; /** * Internal route prefix the vercel.json host rewrite targets. SolidStart * file-routing places subdomain pages under `src/routes//*`. * Empty string for main. */ baseRoutePrefix: string; /** Human-friendly brand / product name. */ displayName: string; /** Appended to page titles, e.g. `" | Nessa"`. */ titleSuffix: string; /** Hex brand color used for theming accents / OG image backgrounds. */ brandColor: string; /** Dark mode variant of the brand color (used when dark mode is active). */ brandColorDark?: string; /** Whether the subdomain header should use a solid background (default: false for transparent). */ headerOpaque?: boolean; /** Default OpenGraph image path (resolved against the site root). */ ogDefaultImage: string; /** Favicon path for this site. */ faviconPath: string; } /** * Derive the base domain from `VITE_DOMAIN`. * * `VITE_DOMAIN` is `http://localhost:3000` in dev and `https://freno.me` * (or `.dev`) in prod. We extract the hostname so host matching works * against whichever apex the deployment uses. Falls back to `"freno.me"` * when the env var is absent (unit tests) or points at `localhost` (dev — * where subdomain host matching isn't used anyway; the path-prefix fallback * in `resolveSiteFromLocation` handles dev). */ function computeBaseDomain(): string { try { const v = (import.meta as { env?: Record }).env ?.VITE_DOMAIN; if (!v) return "freno.me"; const hostname = new URL(v).hostname; return hostname === "localhost" ? "freno.me" : hostname; } catch { return "freno.me"; } } /** The apex hostname derived from `VITE_DOMAIN` (e.g. `"freno.me"`). */ export const BASE_DOMAIN = computeBaseDomain(); export const SITE_CONFIG: Record = { main: { id: "main", subdomain: "", domain: BASE_DOMAIN, baseRoutePrefix: "", displayName: "Michael Freno", titleSuffix: " | Michael Freno", brandColor: "#89b4fa", ogDefaultImage: "/blueprint.jpg", faviconPath: "/favicon.ico" }, nessa: { id: "nessa", subdomain: "nessa", domain: `nessa.${BASE_DOMAIN}`, baseRoutePrefix: "/nessa", displayName: "Nessa", titleSuffix: " | Nessa", brandColor: "#527640", brandColorDark: "#6CA86C", headerOpaque: true, ogDefaultImage: "/nessa/og-default.png", faviconPath: "/nessa/favicon.ico" }, lineage: { id: "lineage", subdomain: "lineage", domain: `lineage.${BASE_DOMAIN}`, baseRoutePrefix: "/lineage", displayName: "Life and Lineage", titleSuffix: " | Life and Lineage", brandColor: "#a6e3a1", ogDefaultImage: "/lineage/og-default.png", faviconPath: "/lineage/favicon.ico" }, gaze: { id: "gaze", subdomain: "gaze", domain: `gaze.${BASE_DOMAIN}`, baseRoutePrefix: "/gaze", displayName: "Gaze", titleSuffix: " | Gaze", brandColor: "#f9e2af", ogDefaultImage: "/gaze/og-default.png", faviconPath: "/gaze/favicon.ico" }, inputhalo: { id: "inputhalo", subdomain: "inputhalo", domain: `inputhalo.${BASE_DOMAIN}`, baseRoutePrefix: "/inputhalo", displayName: "InputHalo", titleSuffix: " | InputHalo", brandColor: "#f38ba8", ogDefaultImage: "/inputhalo/og-default.png", faviconPath: "/inputhalo/favicon.ico" } }; /** Ordered subdomain sites used for host matching. */ const SUBDOMAIN_SITES: ReadonlyArray = [ SITE_CONFIG.nessa, SITE_CONFIG.lineage, SITE_CONFIG.gaze, SITE_CONFIG.inputhalo ]; /** Matches `.localhost` and `.localhost:` (dev only). */ const DEV_HOST_RE = /^([a-z0-9-]+)\.localhost$/i; export const MAIN_SITE: Site = SITE_CONFIG.main; /** * Resolve a `Site` from a raw `Host` header value (or hostname). * * Handles: * - exact product subdomains (`nessa.freno.me` → nessa) * - `www.` prefix (`www.freno.me` → main) * - the bare apex (`freno.me` → main) * - port suffixes (`freno.me:3000` → main) * - localhost dev (`localhost` / `localhost:3000` → main) * - subdomain dev (`nessa.localhost` / `nessa.localhost:3000` → nessa) * - unknown hosts / unknown subdomains → main (fail-safe default) * * Pure & synchronous — no I/O, no env access. */ export function resolveSiteFromHost(host: string | null | undefined): Site { if (!host) return MAIN_SITE; // Normalize: trim, lowercase, strip optional `:port` suffix. const normalized = host.trim().toLowerCase().replace(/:\d+$/, ""); if (!normalized) return MAIN_SITE; // Strip a leading `www.` so `www.freno.me` behaves like `freno.me`. const withoutWww = normalized.replace(/^www\./, ""); if (withoutWww === BASE_DOMAIN) return MAIN_SITE; // Exact subdomain. match. for (const site of SUBDOMAIN_SITES) { if (withoutWww === `${site.subdomain}.${BASE_DOMAIN}`) return site; } // Dev pattern: .localhost[:port] (browsers resolve `*.localhost`). const devMatch = normalized.match(DEV_HOST_RE); if (devMatch) { const sub = devMatch[1]!.toLowerCase(); for (const site of SUBDOMAIN_SITES) { if (sub === site.subdomain) return site; } // `localhost` alone or unknown `.localhost` → main. return MAIN_SITE; } // Unknown `*.freno.me` (e.g. a future subdomain not yet configured) → main. if (withoutWww.endsWith(`.${BASE_DOMAIN}`)) return MAIN_SITE; // Anything else entirely (IPs, foreign hosts) → main as a safe default. return MAIN_SITE; } /** * Resolve a `Site` from a URL pathname by matching a known subdomain route * prefix (e.g. `/nessa/contact` → nessa, `/gaze/downloads` → gaze). * * Returns `null` when the path does not begin with a subdomain prefix so the * caller can distinguish "no match" from "main" and decide whether to fall * back to the host-based result. * * This is the dev-server safety net: on `localhost:3000/nessa/contact` the * Host header is `localhost` (→ main), but the URL path still carries the * subdomain prefix because vercel.json host rewrites do not run locally. * Without this fallback, `useSite()` returns `main` on every subdomain page * in dev, causing `` to render the main-site nav. * * In production the host rewrite strips the prefix, so the path is `/contact` * (no prefix) and this returns `null` — the host-based result already * resolved correctly. * * Pure & synchronous — no I/O, no env access. */ export function resolveSiteFromPath( pathname: string | null | undefined ): Site | null { if (!pathname) return null; for (const site of SUBDOMAIN_SITES) { const prefix = site.baseRoutePrefix; // e.g. "/nessa" // Exact prefix (`/nessa`) or prefix + `/` (`/nessa/contact`). if (pathname === prefix || pathname.startsWith(`${prefix}/`)) { return site; } } return null; } /** * Resolve the active site from a client `window.location`, used by the * SolidJS `SiteContext` provider during hydration. Server codepaths should * use `getSiteFromEvent` / `getSiteFromRequest` instead. * * When the hostname does not identify a subdomain (e.g. `localhost` in dev), * falls back to checking the URL pathname for a subdomain route prefix so * that `localhost:3000/nessa/contact` resolves to nessa. */ export function resolveSiteFromLocation( hostname: string | null | undefined, pathname?: string | null | undefined ): Site { const hostResult = resolveSiteFromHost(hostname); if (hostResult.id !== "main") return hostResult; return resolveSiteFromPath(pathname) ?? hostResult; } // ───────────────────────────────────────────────────────────────────────── // URL builders — derive full URLs from VITE_DOMAIN (no ~/env/client import // so this module stays safe for unit tests / pure content modules). //───────────────────────────────────────────────────────────────────────── /** Compute the site origin from VITE_DOMAIN (fallback for tests). */ const SITE_ORIGIN = (() => { try { const v = (import.meta as { env?: Record }).env ?.VITE_DOMAIN; return v || "https://freno.me"; } catch { return "https://freno.me"; } })(); /** True when VITE_DOMAIN points at localhost (dev server). */ function isDevOrigin(): boolean { try { return new URL(SITE_ORIGIN).hostname === "localhost"; } catch { return false; } } /** * Build a full URL for a subdomain site. * * - Dev: path-based — `http://localhost:3000/nessa/contact` * (the dev server has no host rewrite, so subdomains live under `//...`) * - Prod: host-based — `https://nessa.freno.me/contact` */ export function buildSubdomainUrl( subdomain: string, path: string = "/" ): string { const normalizedPath = path.startsWith("/") ? path : `/${path}`; if (isDevOrigin()) { const pathSuffix = normalizedPath === "/" ? "" : normalizedPath; return `${SITE_ORIGIN}/${subdomain}${pathSuffix}`; } try { const url = new URL(SITE_ORIGIN); return `${url.protocol}//${subdomain}.${url.hostname}${normalizedPath}`; } catch { return `https://${subdomain}.${BASE_DOMAIN}${normalizedPath}`; } } /** * Build a full URL for the main (apex) site. * * - Dev: `http://localhost:3000/contact` * - Prod: `https://freno.me/contact` */ export function buildMainSiteUrl(path: string = "/"): string { const normalizedPath = path.startsWith("/") ? path : `/${path}`; return `${SITE_ORIGIN}${normalizedPath === "/" ? "" : normalizedPath}`; }