/**
* Generates the social card and the icon set into `public/`.
*
* Run by hand after a brand change; the output is committed. This is deliberately not
* wired into `pnpm build` — the site must build on a machine with no browser, and these
* files change roughly never.
*
* node scripts/make-assets.mjs
*
* Playwright and sharp are not dependencies of this workspace. Both arrive transitively
* under `astro` (which ships sharp for its image pipeline) and are resolved through it,
* the same trick `bones.mjs` relies on for its capture browser. If that ever stops
* working, `pnpm dlx playwright@1` and `pnpm dlx sharp-cli` do the same job.
*
* The card is drawn in a real browser rather than assembled by hand because it uses the
* site's own fonts and tokens: a social card that does not look like the site is worth
* less than no card. The moai is the same rect geometry as `components/Moai.astro`.
*/
import { createRequire } from 'node:module';
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const publicDir = path.join(root, 'public');
const fontsDir = path.join(root, 'src', 'assets', 'fonts');
// astro's entry point; requiring from there reaches what pnpm hoisted under it.
const require = createRequire(import.meta.resolve('astro'));
const { chromium } = require('playwright');
const sharp = require('sharp');
/* The pixel moai, identical to components/Moai.astro. viewBox is 11x13. */
const MOAI_RECTS = [
[2, 0, 7, 1, '#C9BFA4'],
[1, 1, 9, 2, '#C9BFA4'],
[1, 3, 2, 7, '#A89B7C'],
[3, 3, 6, 7, '#C9BFA4'],
[3, 4, 2, 1, '#5C5340'],
[7, 4, 2, 1, '#5C5340'],
[5, 5, 1, 3, '#A89B7C'],
[2, 10, 8, 2, '#A89B7C'],
];
const moaiSvg = (width) =>
``;
const fontFace = (family, weight, file) => `
@font-face {
font-family: '${family}';
font-style: normal;
font-weight: ${weight};
src: url('file://${path.join(fontsDir, file)}') format('woff2');
}`;
/* The 1200x630 card. Design tokens copied from styles/global.css. */
const cardHtml = `
${moaiSvg(40)}Cashumints.space
Find a Cashu mint you can trust
Every mint on the Nostr network, with community reviews you can verify yourself.
cashumints.space
`;
/* The icon master: the moai on the site's background, generous padding. */
const iconHtml = `${moaiSvg(320)}`;
/**
* A 32x32 ICO wrapping a PNG payload.
*
* sharp cannot write ICO and the format needs no library for this case: the container
* is a 6-byte directory header, one 16-byte entry, then the PNG bytes verbatim. Worth
* having because bare `/favicon.ico` requests from crawlers and feed readers still
* happen, and a 404 there is noise in the log for no reason.
*/
function icoFromPng(png) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserved
header.writeUInt16LE(1, 2); // type: icon
header.writeUInt16LE(1, 4); // one image
const entry = Buffer.alloc(16);
entry[0] = 32; // width
entry[1] = 32; // height
entry[2] = 0; // palette size: 0 means truecolour
entry[3] = 0; // reserved
entry.writeUInt16LE(1, 4); // colour planes
entry.writeUInt16LE(32, 6); // bits per pixel
entry.writeUInt32LE(png.length, 8);
entry.writeUInt32LE(header.length + entry.length, 12);
return Buffer.concat([header, entry, png]);
}
await mkdir(publicDir, { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
await page.setContent(cardHtml, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: path.join(publicDir, 'og.png') });
console.log('og.png');
await page.setViewportSize({ width: 512, height: 512 });
await page.setContent(iconHtml, { waitUntil: 'load' });
const master = await page.screenshot({ type: 'png' });
for (const [name, size] of [
['icon-512.png', 512],
['icon-192.png', 192],
['favicon-32.png', 32],
]) {
await sharp(master).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(publicDir, name));
console.log(name);
}
// iOS ignores transparency and composites on white, so flatten onto the brand ground.
await sharp(master)
.resize(180, 180)
.flatten({ background: '#0E0D0B' })
.png({ compressionLevel: 9 })
.toFile(path.join(publicDir, 'apple-touch-icon.png'));
console.log('apple-touch-icon.png');
const png32 = await sharp(master).resize(32, 32).png({ compressionLevel: 9 }).toBuffer();
await writeFile(path.join(publicDir, 'favicon.ico'), icoFromPng(png32));
console.log('favicon.ico');
// The standalone SVG favicon: the same mark, at any size, for browsers that take it.
await writeFile(path.join(publicDir, 'favicon.svg'), moaiSvg(11));
console.log('favicon.svg');
} finally {
await browser.close();
}