Skip to content

Commit fbfb1f1

Browse files
lwwmanningclaude
andauthored
Add lib/theme.ts source-of-truth and theme-parity verify check (#64)
Signed-off-by: Will Manning <will@willmanning.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c8cc13b commit fbfb1f1

3 files changed

Lines changed: 68 additions & 4 deletions

File tree

scripts/verify.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*
55
* Runs against a live server (default http://localhost:3000, override with
66
* `BASE`). Designed to be run after `bun run start` — checks sitemap, robots,
7-
* RSS feeds, OG images, canonicals, JSON-LD on posts, and internal links.
7+
* RSS feeds, OG images, canonicals, JSON-LD on posts, internal links, and
8+
* theme-token parity between `src/lib/theme.ts` and `src/app/globals.css`.
89
*
910
* Usage:
1011
* bun run start &
@@ -13,6 +14,10 @@
1314
* Exits 1 if any check fails.
1415
*/
1516

17+
import { readFile } from "node:fs/promises";
18+
import path from "node:path";
19+
import { THEME } from "../src/lib/theme";
20+
1621
const BASE = process.env.BASE ?? "http://localhost:3000";
1722

1823
type CheckResult = {
@@ -369,6 +374,41 @@ async function checkInternalLinks(
369374
pass("internal links", `${targets.size} checked`);
370375
}
371376

377+
// Asserts that the TS-side palette in `src/lib/theme.ts` matches the
378+
// CSS-side declarations in `globals.css`. Drift here would mean OG image
379+
// generation paints with one shade and the site renders with another —
380+
// exactly the bug this check is here to catch. Currently only
381+
// `THEME.background` has a corresponding CSS variable (`--background` in
382+
// `:root`); `THEME.foreground` is white-on-dark and uses Tailwind's
383+
// built-in `text-white`, so there's nothing to assert against in CSS.
384+
async function checkThemeParity(): Promise<void> {
385+
let css: string;
386+
try {
387+
css = await readFile(
388+
path.join(process.cwd(), "src", "app", "globals.css"),
389+
"utf-8"
390+
);
391+
} catch (err) {
392+
fail(
393+
"theme parity",
394+
`failed to read globals.css: ${err instanceof Error ? err.message : String(err)}`
395+
);
396+
return;
397+
}
398+
const re = new RegExp(
399+
`--background:\\s*${THEME.background.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")};`,
400+
"i"
401+
);
402+
if (!re.test(css)) {
403+
fail(
404+
"theme parity",
405+
`--background in globals.css does not match THEME.background (${THEME.background})`
406+
);
407+
return;
408+
}
409+
pass("theme parity", `--background = ${THEME.background}`);
410+
}
411+
372412
// The /api/subscribe handler instantiates `new Resend(...)` *inside* the
373413
// request handler so the build doesn't fail when RESEND_API_KEY is unset.
374414
// When the env var is missing it returns 503 with a JSON error body. We
@@ -435,6 +475,7 @@ async function main(): Promise<void> {
435475
await checkRssXml();
436476
await checkRssJson();
437477
await checkRssAtom();
478+
await checkThemeParity();
438479
await checkSubscribe();
439480

440481
const htmlByPath = new Map<string, string>();

src/lib/og.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ImageResponse } from "next/og";
22
import { siteName } from "@/lib/constants";
3+
import { THEME } from "@/lib/theme";
34

45
export const OG_SIZE = { width: 1200, height: 630 } as const;
56
export const OG_CONTENT_TYPE = "image/png";
@@ -37,8 +38,8 @@ function OgCard({
3738
flexDirection: "column",
3839
justifyContent: "space-between",
3940
padding: "80px",
40-
backgroundColor: "#0a0a0a",
41-
color: "#ffffff",
41+
backgroundColor: THEME.background,
42+
color: THEME.foreground,
4243
fontFamily: "sans-serif"
4344
}}
4445
>
@@ -69,7 +70,7 @@ function OgCard({
6970
fontWeight: 300,
7071
lineHeight: 1.08,
7172
letterSpacing: "-0.02em",
72-
color: "#ffffff"
73+
color: THEME.foreground
7374
}}
7475
>
7576
{title}

src/lib/theme.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Palette source of truth for TS-side consumers (OG image generation,
3+
* scripts). The CSS-side source lives in `src/app/globals.css` under
4+
* `:root` (and `@theme inline` aliases). The two MUST stay in sync —
5+
* `scripts/verify.ts` enforces this in CI for the values that exist on
6+
* both sides.
7+
*
8+
* `foreground` is currently white-on-dark and shows up in the site as the
9+
* built-in Tailwind `text-white` (no `--color-foreground` CSS variable).
10+
* We track it here anyway so the OG components have a single name to
11+
* import, and so a future move to a CSS-side `--color-foreground` token
12+
* has an obvious destination.
13+
*
14+
* If you change a palette value here, change it in `globals.css` too and
15+
* update the verify check's expected values.
16+
*/
17+
export const THEME = {
18+
background: "#101010",
19+
foreground: "#ffffff"
20+
} as const;
21+
22+
export type ThemeKey = keyof typeof THEME;

0 commit comments

Comments
 (0)