- Add Content-Security-Policy + security headers to next.config.ts
- Fix hero image cutoff on mobile iOS (40vh → 50svh)
- Mark all plan checklists as complete
- Clean up IDEAS.md: remove implemented items, add ✅ table
- Add photo stories implementation plan draft
9.3 KiB
Photography Features
Status: ✅ Complete — All items implemented and shipped.
Objective
Implement four photography-focused enhancements to the gallery: a hero image carousel with crossfade transitions, automatic full-bleed rendering when a subpage contains only one album, EXIF metadata on grid hover with a global toggle, and a configurable grid layout system supporting masonry vs. uniform modes, custom column counts, gaps, and aspect ratios.
Implementation Plan
Feature 1 — Hero Carousel
-
1. Add
herolist support togallery.yamland config — Extend theGalleryYamlinterface inlib/config.ts:32so thatheroaccepts either a single UUID string (backward compatible) or an array of UUID strings. Add a validation step that normalizes the value to an array (heroImages: string[]) inAppConfigatlib/config.ts:100. Updategallery.yaml.examplewith a commented example showing the array syntax. This lets users define 3–5 hero images. -
2. Create
HeroCarouselclient component — Create a newcomponents/HeroCarousel.tsxclient component. It receives a list of{ src, blurDataURL }objects. It rotates through images on a timer (e.g. 6 seconds), crossfading by layering twonext/imageelements with CSS opacity transitions. Only two are mounted at any time (current + next). Usepriorityon the first image only. Clear/restart the interval on unmount. If only one image is provided, render a single static image with no timer — this maintains backward compatibility with the current single-hero setup. -
3. Update homepage to use
HeroCarousel— Modifyapp/page.tsx:62-79to replace the single<Image>with theHeroCarouselcomponent. FetchgetAssetInfofor each hero image inconfig.heroImages, generate placeholder data viaassetPlaceholder, and pass the array to the carousel. If no hero images are configured, fall back to the existing placeholder div. -
4. Add carousel CSS — Add CSS rules in
app/globals.cssfor the carousel: stack images withposition: absolute, crossfade usingopacitytransitions withvar(--transition-slow), handlez-indexlayering. Ensure the.hero__rightcontainer already hasposition: relative; overflow: hidden(it does) so images fill correctly.
Feature 2 — Full-Bleed Single Albums
-
5. Detect single-album subpages in the catch-all route — In
app/[...path]/page.tsx:150-202(the subpage rendering branch), after fetchinggetSubpageAlbums, check ifalbums.length === 1. If so, skip the subpage album grid entirely and instead render the album detail view (album header +PhotoGrid) directly, consistent with how a standalone album renders. Include the back-link pointing to/since the intermediate subpage grid is being bypassed. -
6. Preserve metadata for single-album subpages — In
generateMetadataatapp/[...path]/page.tsx:46-48, when the slug is a subpage, also check if only one album exists. If so, generate metadata using the album name and photo count instead of the subpage name, since the user lands directly in the album.
Feature 3 — EXIF on Hover
-
7. Extend
PhotoItemwith EXIF summary fields — Add optionalcamera(string),lens(string), andfocalLength(string) fields to thePhotoIteminterface inapp/[...path]/PhotoGrid.tsx:12-20. These are compact summary strings derived server-side fromImmichExifInfo.model,ImmichExifInfo.lensModel, andImmichExifInfo.focalLengthon each asset. -
8. Pass EXIF summary data from server pages — In
app/[...path]/page.tsx, when building thePhotoItemarray forPhotoGrid, extract the EXIF summary fromasset.exifInfo(already present onImmichAsset). Construct the compact strings (e.g."Leica M11","50mm Summilux","50mm") and include them in eachPhotoItem. Only include fields that are non-null. -
9. Add EXIF hover tooltip to
PhotoGrid— InPhotoGrid.tsx, render a small overlay div inside each.photo-grid__itemwhencameraorlensis present. The overlay shows camera + lens + focal length in a compact one-line format. Use CSSopacity: 0by default andopacity: 1on.photo-grid__item:hover, matching the existing hover transition timing. Position at the bottom of the grid cell. -
10. Add an
exifOnHovertoggle togallery.yaml— Add an optional booleanexifOnHoverfield (defaulttrue) to theGalleryYamlinterface andAppConfiginlib/config.ts. Whenfalse, the server pages skip attaching the EXIF data toPhotoItem, and the overlay elements are not rendered. Pass the flag through toPhotoGridas a prop. -
11. Add CSS for EXIF hover overlay — Add styles in
app/globals.cssfor.photo-grid__item-exif: positioned at the bottom of the grid cell usingposition: absolute; bottom: 0, with a dark gradient background, small white text (font-size ~0.7rem),opacity: 0transitioning to1on parent hover, withpointer-events: noneso it doesn't interfere with click-to-lightbox.
Feature 4 — Customizable Grid Layout
-
12. Add grid configuration to
gallery.yaml— ExtendGalleryYamlandAppConfiginlib/config.tswith agridobject containing:columns(number, default 3),gap(number in px, default 12),aspectRatio(string like"1","3/2","auto", default"1"), andlayout(enum"masonry"|"uniform", default"masonry"). Parse and validate these ingetConfig(). Updategallery.yaml.examplewith commented examples. -
13. Pass grid config as CSS custom properties — In the server page components (
app/[...path]/page.tsx), wrapPhotoGridin a container div that sets CSS custom properties from the grid config:--grid-columns,--grid-gap,--grid-aspect-ratio. This keeps the client component simple — it just reads props and the CSS handles the rest. -
14. Update
PhotoGridto accept grid layout prop — Add alayoutprop ("masonry" | "uniform") toPhotoGridProps. When"masonry", use the existing CSScolumn-countapproach. When"uniform", switch to CSSdisplay: gridwithgrid-template-columns: repeat(var(--grid-columns), 1fr). Apply different classnames:.photo-grid--masonryvs.photo-grid--uniform. -
15. Update CSS grid styles — Replace the hardcoded
column-count: 3,column-gap: 12px, andaspect-ratio: 1values inapp/globals.css:390-414with CSS custom properties:column-count: var(--grid-columns),column-gap: var(--grid-gap),aspect-ratio: var(--grid-aspect-ratio). Add.photo-grid--uniformstyles that use CSS Grid instead of columns. Update responsive breakpoints to adjust--grid-columnsaccordingly. WhenaspectRatiois"auto", remove the fixed aspect ratio from masonry items and restoreheight: auto.
Verification Criteria
npx tsc --noEmitpasses with zero errorsnpm run lintpasses with zero warnings- Single hero image config still works identically (backward compatible)
- Array hero config shows crossfade carousel with smooth transitions
- Subpages with 1 album go straight to PhotoGrid (no intermediate album card grid)
- Subpages with 2+ albums still show the album card grid
- EXIF overlay appears on grid hover when
exifOnHover: trueor when omitted (default) - EXIF overlay does not appear when
exifOnHover: false - Grid respects
columns,gap,aspectRatio, andlayoutfromgallery.yaml - Responsive breakpoints still reduce columns on small viewports
Potential Risks and Mitigations
-
Hero carousel timer causes memory leaks on fast navigation Mitigation: Clear interval on component unmount via
useEffectcleanup. Only mount the timer when the component is visible. -
EXIF data bloats server-rendered HTML for large albums Mitigation: Only pass 3 short strings per asset (camera, lens, focal length), not the full EXIF object. This adds ~50-100 bytes per image to the serialized props — negligible even for 200+ photo albums.
-
Grid layout changes break existing CSS overrides or custom styles Mitigation: Use CSS custom properties with sensible defaults so existing deployments work without any
gallery.yamlchanges. The defaults match the current hardcoded values. -
Full-bleed single albums lose the subpage name context Mitigation: Include the subpage name in the back-link text and update metadata to use the album name directly.
Alternative Approaches
-
Hero carousel via CSS-only (no client component): Could use CSS animations with
animation-delayon stacked images, avoiding React state. Trade-off: harder to control timing, no pause-on-hover, and can't preload images intelligently. Rejected in favor of the React component approach for better control. -
EXIF on hover via client-side fetch (lazy): Instead of passing EXIF data server-side, could fetch on first hover per image. Trade-off: adds latency on hover and requires per-image API calls. Since
exifInfois already onImmichAsset, passing it server-side is both simpler and faster. -
Grid config via a separate
lightbox.config.tsfile: Could create a dedicated TypeScript config file instead of extendinggallery.yaml. Trade-off: adds another config surface. Sincegallery.yamlalready serves as the central configuration point and the grid settings are gallery-specific, extending it keeps things consolidated.