#!/usr/bin/env node /** * Proves the Open Graph cards, two ways. `npm run og:proof`, after a build. * * WHY THIS EXISTS AT ALL. `AGENTS.md` R15: *"Nobody on this project will ever * see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a * reader who is not us."* Generating the cards does not fix that — it moves the * invisible thing from "wrong image" to "wrong image, generated". So the two * failures that would stay invisible are checked mechanically: * * 1. **Every page's `og:image` resolves to a file that exists in `dist/`.** A * 404 preview image renders as a blank card, and nothing else in this repo * would notice. Checked by reading the built HTML, not the source. * * 2. **Every card's headline and eyebrow are its page's own `

` and first * `.eyebrow`, character for character.** This is the compliance half. Text * baked into a JPEG is text `npm run check:claims` cannot grep, and under D20 * that script is the only per-step claims control there is — so a card must * never carry a claim its page does not already make in auditable HTML. The * check enforces that structurally rather than trusting an author to * remember it, and it fails in both directions: editing the page without the * registry, or the registry without the page. * * It also catches the quiet one: a straight apostrophe in the registry * against the typographic apostrophe the page renders. Found exactly that on * the first run, on `/practice/insurance/`. * * It reads `src/data/og-cards.ts` DIRECTLY — Node strips the types — so there is * no second list of cards to keep in step with the first. * * Optional: `npm run og:proof -- --sheet` writes a contact sheet of every card * to `dist/og-proof.jpg` so the set can be looked at in one go. Not part of the * check; a human still has to look. */ import { readdir, readFile, stat, writeFile } from 'node:fs/promises'; import { join, relative, sep } from 'node:path'; import sharp from 'sharp'; import { OG_CARDS, PORTRAIT_PAGES, articleCard, ogCardPath, } from '../src/data/og-cards.ts'; const ROOT = process.cwd(); const DIST = join(ROOT, 'dist'); const SITE = 'https://adr.smlcompany.ca'; const strip = (html) => html .replace(/<[^>]+>/g, '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); async function pages(dir = DIST) { const out = []; for (const entry of await readdir(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) out.push(...(await pages(full))); else if (entry.name === 'index.html') { const rel = relative(DIST, dir).split(sep).filter(Boolean).join('/'); out.push({ path: rel ? `/${rel}/` : '/', file: full }); } /* ⚠️ `index.html` ALONE MISSED A WHOLE PAGE. `build.format: 'directory'` puts every route at `/index.html` — except the ones Astro emits outside the convention, and `404.astro` becomes `dist/404.html`. So this script enumerated 22 pages of 23, and the symptom was backwards: it reported the 404 page's card as ORPHANED ("generated, but no built page references it") rather than reporting the page as unchecked. `path` here is an `OG_CARDS` key, which is `Astro.url.pathname` — `/404/`, not `/404.html`. `scripts/lighthouse.mjs` had the same blind spot and needs the URL form instead; see the note there. */ else if (dir === DIST && entry.name.endsWith('.html')) { out.push({ path: `/${entry.name.replace(/\.html$/, '')}/`, file: full }); } } return out.sort((a, b) => a.path.localeCompare(b.path)); } const problems = []; const fail = (msg) => problems.push(msg); try { await stat(join(DIST, 'index.html')); } catch { console.error('dist/ is missing or empty. Run `npm run build` first.'); process.exit(2); } const built = await pages(); const seenCards = new Set(); let checkedHeadlines = 0; let checkedArticles = 0; /** * An article's expected card, from the SAME `articleCard()` the endpoint calls, * given the title in that article's own frontmatter. Read from the `.mdx` rather * than from the built page, so the comparison has two independent sides: what * the article says its title is, and what the route rendered as the `

`. */ async function expectedArticleCard(path) { const slug = path.replace(/^\/insights\/|\/$/g, ''); for (const ext of ['mdx', 'md']) { try { const src = await readFile( join(ROOT, 'src', 'content', 'insights', `${slug}.${ext}`), 'utf8', ); const m = /^title:\s*(.*)$/m.exec(src); if (!m) break; let title = m[1].trim(); // YAML scalar: strip one layer of quoting and unescape a doubled single // quote, which is how YAML writes a literal apostrophe inside '…'. if ( (title.startsWith("'") && title.endsWith("'")) || (title.startsWith('"') && title.endsWith('"')) ) { title = title.slice(1, -1); } title = title.replace(/''/g, "'"); return articleCard(title); } catch { /* try the next extension */ } } return null; } for (const { path, file } of built) { const html = await readFile(file, 'utf8'); // ---- 1. og:image exists ------------------------------------------------- const og = /`. Comparing the card's source of truth against the rendered * `

