PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
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 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m32s
Two findings from review on #93, both verified before fixing. <img src> cannot vary by viewport, so it was always the wide desktop crop. A browser taking neither AVIF nor WebP therefore fell through to the desktop frame on a phone and lost the schoolhouse — the exact failure the two-crop <picture> exists to prevent, surviving in the one path nobody looks at. The band JPEG the build script already emitted was never referenced, which was the tell. It now backs a <source media> placed after the modern formats, so they still win wherever they are supported. Verified by stripping the AVIF and WebP <source>s at runtime and letting <picture> re-resolve, which is what an old browser actually sees: phone hero-band-500.avif → hero-band-700.jpg (band crop, school kept) desktop hero-wide-1672.avif → hero-wide-1200.jpg sharp was not declared: it arrives transitively from next@16.1.6, so the documented regeneration command works today and breaks on a Next upgrade or a clean install that resolves differently. Declared in devDependencies for the same reason next.config.js already declares its traced font files rather than trusting the tracer to keep finding them. The third finding — that the hero's licence is marked unconfirmed in CREDITS.md while the artwork ships — is accurate and deliberate. It is the owner's to answer; recording it as unknown is the point of the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
/**
|
|
* The landing hero artwork.
|
|
*
|
|
* This is the supplied illustration, not a drawing of one — see
|
|
* assets/hero-source.png and scripts/build-hero-images.js, which produces
|
|
* everything under public/brand/hero-*. Regenerate rather than hand-editing.
|
|
*
|
|
* Server-safe by construction: no 'use client', no hooks, no handlers, so it
|
|
* renders in the RSC pass and never reaches the client bundle.
|
|
*
|
|
* TWO CROPS, ONE <picture>
|
|
* ------------------------
|
|
* The slot is two different shapes. On desktop the artwork sits behind the
|
|
* whole panel at roughly 2.1:1 → 2.7:1, with the headline over the empty cream
|
|
* area the illustration reserves on its left. Below the one-column breakpoint
|
|
* it becomes a band between 2.6:1 and 4.9:1, under the search rather than
|
|
* behind it.
|
|
*
|
|
* A single file under `object-fit: cover` centre-crops, and at the band's
|
|
* extreme that slices a strip through the middle of the scene and loses the
|
|
* schoolhouse — exactly how the previous SVG hero failed on phones. So the
|
|
* <source media> switches crop, not just resolution: `band` is pre-cropped
|
|
* around the school and is already near 2.6:1, so the narrow band barely
|
|
* crops it further.
|
|
*
|
|
* `object-position` is set in CSS per breakpoint and is load-bearing — see
|
|
* .heroArt in HomeView.module.css.
|
|
*
|
|
* The artwork is decorative: the proposition beside it carries the meaning, so
|
|
* alt is empty and it is hidden from assistive tech rather than described.
|
|
*/
|
|
|
|
/*
|
|
* The panel is not the viewport. `.main` is capped at 1400px with 1.5rem of
|
|
* padding (1rem under 768px), so the artwork's box is that minus the padding —
|
|
* describing it as 100vw over-requests by ~48px worth of candidate at every
|
|
* width, which on a slow connection is a larger file than needed for the LCP
|
|
* element.
|
|
*/
|
|
const SIZES = '(min-width: 1400px) 1352px, (min-width: 769px) calc(100vw - 3rem), calc(100vw - 2rem)';
|
|
|
|
/** Intrinsic size of the wide crop — the aspect hint that prevents reflow. */
|
|
const WIDE_W = 1672;
|
|
const WIDE_H = 941;
|
|
|
|
export function HeroIllustration() {
|
|
return (
|
|
<picture>
|
|
{/* Band crop first: <source> is first-match-wins, so the narrow-viewport
|
|
rules have to precede the unconstrained desktop ones. */}
|
|
<source
|
|
media="(max-width: 860px)"
|
|
type="image/avif"
|
|
srcSet="/brand/hero-band-500.avif 500w, /brand/hero-band-700.avif 700w, /brand/hero-band-972.avif 972w"
|
|
sizes={SIZES}
|
|
/>
|
|
<source
|
|
media="(max-width: 860px)"
|
|
type="image/webp"
|
|
srcSet="/brand/hero-band-500.webp 500w, /brand/hero-band-700.webp 700w, /brand/hero-band-972.webp 972w"
|
|
sizes={SIZES}
|
|
/>
|
|
{/*
|
|
The band's JPEG, and the reason it exists.
|
|
|
|
<img src> cannot vary by viewport, so it is always the wide crop. A
|
|
browser that takes neither AVIF nor WebP would therefore fall through
|
|
to the desktop frame on a phone and lose the schoolhouse — the exact
|
|
failure this whole component is arranged to prevent, surviving in the
|
|
one path nobody looks at. No `type` here, so it matches anywhere the
|
|
media query does, and it sits after the modern formats so they still
|
|
win where supported.
|
|
*/}
|
|
<source media="(max-width: 860px)" srcSet="/brand/hero-band-700.jpg" />
|
|
<source
|
|
type="image/avif"
|
|
srcSet="/brand/hero-wide-900.avif 900w, /brand/hero-wide-1200.avif 1200w, /brand/hero-wide-1672.avif 1672w"
|
|
sizes={SIZES}
|
|
/>
|
|
<source
|
|
type="image/webp"
|
|
srcSet="/brand/hero-wide-900.webp 900w, /brand/hero-wide-1200.webp 1200w, /brand/hero-wide-1672.webp 1672w"
|
|
sizes={SIZES}
|
|
/>
|
|
{/*
|
|
The hero is the largest thing above the fold, so it is almost certainly
|
|
the LCP element: fetchPriority high, and never lazy. eslint's
|
|
no-img-element wants next/image, which cannot art-direct between two
|
|
different crops — that is the whole point of the <picture> above.
|
|
*/}
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src="/brand/hero-wide-1200.jpg"
|
|
alt=""
|
|
aria-hidden="true"
|
|
width={WIDE_W}
|
|
height={WIDE_H}
|
|
decoding="async"
|
|
fetchPriority="high"
|
|
/>
|
|
</picture>
|
|
);
|
|
}
|