Files
immich-folio/plans/2026-02-13-immich-lightbox-v1.md
ralksta 3d71e9ffb7 chore: add CSP headers, fix mobile hero, clean up plans
- 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
2026-02-15 11:07:39 +01:00

118 lines
10 KiB
Markdown

# Immich Lightbox — Public Photo Gallery
> **Status: ✅ Complete** — All items implemented and shipped.
## Objective
Build a public-facing web gallery that connects to a private Immich server via its API, selects specific albums, and presents them as a beautiful, Lightroom-style portfolio website — similar in spirit to [ralfo.myportfolio.com](http://ralfo.myportfolio.com/). The Immich server stays behind the firewall; only curated album images flow outward through this app.
### Key Goals
- **Album curation** — Admin picks which Immich albums to publish; only those appear on the public site
- **Proxy architecture** — The gallery server talks to Immich; visitors never touch Immich directly. API key stays server-side
- **Portfolio-grade design** — Clean, dark, image-first aesthetic with masonry/grid layout, smooth lightbox, and responsive behavior
- **Low operational overhead** — Single Docker container, minimal config, no database needed
---
## Architecture Overview
**Visitor (Browser)** → HTTPS → **Lightbox Server (Next.js)** → HTTP → **Immich Server (Private API)**
The Lightbox Server sits between visitors and Immich. It proxies images, caches API responses, and holds the album selection config. The Immich server stays private and serves album data and original images only to the Lightbox Server.
**Stack:**
- **Next.js 15** (App Router) — SSR for fast first paint, API routes for proxying
- **TypeScript** — End-to-end type safety
- **CSS Modules + CSS custom properties** — Clean, maintainable dark-theme styling
- **Docker** — Single-container deployment
---
## Implementation Plan
- [x] 1. **Scaffold the Next.js project** — Initialize a Next.js 15 app with TypeScript in the current workspace directory using `npx -y create-next-app@latest ./`. Configure project settings (app router, TypeScript, ESLint). This is the foundation everything else builds on.
- [x] 2. **Create the Immich API client module** — Build a server-only module (`lib/immich.ts`) that wraps the Immich REST API. This module handles authentication (API key from env vars), album listing (`GET /api/albums`), album detail fetching (`GET /api/albums/{id}`), and asset thumbnail/original URL construction. All API calls happen server-side only — the API key never reaches the browser.
- [x] 3. **Create environment configuration** — Set up `.env.local.example` documenting required variables: `IMMICH_API_URL` (the Immich server base URL), `IMMICH_API_KEY` (service account key), and `LIGHTBOX_ALBUMS` (comma-separated album IDs to publish). Add a `lib/config.ts` module that validates and exports typed config at startup.
- [x] 4. **Build the image proxy API route** — Create `app/api/image/[id]/route.ts` that proxies image requests through to Immich. Accepts an asset ID and optional `size` query param (thumbnail vs. original). Streams the response from Immich to the client, setting appropriate cache headers (long `Cache-Control` since images are immutable). This keeps the Immich server unexposed while serving images to visitors.
- [x] 5. **Design and implement the global layout and theme** — Create the root layout (`app/layout.tsx`) and global styles (`app/globals.css`). Establish a dark, minimal design system: near-black backgrounds, subtle borders, elegant typography (Google Fonts — Inter or similar). Include a minimal header/nav with the site title and responsive breakpoints. The aesthetic should feel premium and image-focused, not app-like.
- [x] 6. **Build the homepage — album grid** — Create `app/page.tsx` as a server component that fetches the configured albums from Immich, displays them in a responsive grid layout. Each album card shows its cover image (fetched via the proxy), album title, and asset count. Clicking an album navigates to its detail page. Add hover effects (subtle scale, overlay with title) for an engaging, interactive feel.
- [x] 7. **Build the album detail page — photo grid** — Create `app/album/[id]/page.tsx` as a server component. Fetches all assets in the selected album from Immich, displays them in a masonry or justified grid using CSS Grid. Shows thumbnails initially for fast loading. A back-to-home navigation link or breadcrumb provides context. Each image is clickable to open a fullscreen lightbox.
- [x] 8. **Build the client-side lightbox component** — Create a `components/Lightbox.tsx` client component. When a user clicks a photo in the album grid, an overlay opens showing the full-resolution image with smooth fade/slide transitions. Include previous/next navigation (arrow keys + swipe on mobile), close button (Esc key support), and an optional EXIF info overlay (camera, lens, ISO, aperture, shutter speed — data from Immich). Preload adjacent images for snappy navigation.
- [x] 9. **Add EXIF metadata display** — Enhance the Immich API client to fetch asset EXIF data (`GET /api/assets/{id}`). Display this in the lightbox as an optional info panel: camera model, lens, focal length, aperture, shutter speed, ISO. This adds the "photographer's portfolio" feel that distinguishes this from a basic gallery.
- [x] 10. **Implement image loading optimizations** — Use Next.js `<Image>` component with blur placeholder data-URLs (generated server-side from Immich thumbnails). Implement progressive loading: show tiny blurred thumbnails instantly, then swap to full thumbnails on load. Apply `loading="lazy"` on below-the-fold images. Set proper `sizes` attributes for responsive image delivery.
- [x] 11. **Add server-side caching layer** — Implement an in-memory LRU cache (or file-based cache in production) for Immich API responses. Album lists and metadata rarely change, so cache with a configurable TTL (default 5 minutes). Image proxy responses rely on HTTP cache headers; no server-side image caching needed. This dramatically reduces Immich API load.
- [x] 12. **Create the Docker deployment setup** — Write a `Dockerfile` (multi-stage: build + runtime) and `docker-compose.yml`. The compose file references env vars for Immich connection details. Include health check and proper signal handling. Document deployment in `README.md`.
- [x] 13. **Write the README and configuration guide** — Create a comprehensive `README.md` covering: project overview, screenshots, setup instructions (how to get an Immich API key, how to find album IDs, env var reference), Docker deployment, and development workflow.
---
## Verification Criteria
- **Album selection**: Only albums specified in `LIGHTBOX_ALBUMS` appear on the homepage; other Immich albums are hidden
- **Image proxying**: Browser Network tab shows all image requests going to the Lightbox server, never directly to Immich. The `x-api-key` header never appears in browser-visible requests
- **Responsive layout**: Gallery looks correct on mobile (375px), tablet (768px), and desktop (1440px)
- **Lightbox navigation**: Arrow keys, swipe gestures, and on-screen buttons all work for prev/next. Esc closes the lightbox
- **Performance**: Homepage loads in under 2 seconds with cached data. Lighthouse performance score ≥ 85
- **Docker deployment**: `docker compose up` brings the app up and serves the gallery
### Automated Tests
- Run `npm run build` to validate TypeScript compilation and Next.js build succeeds
- Run `npm run lint` to check for code quality issues
### Manual Verification
> [!IMPORTANT]
> You will need a running Immich instance with at least 2 albums to test against. Please confirm your Immich server URL and provide an API key before we begin implementation.
1. **Homepage check** — Open `http://localhost:3000` in a browser, confirm only the configured albums appear as cards with cover images
2. **Album detail check** — Click an album card, confirm all photos from that album load in a grid
3. **Lightbox check** — Click a photo, confirm the lightbox opens with full-res image, arrow navigation works, Esc closes
4. **Security check** — Open browser DevTools → Network tab, confirm no requests go directly to the Immich server URL, and the API key does not appear in any request headers or URLs
5. **Mobile check** — Use browser responsive mode at 375px width, confirm the layout adapts cleanly
---
## Potential Risks and Mitigations
1. **Immich API changes between versions**
Mitigation: Pin to a specific Immich API version in documentation. Use defensive coding with optional chaining and fallbacks for missing fields. The OpenAPI spec is stable for core album/asset endpoints.
2. **Large albums (1000+ images) causing slow page loads**
Mitigation: Implement pagination or virtual scrolling in the album detail view. Initially load 50 thumbnails and lazy-load more on scroll.
3. **Immich server unreachable causes gallery downtime**
Mitigation: The server-side cache ensures the gallery continues serving recently-fetched data even if Immich is temporarily down. Add graceful error states showing "Gallery temporarily unavailable."
4. **Image proxy becomes a bandwidth bottleneck**
Mitigation: Set aggressive `Cache-Control` headers (1 year for immutable images) so browsers and CDNs cache aggressively. Consider optional CDN (Cloudflare) in front.
5. **API key exposure**
Mitigation: API key is only used in server-side code (API routes, server components). It never appears in client bundles or browser requests. Environment variables are validated at startup.
---
## Alternative Approaches
1. **Static Site Generation (SSG) instead of SSR** — Pre-render all pages at build time, deploy to a static host (Netlify, Vercel). Eliminates the need for a running server. Trade-off: requires a rebuild whenever album contents change; no real-time sync with Immich.
2. **Use Immich Shared Links instead of API** — Create shared links for each album in Immich and embed/proxy those. Trade-off: less control over presentation, relies on Immich's built-in sharing UI, and shared link URLs may expire.
3. **Client-side only (SPA)** — Fetch everything from the browser via CORS. Trade-off: exposes the Immich API key to the browser, which is a security risk. Not recommended.
4. **Sync + Static Files** — A cron job syncs selected album images to local disk, then a static site serves them. Trade-off: duplicates storage, adds operational complexity, but provides complete decoupling from Immich availability.