Repository navigation
feat(photofeature): crop hero images to their focal point - #156
Conversation
The photofeature hero fills a fixed h-screen box with object-cover and no object-position, so CSS crops evenly from both edges at 50% 50%. News subjects sit in the upper third, so the top crop takes faces first — worse the taller and narrower the viewport gets. Writes up four approaches with their migration cost and failure modes, and recommends one. The material finding is that media.focalX / focalY already exist — in the initial baseline, in the production SQL path, and in payload-types — and Payload enables the focal-point selector on upload collections by default, which Media never disables. Editors can already set a focal point; nothing in the frontend has ever read it. No code changes. Opened to settle the approach before implementing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The photofeature hero fills a fixed h-screen box with object-cover and no object-position, so CSS cropped evenly from both edges at 50% 50%. A 3:2 photo in a 16:9 viewport loses about a quarter of its height, split top and bottom, and news subjects sit in the upper third — so the top crop took faces first, worse the taller and narrower the viewport got. media.focalX / focalY already existed and nothing read them. They are in the initial baseline, in the production SQL path, and in payload-types, and Payload enables the focal-point selector on upload collections by default, which Media never disables. So editors could already set a focal point; the frontend simply ignored it. utils/focalPoint.ts turns that stored point into an object-position, clamped to 0-100 and defaulting to dead centre when unset. The photofeature header applies it to every photo it crops: the hero, author headshots, and write-in author photos. Reading the column was necessary but not sufficient. The article route sanitises media through a field whitelist before handing it to the client component — toPublicArticleMedia, and toPublicArticleUser for headshots — and that whitelist dropped focalX/focalY, so the component always saw undefined and fell back to centre. Both sanitisers now carry the focal point through. Verified end to end against a real photofeature by moving the point in the database and watching the crop follow: unset -> 50% 50%, 62/18 -> 62% 18%, 12/8 -> 12% 8%, and 150/-20 -> 100% 0% clamped. No migration: the columns already exist in both paths. Deliberately leaves out a top-biased default for images with no focal point. Payload writes 50/50 for any upload whose point has never been moved, so an untouched image cannot be told apart from a deliberately centred one, and a blanket bias would silently override editors who meant centre. Every image in the database currently sits at 50/50, so this changes nothing on screen until an editor drags a point — it makes the fix possible rather than retroactively re-framing existing photofeatures. pnpm typecheck clean. pnpm lint 0 errors (53 pre-existing migrations/ warnings, unchanged). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implemented — option AFollowing the brainstorm above, this is now a working change rather than a doc. The analysis is kept in
The part that wasn't obviousReading the column was necessary but not sufficient. The article route sanitises media through a field whitelist before handing it to the client component — That's why the first end-to-end test still rendered Verified end to endMoved the focal point in the database against a real photofeature and watched the crop follow:
No migration
One thing deliberately left outNo top-biased default for images with no focal point. Payload writes Note Every image in the database currently sits at Changing that default is a one-line change in Checks
|
Brainstorm, not an implementation — opened to settle the approach before anyone writes code. No code changes, one new doc:
docs/photofeature-focal-point.md.The problem
components/Article/Photofeature/ArticleHeader.tsx:43puts the hero in a fixedh-screenbox and fills it withobject-coverand noobject-position, so CSS crops evenly from both edges at50% 50%.A 3:2 photo in a 16:9 viewport loses ~25% of its height, split top and bottom. News subjects are almost never vertically centred — they sit in the upper third — so the top crop takes faces first. On a phone in portrait, a landscape hero can lose more than half its height.
The finding worth reading
media.focalXandmedia.focalYalready exist and nothing reads them.migrations/20260211_224237_initial_baseline.ts:65scripts/run_deploy_sql_migrations.sh:329payload-types.ts:248focalPoint: focalPointEnabled = true), andMedianever disables itSo editors can already drag a focal point, the values already persist in both environments, and the frontend has never consumed them. The cheapest fix isn't "add a field" — it's "read the field we already have."
Options weighed
media.focalX/Y→object-positionarticlessharpattention/entropy50% 35%)Recommendation
A, with D as the fallback, and B only if it proves necessary.
Reading the existing focal point costs nothing and fixes every surface that crops, not just the hero. Where none is set, fall back to a top bias rather than dead centre. The article-level override is easy to add later —
gradientOpacity(collections/Articles.ts:372) shows the exact pattern, and20260331_100000_add_photofeature.tsshows the migration.I'd skip C. Automatic cropping that can't be corrected is how you end up with a headline photo centred on a lamppost.
Open questions in the doc
35%the right fallback, or should we pick it from heroes that currently crop badly?🤖 Generated with Claude Code