/** * Fetches favicons for all linked domains and saves them locally to public/favicons/. * Run before astro build so favicons are served statically instead of via Google S2. * Idempotent: skips domains that already have a cached favicon. */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const root = join(__dirname, ".."); const linksPath = join(root, "src/content/links/links.json"); const outDir = join(root, "public/favicons"); function getDomain(url) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return null; } } function faviconPath(domain) { return join(outDir, `${domain}.png`); } async function fetchFavicon(domain) { const url = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const buf = await res.arrayBuffer(); return Buffer.from(buf); } async function main() { const links = JSON.parse(readFileSync(linksPath, "utf-8")); const domains = [ ...new Set(links.map((l) => getDomain(l.url)).filter(Boolean)), ]; if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); let fetched = 0; let skipped = 0; await Promise.all( domains.map(async (domain) => { const dest = faviconPath(domain); if (existsSync(dest)) { skipped++; return; } try { const data = await fetchFavicon(domain); writeFileSync(dest, data); fetched++; console.log(` ✓ ${domain}`); } catch (err) { console.warn(` ✗ ${domain}: ${err.message}`); } }), ); console.log( `Favicons: ${fetched} fetched, ${skipped} cached, ${domains.length} total`, ); } main().catch((err) => { console.error(err); process.exit(1); });