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

@@ -89,7 +89,7 @@ function getGtActivityPromise(): Promise<ContributionDay[]> {
.catch(() => []));
}
// ── Subdomain nav rendering (task 04) ─────────────────────────────────────
// ── Subdomain nav rendering ──────────────────────────────────────────────
//
// The main site retains its bespoke LeftBar / RightBarContent rendering
// unchanged (Recent Posts, auth-aware Account/Login/SignOut, admin links,
@@ -578,170 +578,170 @@ function MainLeftBarContent() {
<div class="text-text flex flex-1 flex-col px-4 pb-4 text-xl font-bold">
<div class="flex flex-col py-8">
<span class="text-lg font-semibold">Recent Posts</span>
<div class="flex max-h-[50dvh] flex-col gap-3 pt-4">
<Show
when={recentPosts()}
fallback={
<For each={[1, 2, 3]}>
{() => (
<div class="flex w-52 flex-col">
<div class="relative overflow-hidden">
<SkeletonBox class="float-right ml-2 h-12 w-16" />
<div class="flex flex-col">
<SkeletonText class="h-6 w-full" />
<SkeletonText class="mt-1.5 h-6 w-2/3" />
<div class="flex max-h-[50dvh] flex-col gap-3 pt-4">
<Show
when={recentPosts()}
fallback={
<For each={[1, 2, 3]}>
{() => (
<div class="flex w-52 flex-col">
<div class="relative overflow-hidden">
<SkeletonBox class="float-right ml-2 h-12 w-16" />
<div class="flex flex-col">
<SkeletonText class="h-6 w-full" />
<SkeletonText class="mt-1.5 h-6 w-2/3" />
</div>
</div>
<SkeletonText class="mt-1.5 h-6 w-40" />
<SkeletonText class="mt-1.5 h-4 w-1/2" />
</div>
<SkeletonText class="mt-1.5 h-6 w-40" />
<SkeletonText class="mt-1.5 h-4 w-1/2" />
</div>
)}
</For>
}
>
<For each={recentPosts()}>
{(post) => (
<a
href={`/blog/${post.title}`}
onClick={handleLinkClick}
class="hover:text-subtext0 block w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold"
>
<Typewriter class="flex flex-col" keepAlive={false}>
<div class="relative overflow-hidden">
<img
src={getThumbnailUrl(post.banner_photo)}
alt="post-cover"
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
onError={(e) => {
const img = e.currentTarget;
if (
img.src !==
(post.banner_photo || "/blueprint.jpg")
) {
img.src = post.banner_photo || "/blueprint.jpg";
}
}}
/>
<span class="inline wrap-break-word hyphens-auto">
{insertSoftHyphens(post.title.replace(/_/g, " "))}
</span>
</div>
<span class="text-subtext0 clear-both text-sm">
{new Date(post.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric"
})}
</span>
</Typewriter>
</a>
)}
</For>
}
>
<For each={recentPosts()}>
{(post) => (
<a
href={`/blog/${post.title}`}
onClick={handleLinkClick}
class="hover:text-subtext0 block w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold"
>
<Typewriter class="flex flex-col" keepAlive={false}>
<div class="relative overflow-hidden">
<img
src={getThumbnailUrl(post.banner_photo)}
alt="post-cover"
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
onError={(e) => {
const img = e.currentTarget;
if (
img.src !==
(post.banner_photo || "/blueprint.jpg")
) {
img.src = post.banner_photo || "/blueprint.jpg";
}
}}
/>
<span class="inline wrap-break-word hyphens-auto">
{insertSoftHyphens(post.title.replace(/_/g, " "))}
</span>
</div>
<span class="text-subtext0 clear-both text-sm">
{new Date(post.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric"
})}
</span>
</Typewriter>
</a>
)}
</For>
</Show>
</Show>
</div>
</div>
</div>
<div class="mt-auto">
<Typewriter keepAlive={false}>
<ul class="flex flex-col gap-4 pt-6">
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/" onClick={handleLinkClick}>
Home
</a>
</li>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/blog" onClick={handleLinkClick}>
Blog
</a>
</li>
<Show when={isMounted() && isAdmin()}>
<div class="mt-auto">
<Typewriter keepAlive={false}>
<ul class="flex flex-col gap-4 pt-6">
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/analytics" onClick={handleLinkClick}>
Analytics
<a href="/" onClick={handleLinkClick}>
Home
</a>
</li>
</Show>
<li
class="hover:text-subtext0 w-fit cursor-pointer transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
onClick={() => {
navigate("/account");
handleLinkClick();
}}
>
<Show
when={isMounted() && isAuthenticated()}
fallback={
<a href="/login" onClick={handleLinkClick}>
Login
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/blog" onClick={handleLinkClick}>
Blog
</a>
</li>
<Show when={isMounted() && isAdmin()}>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/analytics" onClick={handleLinkClick}>
Analytics
</a>
}
>
<A href="/account" onClick={handleLinkClick}>
Account
<Show when={email()}>
<span class="text-subtext0 text-sm font-normal">
{" "}
({email()})
</span>
</Show>
</A>
</li>
</Show>
</li>
<Show when={isMounted() && isAuthenticated()}>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<button
onClick={handleSignOut}
disabled={signOutLoading()}
class="text-left disabled:opacity-50"
<li
class="hover:text-subtext0 w-fit cursor-pointer transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
onClick={() => {
navigate("/account");
handleLinkClick();
}}
>
<Show
when={isMounted() && isAuthenticated()}
fallback={
<a href="/login" onClick={handleLinkClick}>
Login
</a>
}
>
{signOutLoading() ? "Signing Out..." : "Sign Out"}
</button>
<A href="/account" onClick={handleLinkClick}>
Account
<Show when={email()}>
<span class="text-subtext0 text-sm font-normal">
{" "}
({email()})
</span>
</Show>
</A>
</Show>
</li>
</Show>
</ul>
</Typewriter>
<Show when={isMounted() && isAuthenticated()}>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<button
onClick={handleSignOut}
disabled={signOutLoading()}
class="text-left disabled:opacity-50"
>
{signOutLoading() ? "Signing Out..." : "Sign Out"}
</button>
</li>
</Show>
</ul>
</Typewriter>
<ul class="pt-4 pb-6">
<li
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
classList={{
"opacity-0 pointer-events-none": !getLostVisible(),
"opacity-100": getLostVisible()
}}
>
<button
onClick={() => {
const lostUrls = [
"/dev/null",
"/segfault",
"/void",
"/404",
"/lost-and-still-lost"
];
const randomUrl =
lostUrls[Math.floor(Math.random() * lostUrls.length)];
navigate(randomUrl);
handleLinkClick();
<ul class="pt-4 pb-6">
<li
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
classList={{
"opacity-0 pointer-events-none": !getLostVisible(),
"opacity-100": getLostVisible()
}}
class="text-left font-mono transition-opacity duration-75"
style={{ "will-change": "contents" }}
>
{getLostText()}
</button>
</li>
</ul>
<button
onClick={() => {
const lostUrls = [
"/dev/null",
"/segfault",
"/void",
"/404",
"/lost-and-still-lost"
];
const randomUrl =
lostUrls[Math.floor(Math.random() * lostUrls.length)];
navigate(randomUrl);
handleLinkClick();
}}
class="text-left font-mono transition-opacity duration-75"
style={{ "will-change": "contents" }}
>
{getLostText()}
</button>
</li>
</ul>
<hr class="border-overlay0 -mx-4 my-auto" />
<div class="my-auto">
<DarkModeToggle />
</div>
<hr class="border-overlay0 -mx-4 my-auto" />
<div class="my-auto">
<DarkModeToggle />
</div>
<div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
<RightBarContent />
<div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
<RightBarContent />
</div>
</div>
</div>
</div>
</>
);
}
@@ -828,9 +828,7 @@ export function LeftBar() {
// ("bars render appropriately styled per site — brand color hint from
// SITE_CONFIG"). Main keeps the existing neutral styling.
const accentBorder = () =>
site().id === "main"
? undefined
: { "border-color": site().brandColor };
site().id === "main" ? undefined : { "border-color": site().brandColor };
return (
<nav

View File

@@ -1,10 +1,4 @@
import {
createSignal,
onMount,
createEffect,
Show,
type JSX
} from "solid-js";
import { createSignal, onMount, createEffect, Show, type JSX } from "solid-js";
import { useSearchParams, query, createAsync } from "@solidjs/router";
import { action, redirect } from "@solidjs/router";
import { PageHead } from "~/components/PageHead";
@@ -43,7 +37,7 @@ import {
} from "~/lib/contact-config";
/**
* Shared, site-aware contact form (task 09 — per-subdomain contact pages).
* Shared, site-aware contact form — per-subdomain contact pages.
*
* Extracted verbatim-in-spirit from the legacy `src/routes/contact.tsx` so the
* main-site contact flow (`freno.me/contact`) keeps its exact Turnstile +
@@ -57,10 +51,10 @@ import {
* `CONTACT_CONTEXT[site().id]` (subjectPrefix, recipientLabel, heading,
* PageHead title + description). Props override the defaults.
* - Emits `<PageHead>` so every per-subdomain `/contact` route gets
* site-aware title / canonical / OG tags for free (task 02).
* site-aware title / canonical / OG tags for free.
* - The Turnstile site key (`VITE_TURNSTILE_SITE_KEY`) is shared across all
* subdomains — ensure it is configured for `*.freno.me` in the Cloudflare
* Turnstile dashboard (see task notes).
* Turnstile dashboard.
*
* Email routing:
* - JS path: `api.misc.sendContactRequest.mutate({ …, subjectPrefix })` — the
@@ -247,7 +241,8 @@ export function ContactForm(props: ContactFormProps) {
const ctx = () => getContactContext(site().id);
// Effective values — props override the site-context defaults.
const effectiveSubjectPrefix = () => props.subjectPrefix ?? ctx().subjectPrefix;
const effectiveSubjectPrefix = () =>
props.subjectPrefix ?? ctx().subjectPrefix;
const effectiveRecipientLabel = () =>
props.recipientLabel ?? ctx().recipientLabel;
const effectiveHeading = () => props.heading ?? ctx().heading;
@@ -283,7 +278,7 @@ export function ContactForm(props: ContactFormProps) {
// Load Cloudflare Turnstile script with explicit rendering.
// The site key is shared across all subdomains — ensure it is configured
// for `*.freno.me` in the Cloudflare Turnstile dashboard (task notes).
// for `*.freno.me` in the Cloudflare Turnstile dashboard.
const script = document.createElement("script");
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
script.async = true;

View File

@@ -6,7 +6,7 @@ import { getClientCookie } from "~/lib/cookies.client";
/**
* Product discriminator forwarded to the generalized
* `misc.sendDeletionRequestEmail` mutation so the email copy + cooldown
* cookie are product-appropriate (task 11).
* cookie are product-appropriate.
*/
export type DeletionProduct = "lineage" | "nessa";

View File

@@ -1,5 +1,5 @@
/**
* Unit tests for `PageHead` site-aware metadata derivation (task 02).
* Unit tests for `PageHead` site-aware metadata derivation.
*
* `resolvePageHeadMeta` is a pure function over (props, site, pathname), so
* these tests mirror the acceptance matrix without a DOM / SolidJS router.
@@ -28,21 +28,13 @@ describe("resolvePageHeadMeta — title suffix per site", () => {
for (const { id, suffix } of cases) {
it(`${id} → title is "${BASE_PROPS.title}${suffix}"`, () => {
const meta = resolvePageHeadMeta(
BASE_PROPS,
SITE_CONFIG[id],
"/blog"
);
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG[id], "/blog");
expect(meta.title).toBe(`${BASE_PROPS.title}${suffix}`);
});
}
it("main produces 'Home | Michael Freno' for the homepage", () => {
const meta = resolvePageHeadMeta(
{ title: "Home" },
SITE_CONFIG.main,
"/"
);
const meta = resolvePageHeadMeta({ title: "Home" }, SITE_CONFIG.main, "/");
expect(meta.title).toBe("Home | Michael Freno");
});
});
@@ -64,11 +56,7 @@ describe("resolvePageHeadMeta — canonical URL derivation", () => {
});
it("nessa /contact → https://nessa.freno.me/contact", () => {
const meta = resolvePageHeadMeta(
BASE_PROPS,
SITE_CONFIG.nessa,
"/contact"
);
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nessa, "/contact");
expect(meta.canonical).toBe("https://nessa.freno.me/contact");
});

View File

@@ -18,7 +18,7 @@ export {
/**
* PageHead component for consistent page metadata across the application.
*
* Site-aware (task 02): reads `useSite()` for the per-site title suffix,
* Site-aware: reads `useSite()` for the per-site title suffix,
* canonical domain, and default OpenGraph image, so the same component
* renders `" | Michael Freno"` / `" | Nessa"` / … depending on the active
* subdomain. Canonical URLs are auto-derived from the site domain + the

View File

@@ -1,5 +1,5 @@
/**
* Pure metadata derivation for `PageHead` (task 02).
* Pure metadata derivation for `PageHead`.
*
* Intentionally imports NOTHING from solid-js / @solidjs/router / @solidjs/meta
* so it can be unit-tested in `bun:test` without spinning up the SolidJS