doxa-maps/documentation/architecture/poster-generation.md
doxa-maps-88655c70 9dc4dda8cf
Some checks are pending
build-gate / build (push) Waiting to run
consolidate docs into documentation/ and deployment shells into deployment/
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00

4.3 KiB
Raw Blame History

Architecture — UPG-100 poster generation

The simple overview is in library/scripts/posters/README.md. This is the full reference for the generator: files, environment, pipeline, and the gotchas that have bitten before.

The poster generator for the my-upg-100-list profile renders one 3ft × 3ft print poster per zone (28 today) from the profile's own data.

It lives inside doxa-maps on purpose. It used to sit outside the repo in a sibling generator folder and reach in for data via a UPG100_DATA_PATH env var. That cost real bugs: forget the var and it silently rendered a stale duplicate of clusters.json — which is how Nepal, and later Bangladesh + Bhutan, vanished from posters that looked fine otherwise. The generator and the data it reads now ship together.

Run it

bun run posters            # all 28 posters      -> out/posters/*.html
ONLY=asia-india-tamil-nadu bun run posters:one   # just one (fast, ~15s)
bun run posters:shoot      # HTML -> PNG @3000x3000 (Playwright/Firefox)
bun run posters:gallery    # review pages        -> out/index.html + out/hd.html
bun run posters:export     # region folders      -> ../../../upg-100-posters/

The full pipeline, in order:

posters  ->  posters:shoot  ->  (convert to WebP previews)  ->  bun run build

The WebP step feeds plugin-designs/my-upg-100-list/assets/previews/, which is what the poster wall shows in the app. A template change is not visible in the app until previews are regenerated and the app is rebuilt.

Files

File What it does
generate.mjs THE template. Reads the profile data, writes one HTML poster per zone.
shoot.mjs Screenshots each poster to PNG (Firefox — never Chromium; privacy-first policy).
poster-regions.mjs The region map (India, Africa, …). Shared by both gallery builders.
build-gallery.mjs out/index.html — browsable preview grid, sectioned by region.
build-hd-gallery.mjs out/hd.html — full-resolution review + copy-to-clipboard.
export-public.mjs Copies PNGs into region folders OUTSIDE the repo (see below).
snapshots/ Frozen template versions. v5 is the approved current; v1–v4 are the lineage.

Environment

Var Default Notes
SPARSE=1 — Required. Selects the approved template. Without it you get the older full-vision layout.
FORMAT=3x3 3x4 1500×1500 square sheet.
OFFLINE=1 — Use the cached out/_records.json instead of calling the API.
VITE_API_BASE_URL — Satisfies the fail-closed config gate. No request is made under OFFLINE=1.
ONLY — Build a single zone by zoneKey.
UPG100_DATA_PATH the profile in this repo Only override to test against other data.
SERIES_INDEX / SERIES_NAME derived / List of 100 UPG's Series line on the sheet.

Two things that will bite you

1. The exported PNGs must NOT go in public/. capacitor.config.json sets webDir: "public" and Capacitor packages that directory wholesale — no per-path exclude. The export is ~52 MB, and the per-bundle vite build also fans the served root into every MFE, so it duplicated 11× (631 MB) when it lived there. export-public.mjs therefore writes to ../../../upg-100-posters/, a sibling of the repo. Desktop/web can serve from there or a CDN; the phone gets posters from a CDN or generates on device.

2. Fail-loud guards, not silent overlaps. generate.mjs throws if the header would be drawn over the map frame, or if the below-map block would run off the sheet. Raise the constant or tighten the copy — never delete the guard. Both have caught real regressions (west-africa, sudan, gujarat, oceania-indonesia).

Measurement discipline

SVG <text> does not word-wrap, so all multi-column setting is measured in the generator via CHADV -> textW -> wrapPx -> paraInto. That estimator is calibrated for Lato at body sizes and is wrong elsewhere — measured drift: Georgia bold display type runs ~1.35× wider than predicted, bold digits ~0.98em against an assumed 0.63em, mixed bold/regular runs ~10% narrower. Every size that must fit a box is therefore taken from the rendered artefact (getBBox), not from the formula. When adding new display text, render it and measure before trusting a computed size.