fix: subdomain capture

This commit is contained in:
2026-07-26 09:53:20 -04:00
parent ad5221dc35
commit 5ddbfbc429
6 changed files with 162 additions and 8 deletions

View File

@@ -60,6 +60,42 @@ describe("resolvePageHeadMeta — canonical URL derivation", () => {
expect(meta.canonical).toBe("https://nessa.freno.me/contact");
});
// The in-app subdomain middleware (src/middleware.ts) rewrites the internal
// request path to the prefix form (/nessa/contact) before the SolidStart
// router matches it. useLocation() therefore reports the prefixed path, and
// resolvePageHeadMeta must strip the prefix so the canonical stays public.
it("nessa prefixed /nessa/contact → https://nessa.freno.me/contact", () => {
const meta = resolvePageHeadMeta(
BASE_PROPS,
SITE_CONFIG.nessa,
"/nessa/contact"
);
expect(meta.canonical).toBe("https://nessa.freno.me/contact");
});
it("lineage prefixed /lineage/privacy → https://lineage.freno.me/privacy", () => {
const meta = resolvePageHeadMeta(
BASE_PROPS,
SITE_CONFIG.lineage,
"/lineage/privacy"
);
expect(meta.canonical).toBe("https://lineage.freno.me/privacy");
});
it("nessa prefixed root /nessa/ → https://nessa.freno.me/", () => {
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nessa, "/nessa/");
expect(meta.canonical).toBe("https://nessa.freno.me/");
});
it("main path that happens to start with /nessa is NOT stripped (main site has no prefix)", () => {
const meta = resolvePageHeadMeta(
BASE_PROPS,
SITE_CONFIG.main,
"/nessa/contact"
);
expect(meta.canonical).toBe("https://freno.me/nessa/contact");
});
it("lineage → canonical starts with https://lineage.freno.me", () => {
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.lineage, "/");
expect(meta.canonical.startsWith("https://lineage.freno.me")).toBe(true);

View File

@@ -9,11 +9,12 @@
* Rules:
* - `title` → `props.title + site.titleSuffix`
* - `canonical` → explicit `props.canonical` override wins; otherwise
* `https://${site.domain}${pathname}`. The pathname is the *browser* path
* (from `useLocation`), which is correct because vercel.json host rewrites
* target internal route prefixes (`/nessa`, `/lineage`, …) while leaving
* the public URL intact — so `nessa.freno.me/contact` reports pathname
* `/contact`, and the canonical is `https://nessa.freno.me/contact`.
* `https://${site.domain}${pathname}` where `pathname` is the *public*
* browser path. Because the subdomain prefix (`/lineage`, `/nessa`, …) is
* an internal-only rewrite (applied by `vercel.json` host rewrites or, in
* their absence, by `src/middleware.ts`), `useLocation()` reports the
* prefixed path (`/lineage/privacy`) and we strip the prefix back off so
* the canonical reflects the public URL (`https://lineage.freno.me/privacy`).
* - `ogImage` → explicit `props.ogImage` wins; otherwise `site.ogDefaultImage`.
* - `ogTitle` / `ogDescription` → explicit override wins; otherwise fall
* back to the base title (no suffix) / description (existing behavior).
@@ -54,7 +55,22 @@ export function resolvePageHeadMeta(
pathname: string
): ResolvedPageHeadMeta {
const title = `${props.title}${site.titleSuffix}`;
const canonical = props.canonical ?? `https://${site.domain}${pathname}`;
/**
* The canonical URL is the *public* browser URL, never the internal route
* prefix. SolidStart's file router is host-blind, so subdomain routes live
* under `src/routes/<prefix>/*` and are served either by `vercel.json` host
* rewrites OR by `src/middleware.ts` (the in-app host rewrite). Both append
* the prefix to the internal request path (`/privacy` → `/lineage/privacy`),
* so `useLocation()` reports the prefixed path — which we strip back off so
* the canonical stays `https://lineage.freno.me/privacy`.
*/
const publicPath =
site.baseRoutePrefix &&
(pathname === site.baseRoutePrefix ||
pathname.startsWith(site.baseRoutePrefix + "/"))
? pathname.slice(site.baseRoutePrefix.length) || "/"
: pathname;
const canonical = props.canonical ?? `https://${site.domain}${publicPath}`;
const ogTitle = props.ogTitle ?? props.title;
const ogDescription = props.ogDescription ?? props.description;
const ogImage = props.ogImage ?? site.ogDefaultImage;