Files
immich-folio/README.md
ralksta a9737ad8e7 feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes

Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs

Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden

Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
  - Grid items render with real dimensions
  - overflow:hidden CSS guard (catches the exact bug fixed)
  - Images load with real pixel data (not blurhash)

Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00

5.6 KiB

Immich Folio

A self-hosted photography portfolio powered by Immich. Turns your Immich albums into a beautiful, public-facing gallery — without ever exposing your Immich server to the internet.

Immich Folio acts as a secure reverse proxy between your visitors and your private Immich instance. Your Immich server stays on your local network, completely invisible to the outside world.

Features

  • Split hero homepage — full-height hero with site title, subtitle, and navigation links on the left; hero image on the right
  • Hero image carousel — single image or crossfade carousel of multiple Immich assets
  • Masonry photo grid — responsive layout with natural aspect ratios and configurable columns, gap, and aspect ratio
  • Uniform grid mode — switch to a fixed-aspect uniform grid per-subpage or globally
  • Per-subpage grid overrides — each subpage can define its own columns, gap, aspect ratio, and layout mode
  • Fullscreen lightbox — keyboard and swipe navigation, EXIF panel, adjacent image preloading
  • EXIF metadata on hover — camera body, lens, focal length, aperture, shutter speed, ISO shown directly on the grid
  • ThumbHash placeholders — instant blurred previews while full images load

Content & Organization

  • Subpage grouping — organize albums into named collections (e.g. /japan/tokyo-2023)
  • Auto-generated slugs — URL slugs derived from album names automatically
  • YAML gallery config — all gallery structure defined in a single content/gallery.yaml file
  • Markdown about page — content/about.md with frontmatter for portrait, name, location, and gear list
  • Dynamic OG images — auto-generated social preview images per album

Security

Concern Protection
Server exposure Immich URL and port are never sent to the browser — all requests are proxied server-side
API key Stored only in .env.local, never included in client-side code or responses
Asset IDs Immich UUIDs are encrypted (AES-256) into opaque tokens — visitors cannot guess or enumerate asset IDs
Album scope Only albums explicitly listed in gallery.yaml are accessible — everything else is blocked
Password protection Individual subpages can require a password before content is revealed
Rate limiting Per-IP sliding-window rate limiter on the image proxy (configurable RPM)
No direct access Visitors interact only with the Next.js app; they have zero knowledge of your Immich server's existence

Infrastructure

  • Health check endpoint — GET /api/health for uptime monitoring and container orchestration
  • In-memory caching — configurable TTL for Immich API responses
  • Standalone Docker image — multi-stage build, runs as non-root, ~150 MB

Quick Start

# Clone and install
git clone https://github.com/ralksta/immich-folio.git
cd immich-folio
npm install

# Configure
cp .env.local.example .env.local
# Edit .env.local with your Immich server URL and API key

cp content/gallery.yaml.example content/gallery.yaml
# Edit gallery.yaml with your album UUIDs

# Run
npm run dev

Configuration

Environment Variables (.env.local)

# Required
IMMICH_API_URL=http://your-immich-server:2283
IMMICH_API_KEY=your-api-key

# Optional
SITE_TITLE=My Photography            # default: "Gallery"
SITE_SUBTITLE=A visual journal        # default: empty
CACHE_TTL=300                          # seconds, default: 300
RATE_LIMIT_RPM=120                     # requests/min/IP, default: 120

All gallery structure — hero images, albums, subpages, grid layout, footer — is defined in content/gallery.yaml.

→ Gallery Configuration Guide

Theming

Customize the visual identity of your gallery with built-in presets or fine-grained control over colors, fonts, corners, photo frames, and more.

theme: minimal   # or: studio, editorial, classic

→ Theming Guide

Docker

services:
  lightbox:
    build: .
    container_name: immich-folio
    restart: unless-stopped
    ports:
      - '7211:7211'
    env_file:
      - .env.local
    volumes:
      - ./content:/app/content:ro

Run with:

docker compose up -d

The gallery will be available at http://localhost:7211.

Standalone Docker

# Build
docker build -t immich-folio .

# Run
docker run -d \
  --name immich-folio \
  --restart unless-stopped \
  -p 7211:7211 \
  --env-file .env.local \
  -v ./content:/app/content:ro \
  immich-folio

Note: The content/ volume mount lets you update gallery.yaml and about.md without rebuilding the image.

Health Check

The container includes a built-in health check at /api/health:

curl http://localhost:7211/api/health

Reverse Proxy

Put Immich Folio behind nginx / Caddy / Traefik with TLS. Example Caddy config:

photos.example.com {
    reverse_proxy localhost:7211
}

Tech Stack

  • Next.js 16 (App Router, standalone output)
  • React 19
  • TypeScript
  • Vanilla CSS (no framework)

License

Private — all rights reserved.