diff --git a/nextjs-app/app/apple-icon.png b/nextjs-app/app/apple-icon.png new file mode 100644 index 0000000..fa11243 Binary files /dev/null and b/nextjs-app/app/apple-icon.png differ diff --git a/nextjs-app/app/apple-icon.tsx b/nextjs-app/app/apple-icon.tsx deleted file mode 100644 index 8e03a8f..0000000 --- a/nextjs-app/app/apple-icon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { ImageResponse } from 'next/og'; -import { markDataUri } from '@/components/Logo'; - -// iOS ignores SVG touch icons — it needs a real raster, on an opaque ground, -// with no transparency (it composites its own rounded mask). Previously this -// pointed at favicon.svg, so add-to-home-screen produced a blank tile. -// -// Teal ground with the mark knocked out in white, matching the app icon in the -// guideline's "brand in action" row. -export const size = { width: 180, height: 180 }; -export const contentType = 'image/png'; - -const TEAL = '#0F766E'; - -export default function AppleIcon() { - return new ImageResponse( - ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - -
- ), - size - ); -} diff --git a/nextjs-app/app/icon.png b/nextjs-app/app/icon.png new file mode 100644 index 0000000..6658290 Binary files /dev/null and b/nextjs-app/app/icon.png differ diff --git a/nextjs-app/app/icon.svg b/nextjs-app/app/icon.svg deleted file mode 100644 index 2a77b41..0000000 --- a/nextjs-app/app/icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/nextjs-app/app/opengraph-image.tsx b/nextjs-app/app/opengraph-image.tsx index 1883f82..e3af9a6 100644 --- a/nextjs-app/app/opengraph-image.tsx +++ b/nextjs-app/app/opengraph-image.tsx @@ -1,7 +1,6 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { ImageResponse } from 'next/og'; -import { markDataUri } from '@/components/Logo'; /** * The site had no og:image at all, so every link pasted into a class WhatsApp @@ -28,10 +27,21 @@ 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. + */ +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] = await Promise.all([ + const [regular, bold, mark] = await Promise.all([ font('Manrope-Regular.ttf'), font('Manrope-Bold.ttf'), + markDataUri(), ]); return new ImageResponse( @@ -62,7 +72,7 @@ export default async function OpengraphImage() { {/* Lockup */}
{/* eslint-disable-next-line @next/next/no-img-element */} - +
school compare diff --git a/nextjs-app/components/Footer.module.css b/nextjs-app/components/Footer.module.css index 2f51b3b..1b271e3 100644 --- a/nextjs-app/components/Footer.module.css +++ b/nextjs-app/components/Footer.module.css @@ -47,13 +47,14 @@ .lockupMark { display: inline-flex; flex: 0 0 auto; - width: 34px; + /* Height drives; the artwork is taller than it is wide. */ height: 34px; } -.lockupMark svg { - width: 100%; +.lockupMark img { + display: block; height: 100%; + width: auto; } /* Same wordmark as the header, inverted: "school" in the band's foreground, diff --git a/nextjs-app/components/Footer.tsx b/nextjs-app/components/Footer.tsx index 55ff700..68f0ef1 100644 --- a/nextjs-app/components/Footer.tsx +++ b/nextjs-app/components/Footer.tsx @@ -25,7 +25,7 @@ export function Footer() { */}

- + schoolcompare diff --git a/nextjs-app/components/Logo.tsx b/nextjs-app/components/Logo.tsx index 7809df0..3301f13 100644 --- a/nextjs-app/components/Logo.tsx +++ b/nextjs-app/components/Logo.tsx @@ -1,107 +1,84 @@ /** * The schoolcompare mark. * - * A location pin with a leaf growing inside it: where a school is, and a child - * growing there. The pin's tail sweeps left as the guideline's "signature path - * shape" — the journey a parent takes to find the right school — so the mark - * and the illustration style share one gesture. + * This is the supplied brand artwork, not a reconstruction of it. Two + * colourways were extracted from the logo sheet and live in public/brand: * - * This is the single source for the mark. app/icon.svg, app/apple-icon.tsx and - * app/opengraph-image.tsx all derive from the same geometry (see MARK_PATHS), - * because the header and the favicon had previously drifted into two different - * logos. + * mark.png the primary lockup's mark — teal pin, white window and + * path, green leaves. Reads correctly on Warm White, + * white cards and Sand, and on both dark-theme grounds. + * mark-on-dark.png the on-dark colourway — white pin with the counter + * knocked through to the ground. Required wherever the + * ground is teal, because there the teal pin's silhouette + * disappears and only the white window survives. * - * Drawn on a 48×48 grid. The pin takes the brand hue and the leaf is knocked - * out of it, so the mark needs exactly two colours and works on any ground. + * `variant="auto"` serves the on-dark artwork to dark-theme viewers via a + * source, so this needs no JavaScript and no client boundary. + * + * The wordmark beside it is live text in Manrope rather than the sheet's + * raster: the written style guide specifies Manrope, and live text stays + * selectable, scales cleanly and recolours with the theme. + * + * RESOLUTION CAVEAT: the largest instance on the supplied sheet is 153×189, + * which is ample for the header (36px), the favicon and the share card, but + * short of a 512px PWA icon — that one is upscaled and is slightly soft. Drop + * a vector (SVG/AI/EPS) into public/brand and regenerate to fix it; every + * consumer goes through this component or public/brand, so it is one swap. */ -/** Geometry shared by every rendering of the mark, on a 0 0 48 48 viewBox. */ -export const MARK_PATHS = { - /** The path tail, stroked — 8.5 wide with a round cap. */ - tail: 'M17 30 C15 35.6 11.6 40.2 7.8 42.8', - /** The pin head. */ - head: { cx: 26.5, cy: 18.5, r: 16.5 }, - /** Leaf stem, stroked — 2.6 wide with a round cap. */ - stem: 'M26.5 29.5 C26.5 25 26.6 21 27 18', - /** Upper leaf, filled. */ - leafUpper: 'M27 19.6 C28.2 13.8 32.4 9.8 37.6 9.4 C38 15.6 33.8 20.8 27 19.6 Z', - /** Lower leaf, filled. */ - leafLower: 'M26.4 24.6 C25.2 20 21.2 17 16.4 17.1 C16.2 21.8 19.8 25.8 26.4 24.6 Z', -} as const; +const MARK_LIGHT = '/brand/mark.png'; +const MARK_ON_DARK = '/brand/mark-on-dark.png'; -/** - * The mark as a standalone SVG string. - * - * Satori (next/og) renders `` with a data URI far more reliably than it - * renders inline SVG children, so the generated icon and share card both draw - * the mark this way — from the same geometry as the React component above, - * which is the whole point of MARK_PATHS. - */ -export function markSvg(pin: string, leaf: string): string { - return [ - '', - ``, - ``, - ``, - ``, - ``, - '', - ].join(''); -} - -/** The same string as a data URI, ready for ``. */ -export function markDataUri(pin: string, leaf: string): string { - return `data:image/svg+xml;base64,${Buffer.from(markSvg(pin, leaf)).toString('base64')}`; -} +/** Intrinsic aspect of the artwork, used to derive width from height. */ +const ASPECT = 153 / 189; interface LogoMarkProps { className?: string; - /** Rendered size in px. Defaults to inheriting via CSS. */ + /** Rendered height in px. Width follows the artwork's aspect ratio. */ size?: number; - /** The pin. Defaults to the brand token so it inverts with the theme. */ - pin?: string; - /** The leaf knocked out of the pin. */ - leaf?: string; + /** + * Which colourway to serve. + * - `auto` teal pin, swapping to the on-dark artwork in the dark theme + * - `onDark` always the white pin — for teal grounds, which are teal in + * both themes (the footer band) + */ + variant?: 'auto' | 'onDark'; title?: string; } -export function LogoMark({ - className, - size, - pin = 'var(--brand)', - leaf = 'var(--bg-card)', - title, -}: LogoMarkProps) { +export function LogoMark({ className, size = 36, variant = 'auto', title }: LogoMarkProps) { + const height = size; + const width = Math.round(size * ASPECT); + const alt = title ?? ''; + + if (variant === 'onDark') { + return ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ); + } + return ( - - {title ? {title} : null} - + + {/* eslint-disable-next-line @next/next/no-img-element */} + {alt} - - - - - + ); } diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css index 3bea92b..b372b01 100644 --- a/nextjs-app/components/Navigation.module.css +++ b/nextjs-app/components/Navigation.module.css @@ -57,13 +57,15 @@ .logoIcon { display: inline-flex; flex: 0 0 auto; - width: 36px; - height: 36px; + /* The artwork is taller than it is wide (153:189), so height drives the box + and width follows. Constraining both would squash the pin. */ + height: 38px; } -.logoIcon svg { - width: 100%; +.logoIcon img { + display: block; height: 100%; + width: auto; } /* diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 62107a9..d579238 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -102,14 +102,13 @@ export function Navigation() {
{/* - LogoMark's defaults are already correct for this ground: the pin - takes var(--brand) and the leaf is knocked out in var(--bg-card), - which is exactly the header's own background in both themes. No - explicit pin/leaf needed here — the footer, which sits on the - sunken teal band, does have to pass them. + variant="auto": the teal pin on the white header, swapping to the + on-dark artwork for dark-theme viewers. The footer has to force + onDark instead, because its band is teal in both themes and the + teal pin has no silhouette against it. */} - + schoolcompare diff --git a/nextjs-app/next.config.js b/nextjs-app/next.config.js index 0139401..54474c0 100644 --- a/nextjs-app/next.config.js +++ b/nextjs-app/next.config.js @@ -81,9 +81,10 @@ const nextConfig = { ], }, { - // favicon.svg was replaced by app/icon.svg (the mark now has one - // source in components/Logo.tsx); this rule was caching a 404. - source: '/icon.svg', + // The mark is now the supplied raster artwork, so the favicon is + // app/icon.png rather than an SVG. Pointing this at the old path was + // caching a 404. + source: '/icon.png', headers: [ { key: 'Cache-Control', diff --git a/nextjs-app/public/brand/mark-on-dark.png b/nextjs-app/public/brand/mark-on-dark.png new file mode 100644 index 0000000..d76e82b Binary files /dev/null and b/nextjs-app/public/brand/mark-on-dark.png differ diff --git a/nextjs-app/public/brand/mark.png b/nextjs-app/public/brand/mark.png new file mode 100644 index 0000000..e02914f Binary files /dev/null and b/nextjs-app/public/brand/mark.png differ diff --git a/nextjs-app/public/icon-192.png b/nextjs-app/public/icon-192.png index db38a01..cccb489 100644 Binary files a/nextjs-app/public/icon-192.png and b/nextjs-app/public/icon-192.png differ diff --git a/nextjs-app/public/icon-512.png b/nextjs-app/public/icon-512.png index 5fd5ab3..b5192da 100644 Binary files a/nextjs-app/public/icon-512.png and b/nextjs-app/public/icon-512.png differ diff --git a/nextjs-app/public/icon-maskable-512.png b/nextjs-app/public/icon-maskable-512.png index 82a913f..b3a22a6 100644 Binary files a/nextjs-app/public/icon-maskable-512.png and b/nextjs-app/public/icon-maskable-512.png differ diff --git a/nextjs-app/public/manifest.json b/nextjs-app/public/manifest.json index 7bfef57..639c5bf 100644 --- a/nextjs-app/public/manifest.json +++ b/nextjs-app/public/manifest.json @@ -8,9 +8,10 @@ "theme_color": "#0F766E", "icons": [ { - "src": "/icon.svg", - "sizes": "any", - "type": "image/svg+xml" + "src": "/icon.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any" }, { "src": "/icon-192.png",