fix(brand): give both logo colourways one canvas so the aspect hint is right
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s

LogoMark derived width from a single hardcoded 153/189, taken from mark.png,
while mark-on-dark.png was 98×112. Both call sites size the mark with
height:100%/width:auto, so the rendered size came out right in the end — but
the width/height attributes are the pre-load aspect hint, so the on-dark
artwork reserved the wrong box and reflowed on load. Any future call site
without that CSS would have rendered it stretched outright.

Per-variant attributes would not have fixed it. variant="auto" is a single
<img> whose srcset swaps the file underneath it, so one set of attributes has
to serve both colourways; the only correct fix is for the two files to share
a ratio.

They also had different artwork ratios (0.810 against 0.833) and different
padding, which the review did not reach: the logo changed size slightly
whenever the OS theme flipped. Both are now written onto one 176×208 canvas
with the artwork at the same height, centred, so they are drop-in swappable.

Measured on the built output, header mark, before → after:
  light   attr 31×38, rendered 30.75×38   →   attr 32×38, rendered 32.15×38
  dark    attr 31×38, rendered 33.25×38   →   attr 32×38, rendered 32.15×38
A 2.25px reflow and a cross-theme size jump both become 0.15px of rounding.

The share card reads the same file and had its own hardcoded 45×56, which the
re-crop would have stretched; it is now 47×56, with a comment tying it to the
canvas. The comment on ASPECT records that re-cropping either file means
re-normalising both.

Verified: tsc clean, 159/159 tests, build green, and both files report
naturalWidth 176 × naturalHeight 208 in the browser.
This commit is contained in:
Tudor
2026-08-08 22:39:28 +01:00
parent b11ee3c8de
commit 9ee4d45a94
4 changed files with 23 additions and 3 deletions
+3 -1
View File
@@ -31,6 +31,8 @@ async function font(file: string) {
* The mark, as a data URI. Satori has no access to the public/ URL space, so * The mark, as a data URI. Satori has no access to the public/ URL space, so
* the artwork is read off disk and inlined. This is the same file the header * the artwork is read off disk and inlined. This is the same file the header
* serves — public/brand/mark.png — so the card can never drift from the site. * serves — public/brand/mark.png — so the card can never drift from the site.
* The 47×56 box below is that file's 176:208 ratio; re-crop the artwork and
* this has to move with it, or Satori will stretch it.
*/ */
async function markDataUri() { async function markDataUri() {
const png = await readFile(join(process.cwd(), 'public', 'brand', 'mark.png')); const png = await readFile(join(process.cwd(), 'public', 'brand', 'mark.png'));
@@ -72,7 +74,7 @@ export default async function OpengraphImage() {
{/* Lockup */} {/* Lockup */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img src={mark} width={45} height={56} alt="" /> <img src={mark} width={47} height={56} alt="" />
<div style={{ display: 'flex', fontSize: 34, fontWeight: 700, letterSpacing: '-0.04em' }}> <div style={{ display: 'flex', fontSize: 34, fontWeight: 700, letterSpacing: '-0.04em' }}>
<span style={{ color: INK }}>school</span> <span style={{ color: INK }}>school</span>
<span style={{ color: TEAL }}>compare</span> <span style={{ color: TEAL }}>compare</span>
+20 -2
View File
@@ -12,6 +12,10 @@
* ground is teal, because there the teal pin's silhouette * ground is teal, because there the teal pin's silhouette
* disappears and only the white window survives. * disappears and only the white window survives.
* *
* Both are written onto one 176×208 canvas with the artwork at the same
* height, so they are drop-in swappable — see ASPECT below for why that
* matters rather than merely being neat.
*
* `variant="auto"` serves the on-dark artwork to dark-theme viewers via a * `variant="auto"` serves the on-dark artwork to dark-theme viewers via a
* <picture> source, so this needs no JavaScript and no client boundary. * <picture> source, so this needs no JavaScript and no client boundary.
* *
@@ -29,8 +33,22 @@
const MARK_LIGHT = '/brand/mark.png'; const MARK_LIGHT = '/brand/mark.png';
const MARK_ON_DARK = '/brand/mark-on-dark.png'; const MARK_ON_DARK = '/brand/mark-on-dark.png';
/** Intrinsic aspect of the artwork, used to derive width from height. */ /**
const ASPECT = 153 / 189; * Intrinsic size of the artwork files, used to derive width from height.
*
* Both colourways are deliberately written onto the SAME canvas at the same
* artwork height, so this one ratio is correct for either of them. That is not
* tidiness — it is required. `variant="auto"` renders a single <img> whose
* srcset swaps the file underneath it, so the width/height attributes are
* shared by both colourways and cannot be varied per file. When the two were
* tightly cropped they had different ratios (0.810 and 0.875) and different
* padding, which meant a wrong aspect hint before load — a reflow on load
* under `width: auto` — and a visible jump in logo size whenever the OS theme
* flipped. Re-crop or replace one file and you must re-normalise both.
*/
const ART_WIDTH = 176;
const ART_HEIGHT = 208;
const ASPECT = ART_WIDTH / ART_HEIGHT;
interface LogoMarkProps { interface LogoMarkProps {
className?: string; className?: string;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 54 KiB