Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566// Rasterise the per-scope app-icon PNGs (apple-touch-icon + PWA manifest + JSON-LD/OAuth logo)// from the generated avatar SVGs (public/brand/avatar-<cc>.svg — themselves generated by// gen-brand.mjs from src/config/accents.ts). This replaces the old bespoke orange "@" avatar// (atproto-nl-avatar-*.png), which was hard-coded nl on every domain.//// node scripts/gen-icons.mjs (npm run gen:icons)//// Font-dependent (JetBrains Mono via fontconfig), like the OG cards, so it is a MANUAL step run on// a machine with the font — deliberately NOT part of the deterministic check:brand drift guard.import sharp from 'sharp';import { readFile, writeFile } from 'node:fs/promises';import { fileURLToPath } from 'node:url';import { dirname, resolve } from 'node:path';import { SCOPES } from '../src/config/scopes.ts';
const here = dirname(fileURLToPath(import.meta.url));const brand = resolve(here, '..', 'public', 'brand');
// 180 = apple-touch-icon; 192 + 512 = PWA manifest; 512 doubles as the JSON-LD / OAuth logo.const SIZES = [180, 192, 512];// The .ico fallback (legacy browsers, feed readers, Google's favicon scraper on the bare// /favicon.ico path) bundles these raster sizes. Per-scope so every domain gets its own colours.const ICO_SIZES = [16, 32, 48];
// Wrap PNG buffers in an ICO container. ICO has embedded-PNG support (Vista+), so no re-encoding:// 6-byte ICONDIR header + one 16-byte ICONDIRENTRY per image + the PNG payloads back to back.function pngToIco(images) { const header = Buffer.alloc(6); header.writeUInt16LE(1, 2); // type: 1 = icon header.writeUInt16LE(images.length, 4); const entries = Buffer.alloc(16 * images.length); let offset = 6 + 16 * images.length; images.forEach(({ size, buf }, i) => { const e = i * 16; entries.writeUInt8(size >= 256 ? 0 : size, e); // width (0 = 256) entries.writeUInt8(size >= 256 ? 0 : size, e + 1); // height entries.writeUInt16LE(1, e + 4); // colour planes entries.writeUInt16LE(32, e + 6); // bits per pixel entries.writeUInt32LE(buf.length, e + 8); // payload bytes entries.writeUInt32LE(offset, e + 12); // payload offset offset += buf.length; }); return Buffer.concat([header, entries, ...images.map((im) => im.buf)]);}
for (const s of Object.values(SCOPES)) { const svg = await readFile(resolve(brand, `avatar-${s.cc}.svg`)); for (const size of SIZES) { const out = resolve(brand, `avatar-${s.cc}-${size}.png`); // High render density then downscale keeps the mono text crisp at every size. await sharp(svg, { density: 300 }).resize(size, size).png({ compressionLevel: 9 }).toFile(out); console.log(`wrote public/brand/avatar-${s.cc}-${size}.png`); }
// Per-scope favicon.ico from the (small-optimised) favicon SVG, not the avatar. const favSvg = await readFile(resolve(brand, `favicon-${s.cc}.svg`)); const images = await Promise.all( ICO_SIZES.map(async (size) => ({ size, buf: await sharp(favSvg, { density: 300 }).resize(size, size).png({ compressionLevel: 9 }).toBuffer(), })), ); await writeFile(resolve(brand, `favicon-${s.cc}.ico`), pngToIco(images)); console.log(`wrote public/brand/favicon-${s.cc}.ico`);}