Skip to content

ADR-079 — Asset-origin spine + source/derived separation + WebP delivery (closes #373 root cause; delivers mobile streaming)

Status · Accepted Date · 2026-07-07 Gates · #373 (GH Pages deploy flakiness) · mobile epic #149 S3 (#191) · v0.7.3 / v0.8

Gating sentence: heavy-media asset URLs resolve through a single assetUrl(path, kind) spine whose origin is configurable (local by default; chipi.github.io under the Capacitor __MOBILE__ build; an optional prod CDN later), build-time source (the 10,360-file i18n overlay tree, image masters) lives outside the served static/ tree so only derived outputs ship, and delivery images are WebP re-encoded locally from masters and committed — CI validates and deploys but never encodes.

Context

Two problems share one root cause.

  1. #373 — GH Pages deploys are a coin-flip. The built site is 2.1 GB across 30,812 files, over GitHub Pages' 1 GB and file-count soft limits; the publish step times out ~40% of the time. Measured 2026-07-07 on main: 10,360 of 17,161 static/ files (60%) are the raw per-entity i18n overlay tree (static/data/i18n/<locale>/…) — which is never fetched at runtime (the app loads the 14 collapsed {locale}.json bundles via loadI18nBundle; the raw tree is build-time source that build-i18n-bundles.mjs consumes). It only inflates the deploy because it is misfiled inside static/, which adapter-static copies wholesale. Separately, static/images is 1.6 GB of JPEG (5,308 files, 0 WebP) — the byte bulk.

  2. Mobile stream-heavy (ADR-078) needs the same heavy buckets (images, audio, non-default-locale data) served from chipi.github.io under the Capacitor build. A resolver audit found these URLs are built at ~9 scattered chokepoints with no central helper, and with a real inconsistency in audio (audio-registry stores ${base}${path_mp3} while colophon stores the raw path and prepends ${base} at render — a latent double-prepend bug).

Both trace to: too much media + too many files in one served build, scattered/inconsistent asset-URL construction, and build-time source misfiled as servable assets. Marko also flagged a future need for an optional CDN when Orrery moves to VPS prod — so the origin must be a configurable seam, not a mobile-only hack.

Decision

D1 — assetUrl() origin spine

Introduce one helper (src/lib/asset-url.ts) — assetUrl(path, kind) — through which every heavy-bucket URL (images, audio, per-locale i18n JSON) resolves. Its origin is configurable:

  • default: the SvelteKit base (browser dev / GH Pages / VPS prod) — behaviour unchanged.
  • __MOBILE__ build: https://chipi.github.io/orrery for pruned buckets (this is exactly the mobile S3 / #191 streaming switch — this ADR delivers #191).
  • future: an optional prod CDN origin via env, the same seam.

The ~9 chokepoints (pickHero; the 4 data.ts gallery builders; spacecraft-diagrams; pickVariant — which the 239 MB hotspots inherit; audio-registry; loadI18nBundle) route through it. The audio-base inconsistency is canonicalized here (one place applies base/origin; colophon aligns).

D2 — Source / derived separation

Build-time source must not live in the served tree.

  • i18n: move the raw overlay tree static/data/i18n/<locale>/ → a non-served i18n-src/; build-i18n-bundles.mjs reads from there and writes the 14 collapsed {locale}.json bundles into static/data/i18n/. Cuts ~60% of static/ file count from every build — the direct #373 file-count fix.
  • images: introduce a non-served masters/ store for originals; the build/pipeline re-encodes to derivatives that ship in static/images/.

Principle: same as .ts.js — keep source, ship the compiled derivative.

D3 — WebP-only delivery

Re-encode image masters → WebP (universal support ~97%+, ~30% smaller than JPEG, works with existing bare <img src> — no <picture> markup churn). No JPEG fallback and no AVIF for v1: the ~3% ancient-device gap is accepted (decided 2026-07-07). Because masters are retained (D2), JPEG fallback or AVIF can be re-encoded at any time with zero lock-in — deferred, not foreclosed.

D4 — Local encode, committed derivatives; CI never encodes

Image re-encode runs locally and deliberately (extending the existing curated image pipeline — fetch-assets.ts et al.), and the WebP derivatives are committed (GH Pages serves from git). CI remains a validator + deployer, never an encoder. Masters stay out of main git (git-LFS preferred; external bucket an option — settled at Slice C). Rationale: matches the existing local-curated + per-image-review model, keeps deploys fast/deterministic, avoids re-encoding 5,300 images on every push, and avoids putting masters on the CI bandwidth path.

Slices (tracked under #373)

  • A — i18n source relocation (D2 i18n). Cheapest, highest #373 impact. Move tree, repoint bundler, fix raw-path tests.
  • B — assetUrl() spine (D1). The refactor; canonicalizes audio base. Closes mobile #191.
  • C — WebP masters/derivatives (D2 images + D3 + D4). Largest: masters store + git-LFS decision + local encode pipeline over ~5,300 images.

Order A → B → C. A and B are tractable immediately; C is a larger, separable effort.

Consequences

Positive

  • #373 root cause fixed on two axes: file count (A) and bytes (C); deploys stop being a coin-flip.
  • Mobile streaming (#191) falls out of B for free — one spine serves mobile-now and optional-CDN-prod-later.
  • Audio-base double-prepend bug eliminated.
  • Git image weight drops (WebP replaces JPEG; masters leave main history).

Negative / risks

  • Broad refactor surface (B touches ~9 resolvers): a missed chokepoint = silent 404 in the mobile/CDN build. Mitigation: a mobile-build 404 sweep over every route before shipping.
  • ~3% ancient devices lose images under WebP-only (D3, accepted).
  • Masters storage adds a git-LFS/external dependency and process discipline (re-encode before commit).
  • Test churn: tests that read static/data/i18n/<locale>/… raw paths must repoint to i18n-src/ (A).

Rollback: A is a directory move + a bundler path change (revert = move back). B is env-gated and additive (the helper defaults to today's base; browser build unchanged). C is regenerable from masters and reversible to JPEG. No irreversible step.

References

  • #373 (deploy flakiness) · #191 (mobile gallery/audio streaming) · #376 (future KTX2 textures)
  • ADR-078 (mobile stream-heavy bundle — origin consumer of D1)
  • ADR-057 (locale cookie) · scripts/build-i18n-bundles.mjs · src/lib/image-hero.ts · src/lib/image-vision.ts · src/lib/data.ts

Orrery · ADR-079 · Asset-origin spine + source/derived separation + WebP · July 2026

Orrery — architecture documentation · MIT · No tracking