fix: product subdomain cleanup

This commit is contained in:
2026-07-23 20:52:49 -04:00
parent a6ef4d2955
commit 8e33af43a5
39 changed files with 1597 additions and 630 deletions

View File

@@ -16,13 +16,7 @@ import {
} from "./nav-config";
import { SITE_CONFIG, type SiteId } from "./site-context";
const ALL_SITES: SiteId[] = [
"main",
"nessa",
"lineage",
"gaze",
"inputhalo"
];
const ALL_SITES: SiteId[] = ["main", "nessa", "lineage", "gaze", "inputhalo"];
describe("NAV_CONFIG — per-site link sets", () => {
it("main → Home, Blog, Downloads, Resume, Contact, GitHub, LinkedIn", () => {
@@ -55,12 +49,22 @@ describe("NAV_CONFIG — per-site link sets", () => {
]);
});
it("gaze → Home, Contact, Privacy", () => {
expect(navLabelsFor("gaze")).toEqual(["Home", "Contact", "Privacy"]);
it("gaze → Home, Contact, Privacy, Downloads", () => {
expect(navLabelsFor("gaze")).toEqual([
"Home",
"Contact",
"Privacy",
"Downloads"
]);
});
it("inputhalo → Home, Contact, Privacy", () => {
expect(navLabelsFor("inputhalo")).toEqual(["Home", "Contact", "Privacy"]);
it("inputhalo → Home, Contact, Privacy, Downloads", () => {
expect(navLabelsFor("inputhalo")).toEqual([
"Home",
"Contact",
"Privacy",
"Downloads"
]);
});
});

View File

@@ -69,8 +69,8 @@ export const BACK_TO_FRENO: NavItem = {
* - main: Home, Blog, Downloads, Resume, Contact, GitHub, LinkedIn
* - nessa: Home, Contact, Privacy
* - lineage: Home, Downloads, Contact, Privacy, Account Deletion
* - gaze: Home, Contact, Privacy
* - inputhalo: Home, Contact, Privacy
* - gaze: Home, Contact, Privacy, Downloads
* - inputhalo: Home, Contact, Privacy, Downloads
*/
export const NAV_CONFIG: Record<SiteId, NavItem[]> = {
main: [
@@ -107,12 +107,14 @@ export const NAV_CONFIG: Record<SiteId, NavItem[]> = {
gaze: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" }
{ label: "Privacy", href: "/privacy", icon: "privacy" },
{ label: "Downloads", href: "/downloads", icon: "downloads" }
],
inputhalo: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" }
{ label: "Privacy", href: "/privacy", icon: "privacy" },
{ label: "Downloads", href: "/downloads", icon: "downloads" }
]
};

View File

@@ -8,6 +8,8 @@
import { describe, it, expect } from "bun:test";
import {
resolveSiteFromHost,
resolveSiteFromPath,
resolveSiteFromLocation,
SITE_CONFIG,
type SiteId
} from "./site-context";
@@ -80,15 +82,12 @@ describe("resolveSiteFromHost", () => {
});
it("every SITE_CONFIG entry has a non-empty baseRoutePrefix for subdomains", () => {
for (const id of [
"nessa",
"lineage",
"gaze",
"inputhalo"
] as SiteId[]) {
for (const id of ["nessa", "lineage", "gaze", "inputhalo"] as SiteId[]) {
expect(SITE_CONFIG[id].baseRoutePrefix).toBe(`/${id}`);
expect(SITE_CONFIG[id].subdomain).toBe(id);
expect(SITE_CONFIG[id].titleSuffix).toBe(` | ${SITE_CONFIG[id].displayName}`);
expect(SITE_CONFIG[id].titleSuffix).toBe(
` | ${SITE_CONFIG[id].displayName}`
);
}
});
@@ -97,3 +96,67 @@ describe("resolveSiteFromHost", () => {
expect(SITE_CONFIG.main.baseRoutePrefix).toBe("");
});
});
describe("resolveSiteFromPath", () => {
it("maps a subdomain-prefixed path to the right site", () => {
expect(resolveSiteFromPath("/nessa/contact")?.id).toBe("nessa");
expect(resolveSiteFromPath("/lineage/downloads")?.id).toBe("lineage");
expect(resolveSiteFromPath("/gaze/")?.id).toBe("gaze");
expect(resolveSiteFromPath("/inputhalo/privacy")?.id).toBe("inputhalo");
});
it("matches the exact prefix (landing page)", () => {
expect(resolveSiteFromPath("/nessa")?.id).toBe("nessa");
expect(resolveSiteFromPath("/gaze")?.id).toBe("gaze");
});
it("returns null for non-subdomain paths", () => {
expect(resolveSiteFromPath("/")).toBeNull();
expect(resolveSiteFromPath("/contact")).toBeNull();
expect(resolveSiteFromPath("/blog/post")).toBeNull();
expect(resolveSiteFromPath("/downloads")).toBeNull();
});
it("does not match prefix substrings (no false positives)", () => {
// `/nessa-extra` must NOT match `/nessa`.
expect(resolveSiteFromPath("/nessa-extra")).toBeNull();
expect(resolveSiteFromPath("/gazette")).toBeNull();
});
it("handles empty / null / undefined", () => {
expect(resolveSiteFromPath("")).toBeNull();
expect(resolveSiteFromPath(null)).toBeNull();
expect(resolveSiteFromPath(undefined)).toBeNull();
});
it("returns the full SITE_CONFIG entry", () => {
expect(resolveSiteFromPath("/nessa/contact")).toEqual(SITE_CONFIG.nessa);
expect(resolveSiteFromPath("/gaze")).toEqual(SITE_CONFIG.gaze);
});
});
describe("resolveSiteFromLocation", () => {
it("prefers the host when it identifies a subdomain", () => {
expect(resolveSiteFromLocation("nessa.freno.me", "/contact").id).toBe(
"nessa"
);
expect(resolveSiteFromLocation("gaze.localhost", "/").id).toBe("gaze");
});
it("falls back to the path prefix when the host is localhost/main", () => {
expect(resolveSiteFromLocation("localhost", "/nessa/contact").id).toBe(
"nessa"
);
expect(resolveSiteFromLocation("localhost", "/lineage/").id).toBe(
"lineage"
);
expect(resolveSiteFromLocation("freno.me", "/gaze/downloads").id).toBe(
"gaze"
);
});
it("returns main when neither host nor path identifies a subdomain", () => {
expect(resolveSiteFromLocation("localhost", "/contact").id).toBe("main");
expect(resolveSiteFromLocation("freno.me", "/").id).toBe("main");
});
});

View File

@@ -159,13 +159,54 @@ export function resolveSiteFromHost(host: string | null | undefined): Site {
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 `<SubdomainHeader>` 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
hostname: string | null | undefined,
pathname?: string | null | undefined
): Site {
return resolveSiteFromHost(hostname);
const hostResult = resolveSiteFromHost(hostname);
if (hostResult.id !== "main") return hostResult;
return resolveSiteFromPath(pathname) ?? hostResult;
}