From 5112a3685d8ac7a25fc047bd65b844c062b2d730 Mon Sep 17 00:00:00 2001 From: Will Manning Date: Mon, 4 May 2026 15:51:37 -0400 Subject: [PATCH] Add lib/theme.ts source-of-truth and theme-parity verify check Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Will Manning --- scripts/verify.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- src/lib/og.tsx | 7 ++++--- src/lib/theme.ts | 22 ++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/lib/theme.ts diff --git a/scripts/verify.ts b/scripts/verify.ts index 236adb4..ea180e9 100644 --- a/scripts/verify.ts +++ b/scripts/verify.ts @@ -4,7 +4,8 @@ * * Runs against a live server (default http://localhost:3000, override with * `BASE`). Designed to be run after `bun run start` — checks sitemap, robots, - * RSS feeds, OG images, canonicals, JSON-LD on posts, and internal links. + * RSS feeds, OG images, canonicals, JSON-LD on posts, internal links, and + * theme-token parity between `src/lib/theme.ts` and `src/app/globals.css`. * * Usage: * bun run start & @@ -13,6 +14,10 @@ * Exits 1 if any check fails. */ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { THEME } from "../src/lib/theme"; + const BASE = process.env.BASE ?? "http://localhost:3000"; type CheckResult = { @@ -369,6 +374,41 @@ async function checkInternalLinks( pass("internal links", `${targets.size} checked`); } +// Asserts that the TS-side palette in `src/lib/theme.ts` matches the +// CSS-side declarations in `globals.css`. Drift here would mean OG image +// generation paints with one shade and the site renders with another — +// exactly the bug this check is here to catch. Currently only +// `THEME.background` has a corresponding CSS variable (`--background` in +// `:root`); `THEME.foreground` is white-on-dark and uses Tailwind's +// built-in `text-white`, so there's nothing to assert against in CSS. +async function checkThemeParity(): Promise { + let css: string; + try { + css = await readFile( + path.join(process.cwd(), "src", "app", "globals.css"), + "utf-8" + ); + } catch (err) { + fail( + "theme parity", + `failed to read globals.css: ${err instanceof Error ? err.message : String(err)}` + ); + return; + } + const re = new RegExp( + `--background:\\s*${THEME.background.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")};`, + "i" + ); + if (!re.test(css)) { + fail( + "theme parity", + `--background in globals.css does not match THEME.background (${THEME.background})` + ); + return; + } + pass("theme parity", `--background = ${THEME.background}`); +} + // The /api/subscribe handler instantiates `new Resend(...)` *inside* the // request handler so the build doesn't fail when RESEND_API_KEY is unset. // When the env var is missing it returns 503 with a JSON error body. We @@ -435,6 +475,7 @@ async function main(): Promise { await checkRssXml(); await checkRssJson(); await checkRssAtom(); + await checkThemeParity(); await checkSubscribe(); const htmlByPath = new Map(); diff --git a/src/lib/og.tsx b/src/lib/og.tsx index 82fbf68..704cc2a 100644 --- a/src/lib/og.tsx +++ b/src/lib/og.tsx @@ -1,5 +1,6 @@ import { ImageResponse } from "next/og"; import { siteName } from "@/lib/constants"; +import { THEME } from "@/lib/theme"; export const OG_SIZE = { width: 1200, height: 630 } as const; export const OG_CONTENT_TYPE = "image/png"; @@ -37,8 +38,8 @@ function OgCard({ flexDirection: "column", justifyContent: "space-between", padding: "80px", - backgroundColor: "#0a0a0a", - color: "#ffffff", + backgroundColor: THEME.background, + color: THEME.foreground, fontFamily: "sans-serif" }} > @@ -69,7 +70,7 @@ function OgCard({ fontWeight: 300, lineHeight: 1.08, letterSpacing: "-0.02em", - color: "#ffffff" + color: THEME.foreground }} > {title} diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..91abf24 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,22 @@ +/** + * Palette source of truth for TS-side consumers (OG image generation, + * scripts). The CSS-side source lives in `src/app/globals.css` under + * `:root` (and `@theme inline` aliases). The two MUST stay in sync — + * `scripts/verify.ts` enforces this in CI for the values that exist on + * both sides. + * + * `foreground` is currently white-on-dark and shows up in the site as the + * built-in Tailwind `text-white` (no `--color-foreground` CSS variable). + * We track it here anyway so the OG components have a single name to + * import, and so a future move to a CSS-side `--color-foreground` token + * has an obvious destination. + * + * If you change a palette value here, change it in `globals.css` too and + * update the verify check's expected values. + */ +export const THEME = { + background: "#101010", + foreground: "#ffffff" +} as const; + +export type ThemeKey = keyof typeof THEME;