2026-02-14 00:59:57 +01:00
# Immich Folio
2026-02-13 23:26:37 +01:00
2026-02-14 01:01:37 +01:00
A self-hosted photography portfolio powered by [Immich ](https://immich.app ). 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.
2026-02-13 23:26:37 +01:00
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
<p align="center">
<img src="docs/screenshots/theme-editorial-home.png" width="49%" alt="Homepage — Editorial theme" />
<img src="docs/screenshots/theme-editorial-grid.png" width="49%" alt="Photo grid — Editorial theme" />
</p>
2026-02-14 00:59:57 +01:00
## Features
2026-02-14 10:34:54 +01:00
### Gallery & Layout
2026-02-14 00:59:57 +01:00
2026-02-15 22:04:50 +01:00
- **Configurable hero layouts** — split, fullbleed, minimal, stacked (image + thumbnail strip), typographic (text-only), or mosaic (multi-image grid)
2026-02-14 10:34:54 +01:00
- **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
2026-02-15 22:04:50 +01:00
- **Showcase / filmstrip / editorial-flow layouts** — featured hero + grid, horizontal scroll strips, or alternating full-width and paired images
2026-02-14 10:34:54 +01:00
- **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
2026-02-14 01:01:37 +01:00
2026-02-14 10:34:54 +01:00
### 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
2026-05-29 17:04:41 +02:00
### Admin Panel
- **Visual page builder** — drag & drop interface to manage hero images, standalone albums, subpages, and sections
- **Album picker** — browse all shared Immich albums with search, see photo counts, and add them with one click
- **Settings editor** — configure theme, grid layout, footer, legal/impressum, and SEO from a visual UI
- **Live YAML sync** — changes are written directly to `gallery.yaml` and `settings.yaml` with automatic backups
- **Password protected** — secured with its own admin password, separate from album passwords
2026-03-08 18:27:01 +01:00
<details>
2026-03-08 18:32:52 +01:00
<summary><strong>Security & Infrastructure</strong></summary>
2026-03-08 18:27:01 +01:00
<br>
2026-03-12 11:30:45 +00:00
| Concern | Protection |
| ----------------------- | --------------------------------------------------------------------- |
| **Server exposure ** | Immich URL never leaves your network — all requests proxy server-side |
| **API key ** | Stored only in `.env.local` , never in client code |
| **Asset IDs ** | Immich UUIDs encrypted (AES-256) into opaque tokens |
| **Album scope ** | Only albums in `gallery.yaml` are accessible |
| **Password protection ** | Per-subpage password support |
| **Rate limiting ** | Per-IP sliding-window rate limiter (configurable RPM) |
2026-02-14 01:01:37 +01:00
2026-03-08 18:32:52 +01:00
- Health check endpoint at `GET /api/health`
- In-memory caching with configurable TTL
- Standalone Docker image — multi-stage, non-root, ~150 MB
2026-03-08 16:27:21 +01:00
2026-03-08 18:27:01 +01:00
</details>
2026-07-20 18:57:57 +02:00
## Requirements
- **Immich 3.0 or newer.** Immich 3.0 changed how album assets are retrieved; earlier versions are not supported as of v0.9.0. On an older server, albums render with the correct title but no photos, and the map stays empty — the app logs a warning naming this as the likely cause.
- Node.js 20+ (or just use the Docker image)
2026-02-14 00:59:57 +01:00
## Quick Start
2026-02-13 23:26:37 +01:00
```bash
2026-02-14 00:59:57 +01:00
# Clone and install
2026-02-14 01:02:12 +01:00
git clone https://github.com/ralksta/immich-folio.git
cd immich-folio
2026-02-14 00:59:57 +01:00
npm install
# Configure
cp .env.local.example .env.local
2026-02-14 10:34:54 +01:00
# 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
2026-02-14 00:59:57 +01:00
# Run
2026-02-13 23:26:37 +01:00
npm run dev
```
2026-02-14 00:59:57 +01:00
## Configuration
2026-02-13 23:26:37 +01:00
2026-02-14 10:34:54 +01:00
### Environment Variables (`.env.local`)
2026-02-13 23:26:37 +01:00
2026-02-14 00:59:57 +01:00
```env
# Required
IMMICH_API_URL=http://your-immich-server:2283
IMMICH_API_KEY=your-api-key
2026-02-13 23:26:37 +01:00
2026-02-14 00:59:57 +01:00
# Optional
2026-02-14 10:34:54 +01:00
SITE_TITLE=My Photography # default: "Gallery"
SITE_SUBTITLE=A visual journal # default: empty
CACHE_TTL=300 # seconds, default: 300
2026-07-20 18:57:57 +02:00
RATE_LIMIT_RPM=1500 # requests/min/IP, default: 1500
AUTH_SECRET=long-random-string # required in production
TRUSTED_PROXY_HOPS=1 # reverse proxies in front, default: 0
2026-05-29 17:04:41 +02:00
ADMIN_PASSWORD=your-secure-password # enables /admin panel
2026-02-14 00:59:57 +01:00
```
2026-02-13 23:26:37 +01:00
2026-07-20 18:57:57 +02:00
> Behind a reverse proxy, set `TRUSTED_PROXY_HOPS` to the number of proxies in front of the app (nginx/Traefik/Caddy = 1; Cloudflare in front of nginx = 2). Without it the client IP is read from a header the client itself can set, which defeats the brute-force limits on the password endpoints. See [Trusted Proxies](docs/gallery-config.md#trusted-proxies).
2026-06-29 21:34:12 +03:00
### Immich API Key Permissions
Create a dedicated API key in Immich under **Account Settings → API Keys ** . Immich Folio only needs **read access ** — it never modifies your library.
2026-07-20 18:57:57 +02:00
| Permission | Required | Used for |
| ------------ | -------- | -------------------------------------------------------------------- |
| `album.read` | ✅ Yes | List and fetch album metadata & photo lists |
| `asset.read` | ✅ Yes | Fetch asset metadata, EXIF data, thumbnails, previews, and originals |
| `asset.view` | ✅ Yes | Stream image/video files (thumbnail, preview, video playback) |
2026-06-29 21:34:12 +03:00
> **No write permissions needed.** `album.create`, `asset.upload`, `asset.delete`, etc. can all be left **off**.
> **Tip (Admin Panel):** The Admin Panel also uses `POST /search/metadata` to browse your full library for the hero image picker. This is covered by `asset.read` — no additional permission required.
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
### Gallery Config
2026-02-14 10:34:54 +01:00
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
All gallery structure — hero images, albums, subpages, grid layout, footer — is defined in `content/gallery.yaml` .
2026-02-13 23:26:37 +01:00
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
→ * * [Gallery Configuration Guide ](docs/gallery-config.md )**
2026-02-14 10:34:54 +01:00
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
### Theming
2026-02-14 10:34:54 +01:00
2026-03-08 18:25:10 +01:00
Six built-in presets with distinct visual identities — or mix and match with fine-grained control over colors, fonts, corners, photo frames, hero layout, and grid style.
2026-02-14 10:34:54 +01:00
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
```yaml
2026-03-08 17:53:47 +01:00
theme: studio # or: minimal, editorial, classic, noir, monograph
2026-02-14 10:34:54 +01:00
```
2026-02-14 00:59:57 +01:00
2026-03-08 18:25:10 +01:00
→ * * [View all Themes & Configuration Guide ](docs/theming.md )**
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
2026-02-14 00:59:57 +01:00
## Docker
2026-02-14 10:34:54 +01:00
### Docker Compose (recommended)
```yaml
services:
lightbox:
build: .
container_name: immich-folio
restart: unless-stopped
ports:
2026-02-14 11:47:26 +01:00
- '7211:7211'
2026-02-14 10:34:54 +01:00
env_file:
- .env.local
volumes:
- ./content:/app/content:ro
```
Run with:
2026-02-14 00:59:57 +01:00
```bash
2026-02-14 10:34:54 +01:00
docker compose up -d
```
The gallery will be available at `http://localhost:7211` .
2026-03-08 18:27:01 +01:00
<details>
<summary><strong>Advanced Docker (Standalone, Health Check, Proxy)</strong></summary>
2026-02-14 10:34:54 +01:00
### Standalone Docker
```bash
# Build
2026-02-14 01:02:12 +01:00
docker build -t immich-folio .
2026-02-14 10:34:54 +01:00
# Run
docker run -d \
--name immich-folio \
--restart unless-stopped \
-p 7211:7211 \
--env-file .env.local \
-v ./content:/app/content:ro \
immich-folio
2026-02-14 00:59:57 +01:00
```
2026-02-13 23:26:37 +01:00
2026-02-14 10:34:54 +01:00
> **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` :
```bash
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
}
```
2026-03-08 18:27:01 +01:00
</details>
2026-05-29 17:04:41 +02:00
## Admin Panel
A built-in visual editor at `/admin` lets you manage your gallery without editing YAML files manually.
**Enable it** by setting `ADMIN_PASSWORD` in your environment:
```env
ADMIN_PASSWORD=your-secure-admin-password
```
Then navigate to `http://your-site/admin` and log in.
→ * * [Admin Panel Guide ](docs/admin-panel.md )**
2026-02-14 10:34:54 +01:00
## Tech Stack
- **Next.js 16** (App Router, standalone output)
- **React 19**
- **TypeScript**
- **Vanilla CSS** (no framework)
2026-02-14 00:59:57 +01:00
## License
2026-02-13 23:26:37 +01:00
2026-03-08 17:48:38 +01:00
MIT License — free to use and modify for the Immich community.