fix: product subdomain cleanup
This commit is contained in:
@@ -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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user