` is therefore the same check, and the eyebrow is the literal the * endpoint sets. */ const isArticle = /^\/insights\/[^/]+\/$/.test(path); if (!card && !isArticle) { fail(`${path}: built, not a portrait page, and has no OG_CARDS entry`); continue; } let expected_card = card; if (!expected_card) { expected_card = await expectedArticleCard(path); if (!expected_card) { fail( `${path}: could not read a \`title:\` from this article's own .mdx, so ` + 'its card cannot be compared against anything. That is a failure, not ' + 'a skip — an unchecked card is the one surface check:claims cannot see.', ); continue; } } const expectedEyebrow = expected_card.eyebrow; const h1 = /]*>([\s\S]*?)<\/h1>/.exec(html); if (!h1) { fail(`${path}: no

to compare the card headline against`); } else { const text = strip(h1[1]); const expectedHeadline = expected_card.headline; if (text !== expectedHeadline) { fail( `${path}: card headline is not the page's

.\n` + `

: ${JSON.stringify(text)}\n` + ` card: ${JSON.stringify(expectedHeadline)}`, ); } else { checkedHeadlines += 1; if (isArticle) checkedArticles += 1; } } const eyebrow = /

]*>([\s\S]*?)<\/p>/.exec(html); if (!eyebrow) { fail(`${path}: no .eyebrow to compare the card eyebrow against`); } else { const text = strip(eyebrow[1]); if (text !== expectedEyebrow) { fail( `${path}: card eyebrow is not the page's first .eyebrow.\n` + ` page: ${JSON.stringify(text)}\n` + ` card: ${JSON.stringify(expectedEyebrow)}`, ); } } } /** * ⚠️ AND THE COVERAGE IS ASSERTED, NOT REPORTED. Printing "20 headlines matched" * beside a growing site is how the gap above stayed invisible: the number went up * and never went up ENOUGH, and nothing said so. Every built page except the * portrait pages carries a generated card, so the count must equal that or a page * was silently skipped. */ const shouldCheck = built.filter( ({ path }) => !PORTRAIT_PAGES.includes(path), ).length; if (checkedHeadlines !== shouldCheck) { fail( `only ${checkedHeadlines} of ${shouldCheck} non-portrait pages had their ` + 'card headline compared against their

. A page was skipped, which is ' + 'the failure this assertion exists to make loud.', ); } // ---- 3. no card generated for a page that does not exist ------------------- // A stray card is not a shipped defect, but it is the signature of a page that // was renamed or removed and a registry entry that was not — which the next // person reads as "the card exists, so the page must". let strays = []; try { const files = await readdir(join(DIST, 'og')); strays = files .filter((f) => f.endsWith('.jpg')) .map((f) => `/og/${f}`) .filter((p) => !seenCards.has(p)); } catch { fail('dist/og/ does not exist — no cards were generated'); } for (const s of strays) { fail(`${s}: generated, but no built page references it`); } // ---- optional contact sheet ---------------------------------------------- if (process.argv.includes('--sheet')) { const files = (await readdir(join(DIST, 'og'))) .filter((f) => f.endsWith('.jpg')) .sort(); const COLS = 3; const W = 400; const H = 210; const rows = Math.ceil(files.length / COLS); const tiles = await Promise.all( files.map(async (f, i) => ({ input: await sharp(join(DIST, 'og', f)) .resize(W, H) .toBuffer(), left: (i % COLS) * W, top: Math.floor(i / COLS) * H, })), ); const sheet = await sharp({ create: { width: COLS * W, height: rows * H, channels: 3, background: '#ffffff', }, }) .composite(tiles) .jpeg({ quality: 82 }) .toBuffer(); await writeFile(join(DIST, 'og-proof.jpg'), sheet); console.log( `contact sheet: dist/og-proof.jpg — ${files.length} cards, ${COLS}x${rows}`, ); } console.log( `og:proof — ${built.length} built pages, ${seenCards.size} distinct og:image ` + `targets, ${checkedHeadlines} card headlines matched their page

` + `(${checkedArticles} of them articles).`, ); if (problems.length) { console.error(`\nOG CARD PROBLEMS — ${problems.length}:`); for (const p of problems) console.error(` - ${p}`); process.exit(1); } console.log( 'OK — every og:image resolves, and no card asserts anything its page does not.', );