feat(brand): phase 2 — the branding pipeline #5
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/phase-2-branding"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 of
PLAN.md§13: the branding pipeline. §7's promise — swapping a logo or recolouring the site is a file copy and a container restart, never a rebuild — now has a mechanism and two checks behind it.The three decisions you approved
Recorded in
PLAN.md§7 as D14–D16 so a future reader finds them next to the design they change.D14 — one raster in, every size out.
brand-default/holds a singlelogo.png. The header mark at three pixel ratios, both install icons, the apple-touch icon, the three favicon sizes and a real multi-resolutionfavicon.icoare all derived on request from whicheverlogo.pngis in force, cached in memory, limited to an allowlist of sizes. Shipping fifteen precomputed files would have meant an operator producing fifteen to change a mark — and the realistic outcome of that is a deployment with a new header and the old favicon.D15 — brand text is applied at boot.
npm startnow runsscripts/applyBrand.mjsbefore the server opens a socket. Every page stays prerendered, Pagefind keeps static HTML to index, and the docs are covered by the same pass as the marketing pages.D16 — the header shows the real emblem. Phase 1's placeholder glyph is gone. The site, the product and the Android launcher icon are one mark. The cost, taken knowingly: it is raster art, so
theme.csscannot recolour it — replacinglogo.pngis how the mark changes.Two defects found while proving it
Both were silent, and neither would have shown up in a build log.
The mounted
theme.cssdid not win. Astro emits its own stylesheet after the links written in the page head, so linking the operator's last was not enough — the site's tokens landed after theirs and every override was a no-op.tokens.cssnow lives in@layer tokensand the mounted file is unlayered, which beats it wherever the browser encounters it. This takes order out of the mechanism rather than getting the order right, because the next person to touch the head would have got it wrong again.The documentation was a different site. Starlight builds its own
<head>, so the docs linked a Starlight-default/favicon.svgthat does not exist here (a 404 on every docs page), carried no manifest and no OG card, and never loaded the brand stylesheet at all. A mounted theme recoloured the marketing pages and left the documentation stock — half a rebrand, which reads as a bug in the product rather than a step somebody missed. Fixed with aHeadoverride.What was verified, and how
Against a real bind mount holding a deliberately unmistakable magenta logo, a
theme.cssand a renamedbrand.json:theme.css→mount,logo-40.webp→derived:mount,og-image.png→default. Overriding one file leaves the rest stock, as §7 requires.--bgand the body background both follow the mount, on the marketing pages and the docs.If-None-Match→304.404, andcheckBrandfails the build before it can ship.git archive HEADinto a clean directory,npm ci, all four checks, build, and a live smoke of/brand/*and the manifest there.checkBrand.mjswas negative-tested three ways before being trusted — an off-allowlist size, asiteNametoo short to replace safely, and a value nested inside another value — and each failed the build with the right message.Worth knowing if you touch this later
TEXT_FIELDSinapplyBrand.mjs, or §7 quietly stops being true for that field.checkBrandfails if the two lists disagree.applyBrandrewrites from a record,dist/.brand-applied.json, not from the defaults. The naive version works exactly once and then ignores every later edit.brand.jsonis logged and ignored, not fatal. A marketing site that is up with stock branding beats one that is down with correct branding. Reasoned out at the call site.brand-default/wordmark.svgandog-image.pngare generated and committed bynpm run brand:assets, which reads the emblem and Cinzel's outlines from the sibling checkouts. CI needs neither the artwork nor a font.NaNcoordinates at a non-zero origin for some glyphs, and an SVG path parser stops at the first malformed command — so the first lockup rendered "Runic Gate" and looked like a typo rather than a bug. There is now an assertion that fails the generator instead.favicon.icois written by hand — sharp cannot encode ICO, and the format is a 6-byte header plus PNG payloads. Cheaper than a dependency.Not in this PR
The demo slot's markup (phase 3 writes it; the rewrite rule and the CSS that hides it are here and were tested against a fixture),
checkLinks.mjs, and the CSP header.contactEmailrewrote 0 times because no page shows it until/privacylands in phase 6 — expected, not a failure.