Skip to content

feat(gallery): photo credits on galleries and carousels - #155

Merged
RonanHevenor merged 1 commit into
mainfrom
feat/gallery-photo-credits
Sep 9, 2026
Merged

RonanHevenor merged 1 commit into
mainfrom
feat/gallery-photo-credits

Conversation

@RonanHevenor

Copy link
Copy Markdown
Member

What

Photo galleries and carousels carried no credit at all, and the caption field their blocks already declared was never rendered. Both blocks now take a credit relationship, mirroring the one the inline upload node has had since 20260322 — "Overrides the photographer set on the media record."

Credit resolves per photo in the same order the upload node uses: an explicit block credit → the media record's photographer → its writeInPhotographer free text.

credit is hasMany, so a co-shot frame credits everyone at once on one line — "A, B, C/The Polytechnic" — with a single suffix, each staff name linking to its profile. Write-ins render bare, since they aren't staff.

The two blocks diverge

block treatment
carousel caption + credit beneath the frame, muted — styling unchanged
photo gallery credit only, laid over the photo — no captions

Galleries no longer take a caption at all; the field is gone from the block, so it can't be entered.

Important

Captions already stored in published gallery blocks stop rendering, and Payload drops them the next time an editor saves that article. PhotoGallery strips caption rather than merely ignoring it, so text saved before the field was removed can't resurface, and the sampled area matches exactly what's drawn.

Placing a credit on a photograph

Because a gallery credit sits on the image, lib/imageLuminance.ts decides both where it goes and what colour it is from the pixels it will actually cover.

It reconstructs the caption's own box — the same 12px/18px/px-4 pt-2 pb-3 geometry the component renders with, wrapped to an estimated line count — in each bottom corner, and samples only those two rectangles.

  • Corner: lower luminance standard deviation wins. Text over an even area stays legible where the same text over high-contrast detail does not, whatever colour it is. Left takes ties by a 3-point margin, so near-identical photos in one gallery don't flip corners.
  • Colour: the winning corner's mean luminance picks white or black, with a matching text-shadow to hold an edge.

Measured on seed images at 3 columns:

photo left dev right dev wins text
swe-day 10.3 33.1 left white
winter-carnival 34.4 35.6 left (margin) white
bsa-fashion 45.9 26.6 right white
playhouse 48.8 27.3 right black (mean 212)
freakout-fans 53.5 48.0 right white

Sampling the text's own rectangle rather than the whole image changes the answer in practice: playhouse averages 129 across the frame but 212 in the corner the credit lands in, so it's the one that reads black.

Column width is viewport-relative, so sampling assumes a 1280px reference. Narrower viewports wrap to more lines and cover more of the photo, which only ever makes the measured rectangle a subset of the real one. Legacy-archive rows have no local file to sample and fall back to white, bottom-left.

Migration

None, and that was verified rather than assumed.

Lexical block fields serialize into the existing jsonb richText columns — there are no photo_gallery or carousel tables and no new column. Confirmed by applying migrations/ to a fresh database with PAYLOAD_DISABLE_PUSH=1 and NODE_ENV=production, then writing and reading back a multi-credit gallery. pnpm generate:types is likewise a no-op, since lexical blocks don't surface in payload-types.ts.

Scope

Both block definitions are mirrored in payload.config.ts and collections/LiveArticles.ts, so live-article updates get the same treatment — they already render through the same SerializeLexical path.

Checks

pnpm typecheck clean. pnpm lint 0 errors (53 pre-existing migrations/ warnings, unchanged).

🤖 Generated with Claude Code

Photo galleries and carousels carried no credit at all, and the `caption`
field their blocks already declared was never rendered. Both blocks now take
a `credit` relationship, mirroring the one the inline `upload` node has had
since 20260322 ("Overrides the photographer set on the media record").

Credit resolves per photo in the same order the upload node uses: an explicit
block credit, else the media record's `photographer`, else its
`writeInPhotographer` free text. `credit` is hasMany, so a co-shot frame
credits everyone at once on one line — "A, B, C/The Polytechnic" — with a
single suffix and each staff name linking to its profile. Write-ins are
rendered bare, since they are not staff.

The two blocks diverge in how they present it:

  carousel      caption + credit beneath the frame, muted, unchanged styling
  photo gallery credit only, laid over the photo; no captions

Galleries no longer take a caption at all — the field is gone from the block,
so it cannot be entered. PhotoGallery also strips `caption` before rendering
rather than merely ignoring it, so text saved before the field was removed
cannot resurface, and the area sampled below matches exactly what is drawn.
Captions already stored in published gallery blocks stop rendering, and
Payload drops them the next time an editor saves that article.

Because a gallery credit sits on the photograph, lib/imageLuminance.ts picks
both where it goes and what colour it is from the pixels it will actually
cover. It reconstructs the caption's own box — the same 12px/18px/px-4 pt-2
pb-3 geometry the component renders with, wrapped to an estimated line count —
in each bottom corner, then samples only those two rectangles. The corner with
the lower luminance standard deviation wins, because text over an even area
stays legible where the same text over high-contrast detail does not, whatever
colour it is. Left takes ties by a 3-point margin so near-identical photos in
one gallery do not flip corners. The winning corner's mean luminance then
picks white or black, with a matching text-shadow to hold an edge.

Sampling only the text's own rectangle changes the answer in practice: the
playhouse frame averages 129 over the whole image but 212 in the corner the
credit lands in, so it is the one frame that reads black.

Column width is viewport-relative, so sampling assumes a 1280px reference.
Narrower viewports wrap to more lines and cover more of the photo, which only
ever makes the measured rectangle a subset of the real one. Legacy-archive
rows have no local file to sample and fall back to white, bottom-left.

No migration accompanies this. Lexical block fields serialize into the
existing jsonb richText columns — there are no photo_gallery or carousel
tables, and no new column — verified by applying migrations/ to a fresh
database with PAYLOAD_DISABLE_PUSH=1 and NODE_ENV=production, then writing and
reading back a multi-credit gallery. `pnpm generate:types` is likewise a no-op,
since lexical blocks do not surface in payload-types.ts.

Both block definitions are mirrored in payload.config.ts and
collections/LiveArticles.ts, so live-article updates get the same treatment;
they already render through the same SerializeLexical path.

pnpm typecheck clean. pnpm lint 0 errors (53 pre-existing migrations/
warnings, unchanged).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RonanHevenor
RonanHevenor merged commit 42c0ad6 into main Sep 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant