// publish-html.mjs — put the poster HTML where the app can fetch it, so the // browser can rasterise a poster instead of the repo shipping 42 MB of .webp. // // ★ WHY THIS SCRIPT EXISTS. PosterWall falls back to rendering posters/.html // in the browser when no pre-rendered image is present (library/packet/ // poster-raster.js). That fallback was shipped and verified against a locally // served file — but NOTHING PUBLISHED THE HTML. out/ is gitignored, public/ had // zero .html, so deleting the .webp would have taken poster download from working // to 404. The rasteriser worked; the feature would not have. This closes that gap. // // SCOPE — ZONE POSTERS ONLY, and that is a real distinction, not laziness: // zone posters token-free, drawn from bundled geoBoundaries. Reproducible // offline by anyone with the repo -> safe to stop shipping as // images, because the HTML regenerates the picture. // city posters need CITIES=1 BASEMAP=1 and fetch OSM STREET TILES at generation // time. NOT reproducible offline. Deleting those .webp would make // them unrecoverable without network, so they stay as images. // Treating "42 MB of posters" as one homogeneous pile would have deleted the // irreproducible half. // // ⚠ RUNNING THIS DOES NOT, BY ITSELF, SHRINK THE REPO. Measured with real git // packs, for the 24 zone posters that render: // webp 5.0 MB raw -> ~5.0 MB packed (already compressed; git cannot pack it) // HTML 21.4 MB raw -> 5.78 MB packed (text, and highly repetitive across posters) // Swapping one for the other is about +0.8 MB PACKED — a net loss, not the 42 MB // win the headline suggests. Do not ship that swap and call it a saving. // // WHERE THE REAL 22 MB IS: 176 city-* posters, which this script deliberately does // not touch because they need OSM tiles to regenerate. The zone posters were never // the expensive half. // // So what is this FOR? Two things that are true regardless of size: // 1. It is the missing half of the browser-rasterise fallback. That fallback // fetches posters/.html and nothing published it, so a zone whose image is // absent 404s today. Run this and the fallback has an input. // 2. It makes the HTML the SOURCE a reader can rasterise at any resolution, // instead of one fixed raster — useful for a host that would rather serve text // and let the browser do the work. // Use it when you are dropping the images deliberately, not as a size optimisation. // // node publish-html.mjs # all zone posters found in out/ // ONLY=laos node publish-html.mjs # one import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { dirname, join, resolve } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SRC = join(__dirname, 'out', 'posters'); const REPO = resolve(__dirname, '..', '..', '..'); // ⚠ data/posters, NOT public/assets/posters. public/assets/ is a GENERATED // MIRROR: generate-tree.mjs rmSync's it and re-copies data/ over it on every // `bun run tree`, so anything written there is deleted by the next build. This // script wrote there and its output therefore never survived — which is a large // part of why the browser-rasterise fallback still had no input months after it // shipped. data/ is the tracked source the mirror is made from. const DEST = join(REPO, 'data', 'posters'); const ONLY = process.env.ONLY || ''; if (!existsSync(SRC)) { console.error(`[publish-html] no ${SRC} — run \`node generate.mjs\` first.`); process.exit(1); } // ★ THE ZONE IDS THE APP CAN ACTUALLY REACH — ask the LEGEND, not clusters.json. // Enumerating the raw cluster keys is what this used to do, and it is wrong: // clusters.json carries 30 list definitions but the legend exposes 28 leaves. // deaf-old-world and deaf-new-world-asia-pacific are SUPERSEDED by the unified // `deaf` row (affinity bloc A017) — verified against live data, 63 + 60 = 123 = // A017 exactly, with zero groups on either side of the difference. They are kept // in clusters.json deliberately (three isolation-proof scripts assert // deaf_lists.length === 2), but no user can select them, so a poster for them is // unreachable. Enumerating them made this script warn that two zones were // "MISSING" when nothing was missing at all. const clusters = JSON.parse(readFileSync( join(REPO, 'plugin-designs', 'my-upg-100-list', 'data', 'clusters.json'), 'utf8')); const { buildLegendNodes, leafZones } = await import( pathToFileURL(join(REPO, 'plugin-designs', 'my-upg-100-list', 'data', 'legend-tree.js')).href); const zoneIds = new Set(leafZones(buildLegendNodes(clusters, { records: null })).map((z) => z.id)); mkdirSync(DEST, { recursive: true }); let published = 0; let bytes = 0; const skipped = []; for (const file of readdirSync(SRC)) { if (!file.endsWith('.html')) continue; const id = file.slice(0, -5); if (ONLY && id !== ONLY) continue; if (!zoneIds.has(id)) { skipped.push(id); continue; } const html = readFileSync(join(SRC, file), 'utf8'); writeFileSync(join(DEST, file), html); published += 1; bytes += Buffer.byteLength(html); } console.log(`[publish-html] ${published} poster HTML -> data/posters/`); console.log(`[publish-html] ${(bytes / 1048576).toFixed(1)} MB raw ` + `(gzips to roughly a third over the wire)`); if (skipped.length) { // Loud, not silent: a reader should know what was left behind and why. console.log(`[publish-html] skipped ${skipped.length} non-zone poster(s) ` + `(city-* posters belong to Key Cities and need OSM tiles to regenerate)`); } const missing = [...zoneIds].filter((z) => !existsSync(join(SRC, `${z}.html`))); if (missing.length && !ONLY) { console.warn(`[publish-html] ⚠ ${missing.length} zone(s) have NO poster HTML: ` + `${missing.slice(0, 6).join(', ')}${missing.length > 6 ? '…' : ''}`); console.warn('[publish-html] their download will 404 until generate.mjs covers them.'); }