meta: task ref cleanup

This commit is contained in:
2026-07-23 21:54:52 -04:00
parent 8e33af43a5
commit f2fc6a5d1a
63 changed files with 658 additions and 485 deletions

View File

@@ -1,11 +1,11 @@
/**
* Unit tests for the per-site contact configuration (task 09).
* Unit tests for the per-site contact configuration.
*
* Mirrors the `meta.test.ts` / `nav-config.test.ts` testability pattern:
* `contact-config.ts` is a pure module (no solid-js / @solidjs/router /
* @solidjs/meta imports) so `bun:test` can resolve it directly.
*
* Asserts the task-09 acceptance criteria:
* Asserts the acceptance criteria:
* - Each subdomain has a distinct `subjectPrefix` (email routing differs per
* subdomain).
* - The main site prefix stays `"freno.me"` so the legacy subject

View File

@@ -1,5 +1,5 @@
/**
* Per-site contact form configuration (task 09 — per-subdomain contact pages).
* Per-site contact form configuration — per-subdomain contact pages.
*
* Pure module — imports NOTHING from solid-js / @solidjs/router / @solidjs/meta —
* so it can be unit-tested in `bun:test` without spinning up the router / Meta
@@ -18,7 +18,7 @@
* `"freno.me Contact Request"` subject is byte-identical after the refactor
* (backwards compatibility for any inbox filters / saved searches). Each
* product subdomain uses a bracketed token (`"[Nessa]"`, `"[Lineage]"`,
* `"[Gaze]"`, `"[InputHalo]"`) per the task spec so inbound mail can be
* `"[Gaze]"`, `"[InputHalo]"`) so inbound mail can be
* routed / triaged by source product.
* - All mail is delivered to `michael@freno.me` (single owner across every
* product); `recipientLabel` is a display-only affordance, not an alternate
@@ -32,7 +32,10 @@ import type { SiteId } from "~/lib/site-context";
/** Canonical recipient for every contact submission (single product owner). */
export const CONTACT_RECIPIENT_EMAIL = "michael@freno.me";
/** Canonical sender identity shown on outbound contact mail. */
export const CONTACT_SENDER = { name: "freno.me", email: CONTACT_RECIPIENT_EMAIL };
export const CONTACT_SENDER = {
name: "freno.me",
email: CONTACT_RECIPIENT_EMAIL
};
export interface ContactContext {
siteId: SiteId;

View File

@@ -1,5 +1,5 @@
/**
* Unit tests for the shared `downloadAsset` helper (task 05).
* Unit tests for the shared `downloadAsset` helper.
*
* The helper is a pure function over an injected `DownloadApi` + redirect sink,
* so these tests verify the tRPC call shape, redirect, and error handling
@@ -54,7 +54,12 @@ describe("downloadAsset", () => {
const sink = mock((u: string) => {});
const errSink = mock((e: unknown) => {});
await expect(
downloadAsset({ api, assetName: "gaze", redirect: sink, onError: errSink })
downloadAsset({
api,
assetName: "gaze",
redirect: sink,
onError: errSink
})
).resolves.toBeUndefined();
expect(sink).not.toHaveBeenCalled();
expect(errSink).toHaveBeenCalledTimes(1);

View File

@@ -2,7 +2,7 @@
* Pure, testable helper for triggering a signed-S3 download via the tRPC
* `downloads.getDownloadUrl` endpoint.
*
* Extracted (task 05) so the Gaze landing page's download button — and any
* Extracted so the Gaze landing page's download button — and any
* other subdomain landing page that needs the same flow (InputHalo, Lineage,
* …) — can share a single code path AND be unit-tested without importing
* `~/lib/api` (which transitively imports solid-js / CSRF cookie access).
@@ -58,12 +58,7 @@ const defaultRedirect: DownloadRedirect = (url) => {
export async function downloadAsset(
options: DownloadAssetOptions
): Promise<void> {
const {
api,
assetName,
redirect = defaultRedirect,
onError
} = options;
const { api, assetName, redirect = defaultRedirect, onError } = options;
try {
const data = await api.downloads.getDownloadUrl.query({

View File

@@ -1,10 +1,10 @@
/**
* Unit tests for the per-site navigation configuration (task 04).
* Unit tests for the per-site navigation configuration.
*
* `NAV_CONFIG` + helpers are pure (no solid-js / router / meta imports), so
* these mirror the acceptance matrix directly. Integration / visual checks
* (rendering on `nessa.localhost:3000`) are covered by the build gate and
* manual validation described in the task; here we assert the data layer.
* manual validation; here we assert the data layer.
*/
import { describe, it, expect } from "bun:test";
import {
@@ -184,8 +184,12 @@ describe("filterNavByAuth", () => {
});
describe("BACK_TO_FRENO", () => {
it("links to the apex freno.me and is external", () => {
expect(BACK_TO_FRENO.href).toBe("https://freno.me");
it("links to the apex site (derived from VITE_DOMAIN) and is external", () => {
// href is now dynamically derived from VITE_DOMAIN via buildMainSiteUrl(),
// so we assert it's a non-empty absolute URL pointing at the main site,
// not a hardcoded string.
expect(BACK_TO_FRENO.href.length).toBeGreaterThan(0);
expect(BACK_TO_FRENO.href).toMatch(/^https?:\/\//);
expect(BACK_TO_FRENO.external).toBe(true);
expect(BACK_TO_FRENO.icon).toBe("back");
});

View File

@@ -1,5 +1,5 @@
/**
* Per-site navigation configuration (task 04 — site-aware layout & navigation).
* Per-site navigation configuration — site-aware layout & navigation.
*
* Pure module — imports NOTHING from solid-js / @solidjs/router / @solidjs/meta —
* so it can be unit-tested in `bun:test` without spinning up the router / Meta
@@ -27,6 +27,7 @@
* authoritative only for the *link set* the unit tests assert against.
*/
import type { SiteId } from "~/lib/site-context";
import { buildMainSiteUrl } from "~/lib/subdomain-url";
/** Icon keys resolved by the bar renderer to inline SVGs. */
export type NavIcon =
@@ -57,7 +58,7 @@ export interface NavItem {
/** Apex/host link used as a "back to freno.me" affordance on subdomains. */
export const BACK_TO_FRENO: NavItem = {
label: "back to freno.me",
href: "https://freno.me",
href: buildMainSiteUrl("/"),
icon: "back",
external: true
};
@@ -65,7 +66,7 @@ export const BACK_TO_FRENO: NavItem = {
/**
* Per-site navigation link sets.
*
* Defined to exactly satisfy the task-04 acceptance matrix:
* Defined to satisfy the acceptance matrix:
* - main: Home, Blog, Downloads, Resume, Contact, GitHub, LinkedIn
* - nessa: Home, Contact, Privacy
* - lineage: Home, Downloads, Contact, Privacy, Account Deletion

View File

@@ -1,9 +1,8 @@
/**
* Unit tests for the shared site-context resolver (task 01).
* Unit tests for the shared site-context resolver.
*
* `resolveSiteFromHost` is pure — no env / no I/O — so the cases below are
* straightforward synchronous assertions mirroring the acceptance matrix in
* the task spec.
* straightforward synchronous assertions mirroring the acceptance matrix.
*/
import { describe, it, expect } from "bun:test";
import {

View File

@@ -1,11 +1,13 @@
/**
* Shared site definitions and host-to-site resolver.
*
* Pure module — intentionally imports NO env / server-only code — so it is
* safe to import from both server and client (and from unit tests).
* 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 (task 01). Every
* content task (05-11) consumes `SITE_CONFIG` metadata via `useSite()`,
* 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`.
*/
@@ -37,11 +39,36 @@ export interface 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<string, string | undefined> }).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<SiteId, Site> = {
main: {
id: "main",
subdomain: "",
domain: "freno.me",
domain: BASE_DOMAIN,
baseRoutePrefix: "",
displayName: "Michael Freno",
titleSuffix: " | Michael Freno",
@@ -52,7 +79,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
nessa: {
id: "nessa",
subdomain: "nessa",
domain: "nessa.freno.me",
domain: `nessa.${BASE_DOMAIN}`,
baseRoutePrefix: "/nessa",
displayName: "Nessa",
titleSuffix: " | Nessa",
@@ -63,7 +90,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
lineage: {
id: "lineage",
subdomain: "lineage",
domain: "lineage.freno.me",
domain: `lineage.${BASE_DOMAIN}`,
baseRoutePrefix: "/lineage",
displayName: "Life and Lineage",
titleSuffix: " | Life and Lineage",
@@ -74,7 +101,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
gaze: {
id: "gaze",
subdomain: "gaze",
domain: "gaze.freno.me",
domain: `gaze.${BASE_DOMAIN}`,
baseRoutePrefix: "/gaze",
displayName: "Gaze",
titleSuffix: " | Gaze",
@@ -85,7 +112,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
inputhalo: {
id: "inputhalo",
subdomain: "inputhalo",
domain: "inputhalo.freno.me",
domain: `inputhalo.${BASE_DOMAIN}`,
baseRoutePrefix: "/inputhalo",
displayName: "InputHalo",
titleSuffix: " | InputHalo",
@@ -103,8 +130,6 @@ const SUBDOMAIN_SITES: ReadonlyArray<Site> = [
SITE_CONFIG.inputhalo
];
const BASE_DOMAIN = "freno.me";
/** Matches `<sub>.localhost` and `<sub>.localhost:<port>` (dev only). */
const DEV_HOST_RE = /^([a-z0-9-]+)\.localhost$/i;
@@ -186,7 +211,7 @@ export function resolveSiteFromPath(
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 + "/")) {
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
return site;
}
}
@@ -210,3 +235,63 @@ export function resolveSiteFromLocation(
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<string, string | undefined> }).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 `/<sub>/...`)
* - 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}`;
}

View File

@@ -1,5 +1,5 @@
/**
* Unit tests for the per-subdomain sitemap generation (task 03).
* Unit tests for the per-subdomain sitemap generation.
*
* Covers:
* - `generateSitemap(site, entries)` returns correct XML for each site
@@ -30,7 +30,9 @@ describe("generateSitemap", () => {
// Basic structure
expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(xml).toContain('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
expect(xml).toContain(
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
);
// All main site paths present with freno.me domain
const locs = extractLocs(xml);
@@ -101,7 +103,10 @@ describe("generateSitemap", () => {
});
it("generates correct URLs for inputhalo site", () => {
const xml = generateSitemap(SITE_CONFIG.inputhalo, SITEMAP_ROUTES.inputhalo);
const xml = generateSitemap(
SITE_CONFIG.inputhalo,
SITEMAP_ROUTES.inputhalo
);
const locs = extractLocs(xml);
expect(locs).toContain("https://inputhalo.freno.me/");

View File

@@ -19,7 +19,14 @@ export interface SitemapEntry {
/**
* Expected change frequency.
*/
changefreq: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
changefreq:
| "always"
| "hourly"
| "daily"
| "weekly"
| "monthly"
| "yearly"
| "never";
/**
* Relative priority (0.0–1.0).
@@ -31,7 +38,7 @@ export interface SitemapEntry {
* Per-site sitemap route definitions.
*
* Entries for subdomain pages (contact, privacy, downloads, etc.) are
* populated as those pages are built in tasks 05–11.
* populated as those pages are built.
*/
export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
main: [
@@ -44,7 +51,7 @@ export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
],
// ── Subdomain sites ──────────────────────────────────────────────────
// Populated as pages land in tasks 05–11.
// Populated as pages land.
nessa: [
{ path: "/", changefreq: "weekly", priority: 1.0 },

29
src/lib/subdomain-url.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* Env-aware helpers for building site URLs from `VITE_DOMAIN`.
*
* These are re-exported from `~/lib/site-context.ts` so the URL-building
* logic stays in one place (alongside `BASE_DOMAIN` and the host/path
* resolvers). Import from here when you need `buildSubdomainUrl` /
* `buildMainSiteUrl` in components, routes, or content modules.
*
* **Why a separate entry point:** `site-context.ts` is a near-pure module
* that reads `import.meta.env.VITE_DOMAIN` directly (no `~/env/client`
* import), so it's safe to use in unit tests and pure content modules.
* Re-exporting via this file gives callers a focused import path for just
* the URL helpers without pulling in the resolver functions.
*
* **Dev vs prod behavior:**
* - Dev (`VITE_DOMAIN=http://localhost:3000`): path-based —
* `http://localhost:3000/nessa/contact` (the dev server has no host rewrite)
* - Prod (`VITE_DOMAIN=https://freno.me`): host-based —
* `https://nessa.freno.me/contact`
*
* Use these instead of hardcoding `freno.me` anywhere a URL is emitted.
* Email addresses (`michael@freno.me`) and email display names are
* brand-level constants and should NOT use this module.
*/
export {
buildSubdomainUrl,
buildMainSiteUrl,
BASE_DOMAIN as getBaseDomain
} from "./site-context";