diff --git a/AGENTS.md b/AGENTS.md index 330eaf5..7828732 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,7 +86,7 @@ This project serves four product subdomains (`nessa.freno.me`, `lineage.freno.me`, `gaze.freno.me`, `inputhalo.freno.me`) plus the personal site on `freno.me`. See `docs/subdomain-setup.md` for DNS/Vercel configuration. -- **Route placement:** Subdomain pages live under `src/routes//*` (e.g. `src/routes/nessa/...`). An in-app middleware (`src/middleware.ts`, registered via `app.config.ts` → `middleware`) rewrites the request path based on the `Host` header so `lineage.freno.me/privacy` resolves to the `/lineage/privacy` file route. (The `vercel.json` host `rewrites` are declared but **not applied** by Vercel — the Nitro `vercel` preset emits a Build Output API `config.json` whose `routes` array fully replaces `vercel.json` rewrites/redirects/headers, so the in-app middleware is what actually does the work.) +- **Route placement:** Subdomain pages live under `src/routes//*` (e.g. `src/routes/nessa/...`) as the source-of-truth content components. The public browser path on each subdomain (e.g. `lineage.freno.me/privacy` → `/privacy`) is served by **host-aware root route dispatch**: the root route file (`src/routes/privacy.tsx`, `deletion.tsx`, `contact.tsx`, `downloads.tsx`, `index.tsx`) resolves on the public path on BOTH server and client, then selects the matching subdomain component via `useSite()`. Do **not** rewrite the request path server-side — SolidStart's client `Router` matches on `window.location.pathname`, so a server-only rewrite diverges SSR from hydration and causes hydration mismatches. (The `vercel.json` host `rewrites` are declared but **not applied** by Vercel — the Nitro `vercel` preset emits a Build Output API `config.json` whose `routes` array fully replaces `vercel.json` rewrites/redirects/headers — so the host-aware root dispatch is what actually serves subdomain pages.) - **Site context:** Use `useSite()` (SolidJS) or `getSiteFromEvent`/`getSiteFromRequest` (server) from `src/lib/site-context.ts` to detect the current site. Never host-snoop in route files — SolidStart's router can't match on host. - **API routes:** `/api/*` is a shared pool — subdomain API requests pass through to existing routes via vercel.json pass-through rewrites (ordering matters). - **Auth:** Host-scoped only — no cookie domain broadening. diff --git a/app.config.ts b/app.config.ts index 7b2f273..a38b709 100644 --- a/app.config.ts +++ b/app.config.ts @@ -3,7 +3,6 @@ import tailwindcss from "@tailwindcss/vite"; import { sentryVitePlugin as sentryPlugin } from "@sentry/vite-plugin"; export default defineConfig({ - middleware: "./src/middleware.ts", vite: { plugins: [ tailwindcss(), diff --git a/public/gaze/favicon/apple-touch-icon.png b/public/gaze/favicon/apple-touch-icon.png new file mode 100755 index 0000000..c60c4e7 Binary files /dev/null and b/public/gaze/favicon/apple-touch-icon.png differ diff --git a/public/gaze/favicon/favicon-96x96.png b/public/gaze/favicon/favicon-96x96.png new file mode 100755 index 0000000..00ec4be Binary files /dev/null and b/public/gaze/favicon/favicon-96x96.png differ diff --git a/public/gaze/favicon/favicon.ico b/public/gaze/favicon/favicon.ico new file mode 100755 index 0000000..828623b Binary files /dev/null and b/public/gaze/favicon/favicon.ico differ diff --git a/public/gaze/favicon/favicon.svg b/public/gaze/favicon/favicon.svg new file mode 100755 index 0000000..52aa496 --- /dev/null +++ b/public/gaze/favicon/favicon.svg @@ -0,0 +1,17 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/public/gaze/favicon/site.webmanifest b/public/gaze/favicon/site.webmanifest new file mode 100755 index 0000000..ccf313a --- /dev/null +++ b/public/gaze/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "MyWebSite", + "short_name": "MySite", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/public/gaze/favicon/web-app-manifest-192x192.png b/public/gaze/favicon/web-app-manifest-192x192.png new file mode 100755 index 0000000..145cc05 Binary files /dev/null and b/public/gaze/favicon/web-app-manifest-192x192.png differ diff --git a/public/gaze/favicon/web-app-manifest-512x512.png b/public/gaze/favicon/web-app-manifest-512x512.png new file mode 100755 index 0000000..2d00a64 Binary files /dev/null and b/public/gaze/favicon/web-app-manifest-512x512.png differ diff --git a/public/inputhalo/favicon/apple-touch-icon.png b/public/inputhalo/favicon/apple-touch-icon.png new file mode 100755 index 0000000..e646dfd Binary files /dev/null and b/public/inputhalo/favicon/apple-touch-icon.png differ diff --git a/public/inputhalo/favicon/favicon-96x96.png b/public/inputhalo/favicon/favicon-96x96.png new file mode 100755 index 0000000..eaaa1ce Binary files /dev/null and b/public/inputhalo/favicon/favicon-96x96.png differ diff --git a/public/inputhalo/favicon/favicon.ico b/public/inputhalo/favicon/favicon.ico new file mode 100755 index 0000000..4046b32 Binary files /dev/null and b/public/inputhalo/favicon/favicon.ico differ diff --git a/public/inputhalo/favicon/favicon.svg b/public/inputhalo/favicon/favicon.svg new file mode 100755 index 0000000..ba4f7a3 --- /dev/null +++ b/public/inputhalo/favicon/favicon.svg @@ -0,0 +1,17 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/public/inputhalo/favicon/site.webmanifest b/public/inputhalo/favicon/site.webmanifest new file mode 100755 index 0000000..ccf313a --- /dev/null +++ b/public/inputhalo/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "MyWebSite", + "short_name": "MySite", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/public/inputhalo/favicon/web-app-manifest-192x192.png b/public/inputhalo/favicon/web-app-manifest-192x192.png new file mode 100755 index 0000000..2562bd8 Binary files /dev/null and b/public/inputhalo/favicon/web-app-manifest-192x192.png differ diff --git a/public/inputhalo/favicon/web-app-manifest-512x512.png b/public/inputhalo/favicon/web-app-manifest-512x512.png new file mode 100755 index 0000000..57849a0 Binary files /dev/null and b/public/inputhalo/favicon/web-app-manifest-512x512.png differ diff --git a/public/lineage/favicon/apple-touch-icon.png b/public/lineage/favicon/apple-touch-icon.png new file mode 100755 index 0000000..e9f4944 Binary files /dev/null and b/public/lineage/favicon/apple-touch-icon.png differ diff --git a/public/lineage/favicon/favicon-96x96.png b/public/lineage/favicon/favicon-96x96.png new file mode 100755 index 0000000..961bb99 Binary files /dev/null and b/public/lineage/favicon/favicon-96x96.png differ diff --git a/public/lineage/favicon/favicon.ico b/public/lineage/favicon/favicon.ico new file mode 100755 index 0000000..4cb9ce6 Binary files /dev/null and b/public/lineage/favicon/favicon.ico differ diff --git a/public/lineage/favicon/favicon.svg b/public/lineage/favicon/favicon.svg new file mode 100755 index 0000000..22e5d91 --- /dev/null +++ b/public/lineage/favicon/favicon.svg @@ -0,0 +1,3 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/public/lineage/favicon/site.webmanifest b/public/lineage/favicon/site.webmanifest new file mode 100755 index 0000000..ccf313a --- /dev/null +++ b/public/lineage/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "MyWebSite", + "short_name": "MySite", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/public/lineage/favicon/web-app-manifest-192x192.png b/public/lineage/favicon/web-app-manifest-192x192.png new file mode 100755 index 0000000..1a19edf Binary files /dev/null and b/public/lineage/favicon/web-app-manifest-192x192.png differ diff --git a/public/lineage/favicon/web-app-manifest-512x512.png b/public/lineage/favicon/web-app-manifest-512x512.png new file mode 100755 index 0000000..425119c Binary files /dev/null and b/public/lineage/favicon/web-app-manifest-512x512.png differ diff --git a/public/nessa/favicon/apple-touch-icon.png b/public/nessa/favicon/apple-touch-icon.png new file mode 100755 index 0000000..2525c63 Binary files /dev/null and b/public/nessa/favicon/apple-touch-icon.png differ diff --git a/public/nessa/favicon/favicon-96x96.png b/public/nessa/favicon/favicon-96x96.png new file mode 100755 index 0000000..9494742 Binary files /dev/null and b/public/nessa/favicon/favicon-96x96.png differ diff --git a/public/nessa/favicon/favicon.ico b/public/nessa/favicon/favicon.ico new file mode 100755 index 0000000..a3bc93c Binary files /dev/null and b/public/nessa/favicon/favicon.ico differ diff --git a/public/nessa/favicon/favicon.svg b/public/nessa/favicon/favicon.svg new file mode 100755 index 0000000..04cf2c9 --- /dev/null +++ b/public/nessa/favicon/favicon.svg @@ -0,0 +1,17 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/public/nessa/favicon/site.webmanifest b/public/nessa/favicon/site.webmanifest new file mode 100755 index 0000000..ccf313a --- /dev/null +++ b/public/nessa/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "MyWebSite", + "short_name": "MySite", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/public/nessa/favicon/web-app-manifest-192x192.png b/public/nessa/favicon/web-app-manifest-192x192.png new file mode 100755 index 0000000..9445c78 Binary files /dev/null and b/public/nessa/favicon/web-app-manifest-192x192.png differ diff --git a/public/nessa/favicon/web-app-manifest-512x512.png b/public/nessa/favicon/web-app-manifest-512x512.png new file mode 100755 index 0000000..da2a2d9 Binary files /dev/null and b/public/nessa/favicon/web-app-manifest-512x512.png differ diff --git a/src/components/PageHead.test.ts b/src/components/PageHead.test.ts index aa5936b..0dd39f1 100644 --- a/src/components/PageHead.test.ts +++ b/src/components/PageHead.test.ts @@ -60,10 +60,10 @@ 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. + // Subdomain routing is host-aware (root routes dispatch by useSite()), so + // useLocation() normally reports the public path. resolvePageHeadMeta also + // defensively strips a subdomain prefix from a prefixed pathname, so the + // canonical stays public even if the prefixed form ever appears. it("nessa prefixed /nessa/contact → https://nessa.freno.me/contact", () => { const meta = resolvePageHeadMeta( BASE_PROPS, diff --git a/src/components/SubdomainHeader.tsx b/src/components/SubdomainHeader.tsx index d7767a3..2cd5928 100644 --- a/src/components/SubdomainHeader.tsx +++ b/src/components/SubdomainHeader.tsx @@ -12,13 +12,13 @@ import { For, Show } from "solid-js"; import { A, useLocation } from "@solidjs/router"; import { useSite } from "~/context/SiteContext"; import { useDarkMode } from "~/context/darkMode"; -import { NAV_CONFIG, BACK_TO_FRENO } from "~/lib/nav-config"; +import { NAV_CONFIG } from "~/lib/nav-config"; import { DarkModeToggle } from "~/components/DarkModeToggle"; export default function SubdomainHeader() { const site = useSite(); const location = useLocation(); - const { isDark, toggleDarkMode } = useDarkMode(); + const { isDark } = useDarkMode(); const brandName = () => site().displayName; const brandColor = () => @@ -32,10 +32,7 @@ export default function SubdomainHeader() { }; return ( -
+
/*` 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`. + * The canonical URL is the *public* browser URL, never any internal route + * prefix. Subdomain routing is host-aware (root routes dispatch by + * `useSite()`), so `useLocation()` returns the public path. The strip below + * is a defensive no-op for the public path and still yields the canonical + * `https://lineage.freno.me/privacy` if a prefixed form ever appears. */ const publicPath = site.baseRoutePrefix && diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index c4445fc..7d40183 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,4 +1,10 @@ -import { JSX, splitProps, Show, createSignal, createEffect } from "solid-js"; +import { + type JSX, + splitProps, + Show, + createSignal, + createEffect +} from "solid-js"; import { Spinner } from "~/components/Spinner"; export interface ButtonProps extends JSX.ButtonHTMLAttributes { @@ -6,6 +12,11 @@ export interface ButtonProps extends JSX.ButtonHTMLAttributes size?: "sm" | "md" | "lg"; loading?: boolean; fullWidth?: boolean; + /** + * Override the variant's background color with an explicit CSS color. + * Used by download pages to theme the button with the product brand color. + */ + color?: string; } export default function Button(props: ButtonProps) { @@ -16,7 +27,8 @@ export default function Button(props: ButtonProps) { "fullWidth", "class", "children", - "disabled" + "disabled", + "color" ]); let contentRef: HTMLSpanElement | undefined; @@ -53,8 +65,8 @@ export default function Button(props: ButtonProps) { : "bg-surface0 hover:brightness-125 active:scale-90"; case "download": return isDisabledOrLoading - ? "bg-green text-base cursor-not-allowed brightness-75" - : "bg-green text-base hover:brightness-125 active:scale-90"; + ? "text-base cursor-not-allowed brightness-75" + : "text-base hover:brightness-125 active:scale-90"; case "danger": return isDisabledOrLoading ? "bg-red cursor-not-allowed brightness-75" @@ -83,11 +95,19 @@ export default function Button(props: ButtonProps) { const widthClass = () => (local.fullWidth ? "w-full" : ""); + const buttonStyle = (): JSX.CSSProperties => { + if (local.color) { + return { background: local.color }; + } + return {}; + }; + return (