Wordle module, cross-language tests, and the deploy path

The browser engine is a port of the Python one and CI proves it: all 21.2M
(guess, answer) pairs hashed on both sides to the same SHA-256. Six TS tests,
including the duplicate-letter table and the twelve pinned seed vectors that
keep ?seed= permalinks pointing at the same word the recording used.

Word lists are split by how they are used. answers.json is inlined because the
board needs it before first paint to turn a seed into a word, and a fetch there
means a visibly empty board on a cold cache. guesses.json is fetched, because it
is three times larger and only needed the first time somebody presses Enter;
until it lands, validation falls back to the answer list, which accepts strictly
fewer words. The failure mode is 'your real word was briefly rejected', not 'a
non-word was accepted' — the right way round.

The solver runs in a worker constructed from a same-origin module URL, never
Vite's ?worker&inline: that yields a blob:, and production CSP has no
worker-src, so it falls back to default-src 'self' and the worker is blocked
with no console error. It would fail in production only.

deploy.sh smoke-tests the real public hostname from the deploying machine and
fails on a body under 1 kB, because the bind bug's signature is a valid
certificate over an empty 200 and a local --resolve check passes anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 15:53:25 -07:00
parent 2c2dcad9fd
commit b601511e7f
38 changed files with 3419 additions and 646 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env node
/**
* Renders the social card for every route.
*
* One 1200x630 PNG per route, screenshotted from the real built page, written
* to `public/og/`. Not generated from a template: a card drawn by a separate
* renderer drifts from the page it advertises, and this way a change to the
* board or the palette shows up in the card by construction.
*
* ── ORDERING ───────────────────────────────────────────────────────────────
*
* This reads `dist/` and writes `public/`, which means the cards are always one
* build behind until you build again:
*
* pnpm build && node scripts/og.mjs && pnpm build
*
* That is deliberate. The alternative — screenshotting the dev server — renders
* unminified CSS and a different font-loading path, and the cards came out
* subtly wrong in a way nobody noticed until they were on X.
*
* ── WHERE THIS RUNS ────────────────────────────────────────────────────────
*
* amd-server (x86), where Playwright's chromium is installed. It is NOT part of
* the deploy: the deploy host has no browser, and a deploy that silently
* skipped card generation would ship a site whose every link previews as a
* broken image. Run it here, commit the PNGs, deploy the PNGs.
*
* Env:
* PIG_OG_SCALE device pixel ratio, default 2 (so the file is 2400x1260 for
* a 1200x630 CSS-pixel card; every unfurler downsamples, and
* 1x text on a retina timeline looks like a fax).
*/
import fs from 'node:fs';
import path from 'node:path';
import { abs, die, dim, exists, expandRoutes, green, loadAllMetas, loadVerticals, rel, serveStatic, table } from './_lib.mjs';
const WIDTH = 1200;
const HEIGHT = 630;
const SCALE = Number(process.env.PIG_OG_SCALE ?? 2);
const DIST = abs('dist');
const OUT_DIR = abs('public', 'og');
if (!exists(path.join(DIST, 'index.html'))) {
die(`${rel(DIST)}/index.html does not exist. Run \`pnpm build\` first — the cards are shot from the built site.`);
}
const { chromium } = await import('playwright').catch(() => {
die('playwright is not installed. `pnpm install` first.');
});
const executable = chromium.executablePath();
if (!exists(executable)) {
die(
`Playwright's chromium is not installed at ${executable}.\n` +
' Run this on amd-server (x86), where it is installed, and commit the PNGs. ' +
'The deploy host has no browser, which is exactly why card generation is not part of the deploy.',
);
}
/* ------------------------------------------------------- routes -> filenames */
const { metas } = loadAllMetas();
const verticals = loadVerticals();
if (verticals.error) die(`could not read the verticals: ${verticals.error}`);
const { routes, errors } = expandRoutes({ metas, verticals: verticals.list });
if (errors.length) die(`route enumeration failed:\n - ${errors.join('\n - ')}`);
/**
* A demo's filename comes from its own `meta.ogImage`, not from its slug.
*
* `check-demos` rule 4 asserts that file exists; if this script invented a
* different name, the two would disagree and the check would fail on a card
* that had just been generated.
*/
function outputName(route) {
if (route.kind === 'home') return 'home.png';
if (route.kind === 'demo') {
const declared = metas.get(route.slug)?.ogImage;
return typeof declared === 'string' && declared.trim() !== '' ? path.basename(declared) : `${route.slug}.png`;
}
// Prefixed, because a vertical slug and a demo slug live in the same
// directory and nothing stops them colliding.
if (route.kind === 'vertical') return `vertical-${route.id}.png`;
return `${route.path.replace(/^\/+/, '').replace(/\//g, '-') || 'home'}.png`;
}
const targets = routes.map((route) => ({ route, name: outputName(route) }));
const byName = new Map();
for (const target of targets) {
const clash = byName.get(target.name);
if (clash) {
die(
`${target.route.path} and ${clash.path} would both write public/og/${target.name}. ` +
'Change one of their `meta.ogImage` values; a shared card means one of the two pages advertises the other.',
);
}
byName.set(target.name, target.route);
}
/* --------------------------------------------------------------- screenshot */
fs.mkdirSync(OUT_DIR, { recursive: true });
const server = await serveStatic(DIST);
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: WIDTH, height: HEIGHT },
deviceScaleFactor: SCALE,
colorScheme: 'light',
// Every animation on the site is an entrance. Shooting mid-flight catches
// elements at 40% opacity, which reads as a rendering bug in the card.
reducedMotion: 'reduce',
});
const rows = [];
try {
for (const { route, name } of targets) {
const page = await context.newPage();
await page.goto(`${server.origin}${route.path}`, { waitUntil: 'load', timeout: 30_000 });
await page.waitForSelector('#root > *', { timeout: 30_000 }).catch(() => {});
await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
await page.evaluate(() => document.fonts.ready.then(() => true)).catch(() => {});
const file = path.join(OUT_DIR, name);
// `clip` rather than fullPage: the card is a 1200x630 window onto the top
// of the page, and fullPage would hand the unfurler a 1200x9000 strip that
// every platform crops to something arbitrary.
await page.screenshot({ path: file, clip: { x: 0, y: 0, width: WIDTH, height: HEIGHT } });
await page.close();
rows.push([route.path, `public/og/${name}`, `${(fs.statSync(file).size / 1024).toFixed(0)} kB`]);
}
} finally {
await context.close();
await browser.close();
await server.close();
}
console.log(table(['route', 'card', 'size'], rows));
console.log('');
console.log(green(`og: wrote ${rows.length} cards at ${WIDTH}x${HEIGHT} CSS px (x${SCALE}).`));
console.log(dim('Rebuild before deploying, or dist/ still holds the previous cards.'));