Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103// Assemble the pure-SVG per-country brand kit (avatar, favicon, two-colour wordmark logo — each// SVG + PNG) from the single source of truth (src/config/accents.ts + scopes.ts). All colours// derive from the accent packs; nothing is hand-picked here. The Bluesky BANNER is separate// (scripts/gen-banners.mjs — it composites the character-map, so it's a rendered raster, not SVG).//// node scripts/gen-brandkit.mjs [outdir] (default: dist/brandkit)//// Self-contained (renders every SVG from the packs, so it also covers friendly non-scope sites like// atproto.fr). Font-dependent (JetBrains Mono via fontconfig), like gen-icons/og-card — run it on a// machine with the font. Outputs one folder per site plus a README.import sharp from 'sharp';import { writeFile, mkdir } from 'node:fs/promises';import { fileURLToPath } from 'node:url';import { dirname, resolve } from 'node:path';import { ACCENT_PACKS } from '../src/config/accents.ts';import { SCOPES } from '../src/config/scopes.ts';
const here = dirname(fileURLToPath(import.meta.url));const root = resolve(here, '..');const outDir = resolve(process.cwd(), process.argv[2] ?? 'dist/brandkit');
// Dark tones the marks sit on (global.css dark theme).const TILE = '#1d2021';const INK_DARK = '#fbf1c7'; // inkstrong on darkconst INK_LIGHT = '#282828'; // inkstrong on light (light-bg logo)const MONO = "'JetBrains Mono', ui-monospace, monospace";
const hue = (pack, mode) => ACCENT_PACKS[pack][mode].accent;
// The sites: every built scope (with its two-colour pair) + friendly non-scope communities. atproto.fr// is an existing French community on their own design — we make them a proper lockup (blue/red).const SITES = [ ...Object.values(SCOPES).map((s) => ({ cc: s.cc, p: s.accent, s: s.accent2 })), { cc: 'fr', p: 'cobalt', s: 'red', external: true }, // Prospective countries: no site yet, but flag-derived assets ready if a community starts one. { cc: 'de', p: 'red', s: 'yellow', prospective: true },];
// --- favicon-<cc>.svg / avatar-<cc>.svg (two-colour, mirrors scripts/gen-brand.mjs) -------------// Avatar's slashes carry the pair like the wordmark (slash 1 = primary, slash 2 = secondary), cursor// stays primary; favicon has no slashes, so its bar splits top = primary / bottom = secondary.const faviconSvg = (cc, accent, accent2) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12.8" fill="${TILE}"></rect><text x="10.7" y="42" font-size="28" font-family="${MONO}" font-weight="700" letter-spacing="-0.04em" fill="${INK_DARK}">${cc}</text><rect x="45.4" y="22.8" width="7.8" height="10.1" fill="${accent}"></rect><rect x="45.4" y="32.9" width="7.8" height="10.1" fill="${accent2}"></rect></svg>\n`;
const avatarSvg = (cc, accent, accent2) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><rect width="1000" height="1000" rx="200" fill="${TILE}"></rect><text x="185" y="470" font-size="210" font-family="${MONO}" font-weight="700" letter-spacing="-0.04em"><tspan fill="${INK_DARK}">at:</tspan><tspan fill="${accent}">/</tspan><tspan fill="${accent2}">/</tspan></text><text x="185" y="711.5" font-size="210" font-family="${MONO}" font-weight="700" letter-spacing="-0.04em" fill="${INK_DARK}">${cc}</text><rect x="445" y="566" width="59" height="151" fill="${accent}"></rect></svg>\n`;
// --- //atproto.<cc> two-colour wordmark ---------------------------------------------------------// slash 1 + dot = a1 (primary), slash 2 = a2 (secondary), letters = ink.function wordmarkSvg(cc, a1, a2, ink, { w = 1440, h = 260, x = 30, y = 196, fs = 200, ls = -6 } = {}) { return ( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}">` + `<text x="${x}" y="${y}" font-family="${MONO}" font-weight="700" font-size="${fs}" letter-spacing="${ls}" xml:space="preserve">` + `<tspan fill="${a1}">/</tspan><tspan fill="${a2}" dx="${Math.round(fs * 0.05)}">/</tspan><tspan fill="${ink}">atproto.${cc}</tspan></text></svg>\n` );}
async function png(svg, size, out) { const opts = Array.isArray(size) ? { width: size[0], height: size[1] } : { width: size, height: size }; await sharp(Buffer.from(svg), { density: 300 }).resize(opts).png({ compressionLevel: 9 }).toFile(out);}
for (const site of SITES) { const { cc, p, s } = site; const dir = resolve(outDir, cc); await mkdir(dir, { recursive: true });
// favicon + avatar (two-colour: slashes/split bar carry the pair). const fav = faviconSvg(cc, hue(p, 'dark'), hue(s, 'dark')); const av = avatarSvg(cc, hue(p, 'dark'), hue(s, 'dark')); await writeFile(resolve(dir, `favicon-${cc}.svg`), fav); await writeFile(resolve(dir, `avatar-${cc}.svg`), av); await png(fav, 512, resolve(dir, `favicon-${cc}.png`)); await png(av, 1000, resolve(dir, `avatar-${cc}.png`));
// two-colour wordmark logo: light-bg (dark letters) + dark-bg (cream letters) variants. const logoLight = wordmarkSvg(cc, hue(p, 'light'), hue(s, 'light'), INK_LIGHT); const logoDark = wordmarkSvg(cc, hue(p, 'dark'), hue(s, 'dark'), INK_DARK); await writeFile(resolve(dir, `logo-${cc}.svg`), logoLight); await writeFile(resolve(dir, `logo-${cc}-dark.svg`), logoDark); await png(logoLight, [1440, 260], resolve(dir, `logo-${cc}.png`)); await png(logoDark, [1440, 260], resolve(dir, `logo-${cc}-dark.png`));
const tag = site.external ? ' (external)' : site.prospective ? ' (proposed)' : ''; console.log(`brandkit/${cc}: avatar, favicon, two-colour logo (+dark) — SVG + PNG${tag}`);}
const readme = `# atproto.<cc> brand kit\n\n` + `Generated from the code repo's single source of truth (\`src/config/accents.ts\` + \`scopes.ts\`)\n` + `by \`scripts/gen-brandkit.mjs\`. Do not hand-edit — rerun the generator after a colour change.\n\n` + `One folder per site. Each contains, as **SVG + PNG**:\n\n` + `- \`avatar-<cc>\` — square profile avatar (dark tile, \`at://\` + cc; the two slashes carry the pair,\n` + ` slash 1 primary / slash 2 secondary, cursor primary). PNG 1000×1000.\n` + `- \`favicon-<cc>\` — 64px browser mark (cc + a two-tone bar: top primary / bottom secondary). PNG 512×512.\n` + `- \`logo-<cc>\` — the **two-colour** \`//atproto.<cc>\` wordmark (flag-derived: slash 1 primary, slash 2\n` + ` secondary), transparent. Light-bg variant (dark letters); \`-dark\` variant (cream letters). PNG 1440×260.\n` + `- \`banner-<cc>.png\` — Bluesky profile banner (light, tagline headline + faint map, avatar-safe).\n` + ` 3000×1000. Generated separately by \`scripts/gen-banners.mjs\` (it composites the character-map).\n\n` + `Sites: ${SITES.map((s) => s.cc + (s.external ? ' (friendly, external)' : s.prospective ? ' (proposed)' : '')).join(', ')}.\n`;await writeFile(resolve(outDir, 'README.md'), readme);console.log(`\nwrote kit to ${outDir}`);