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.
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
import { readFile } from 'node:fs/promises';
|
||
import { join } from 'node:path';
|
||
import { ImageResponse } from 'next/og';
|
||
|
||
/**
|
||
* The site had no og:image at all, so every link pasted into a class WhatsApp
|
||
* group or a Slack channel rendered as a bare grey text card — on the highest
|
||
* intent channel this product has.
|
||
*
|
||
* The card is the brand's own proposition on the Sand ground, with the mark
|
||
* and a suggestion of the illustration's rolling landscape along the bottom.
|
||
* It states the promise rather than decorating it.
|
||
*/
|
||
export const alt = 'schoolcompare — find the school where your child will flourish';
|
||
export const size = { width: 1200, height: 630 };
|
||
export const contentType = 'image/png';
|
||
|
||
const SAND = '#F5EFE6';
|
||
const INK = '#1C2731';
|
||
const MUTED = '#5F6A75';
|
||
const TEAL = '#0F766E';
|
||
const SAGE = '#A7D7C5';
|
||
const SAGE_DEEP = '#7FC3AC';
|
||
const SAGE_PALE = '#D6EDE2';
|
||
|
||
async function font(file: string) {
|
||
return readFile(join(process.cwd(), 'assets', file));
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
* 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() {
|
||
const png = await readFile(join(process.cwd(), 'public', 'brand', 'mark.png'));
|
||
return `data:image/png;base64,${png.toString('base64')}`;
|
||
}
|
||
|
||
export default async function OpengraphImage() {
|
||
const [regular, bold, mark] = await Promise.all([
|
||
font('Manrope-Regular.ttf'),
|
||
font('Manrope-Bold.ttf'),
|
||
markDataUri(),
|
||
]);
|
||
|
||
return new ImageResponse(
|
||
(
|
||
<div
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'space-between',
|
||
background: SAND,
|
||
padding: '58px 72px 0',
|
||
fontFamily: 'Manrope',
|
||
position: 'relative',
|
||
}}
|
||
>
|
||
{/* Rolling hills along the bottom edge — the illustration style,
|
||
reduced to the one gesture that survives at thumbnail size. */}
|
||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 210, display: 'flex' }}>
|
||
<svg width="1200" height="210" viewBox="0 0 1200 210">
|
||
<path d="M0 92 C 200 56 380 100 560 78 C 760 54 940 92 1200 62 L1200 210 L0 210 Z" fill={SAGE_PALE} />
|
||
<path d="M0 132 C 230 96 420 140 620 118 C 830 94 1000 130 1200 106 L1200 210 L0 210 Z" fill={SAGE} />
|
||
<path d="M0 176 C 260 146 470 186 690 166 C 900 146 1040 174 1200 158 L1200 210 L0 210 Z" fill={SAGE_DEEP} />
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Lockup */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={mark} width={47} height={56} alt="" />
|
||
<div style={{ display: 'flex', fontSize: 34, fontWeight: 700, letterSpacing: '-0.04em' }}>
|
||
<span style={{ color: INK }}>school</span>
|
||
<span style={{ color: TEAL }}>compare</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Proposition. Sized so the headline holds two lines and the subhead
|
||
one — an orphaned word reads as an accident at this size. */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 18, paddingBottom: 236 }}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
fontSize: 72,
|
||
fontWeight: 700,
|
||
letterSpacing: '-0.035em',
|
||
lineHeight: 1.05,
|
||
color: INK,
|
||
maxWidth: 900,
|
||
}}
|
||
>
|
||
<span style={{ marginRight: 18 }}>Find the school where</span>
|
||
<span style={{ marginRight: 18 }}>your child will</span>
|
||
<span style={{ color: TEAL }}>flourish.</span>
|
||
</div>
|
||
<div style={{ display: 'flex', fontSize: 27, color: MUTED, letterSpacing: '-0.005em' }}>
|
||
24,000+ schools in England · SATs, GCSEs, Ofsted and admissions
|
||
</div>
|
||
</div>
|
||
</div>
|
||
),
|
||
{
|
||
...size,
|
||
fonts: [
|
||
{ name: 'Manrope', data: regular, weight: 400, style: 'normal' },
|
||
{ name: 'Manrope', data: bold, weight: 700, style: 'normal' },
|
||
],
|
||
}
|
||
);
|
||
}
|