diff --git a/frontend/README-VERCEL.md b/frontend/README-VERCEL.md index b115f3c..5472b18 100644 --- a/frontend/README-VERCEL.md +++ b/frontend/README-VERCEL.md @@ -9,8 +9,12 @@ a local CLI deployment is not the routine production path. After promotion, confirm the production deployment references the intended commit. Open the canonical domain and verify the theme toggle changes theme -and the client scene mounts. Build success and analytics events alone do not -establish that client initialization worked. +and persists after reload, the specimen plates render, and `/support` shows its +buy links. Build success and analytics events alone do not establish that client +initialization worked. + +`npm run og` regenerates the share cards in `public/og*` with playwright-core +and the locally installed Google Chrome; it isn't part of the build. ## Rendering and security headers diff --git a/frontend/app/(site)/support/page.tsx b/frontend/app/(site)/support/page.tsx index 9df2d06..610c058 100644 --- a/frontend/app/(site)/support/page.tsx +++ b/frontend/app/(site)/support/page.tsx @@ -16,7 +16,7 @@ import { } from "@/lib/support"; const description = - "Back CodesWhat through GitHub Sponsors or a Stripe payment link, see what each project costs to run, and what happens to a project that can't cover its costs."; + "Back CodesWhat through GitHub Sponsors or a Stripe payment link, and see what each project costs to run."; const SUPPORT_OG = { url: "/og/support.png", diff --git a/frontend/lib/theme-init-script.ts b/frontend/lib/theme-init-script.ts index 440f656..8818133 100644 --- a/frontend/lib/theme-init-script.ts +++ b/frontend/lib/theme-init-script.ts @@ -1,13 +1,15 @@ // Runs before first paint (nonce'd in the root layout) so the page never flashes the wrong // paper. An explicit choice saved by the header toggle wins; otherwise follow the OS, and // fall back to light. data-theme on is what the Catalogue tokens key off. -export const THEME_INIT_SCRIPT = `try { - var saved = localStorage.getItem('theme') - var theme = saved === 'light' || saved === 'dark' - ? saved - : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') +export const THEME_INIT_SCRIPT = `(function () { + var saved = null + try { saved = localStorage.getItem('theme') } catch (_) {} + var theme = 'light' + try { + theme = saved === 'light' || saved === 'dark' + ? saved + : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + } catch (_) {} document.documentElement.dataset.theme = theme document.documentElement.style.colorScheme = theme -} catch (_) { - document.documentElement.dataset.theme = 'light' -}`; +})()`; diff --git a/frontend/test/site-browser.test.mjs b/frontend/test/site-browser.test.mjs index 836aa25..b9ce1e3 100644 --- a/frontend/test/site-browser.test.mjs +++ b/frontend/test/site-browser.test.mjs @@ -163,6 +163,23 @@ test("light is the default paper when the system has no dark preference", { assert.equal(await darkPage.locator("html").getAttribute("data-theme"), "dark"); }); +test("a dark system preference still wins when storage is blocked", { + timeout: 15_000, +}, async (t) => { + const context = await browser.newContext({ colorScheme: "dark" }); + t.after(() => context.close()); + await context.addInitScript(() => { + Object.defineProperty(window, "localStorage", { + get() { + throw new DOMException("blocked", "SecurityError"); + }, + }); + }); + const page = await context.newPage(); + await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); + assert.equal(await page.locator("html").getAttribute("data-theme"), "dark"); +}); + test("a project page renders its specimen and unknown slugs 404", { timeout: 20_000, }, async (t) => {