release: v0.11.0 (#459)

Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.

## Headline changes since v0.10.0

- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)

Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).

## Verified

`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.

## After merge

Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ralksta
2026-08-15 09:09:18 +02:00
committed by GitHub
parent 6eda34cc50
commit 2f546f440d
61 changed files with 3953 additions and 1115 deletions

212
README.md
View File

@@ -1,59 +1,30 @@
# Immich Folio
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.
**Turn your Immich albums into a public photography portfolio — 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.
<p align="center">
<img src="docs/screenshots/theme-studio-modern-home.png" width="98%" alt="Homepage — Studio Modern theme" />
</p>
<p align="center">
<img src="docs/screenshots/theme-studio-modern-grid.png" width="49%" alt="Album photo grid" />
<img src="docs/screenshots/page-collection.png" width="49%" alt="Collection overview grouping several albums" />
</p>
<p align="center">
<img src="docs/screenshots/lightbox-exif.png" width="98%" alt="Fullscreen lightbox with the EXIF panel open" />
<p>
<a href="LICENSE"><img src="https://img.shields.io/github/license/ralksta/immich-folio" alt="MIT License" /></a>
<a href="https://github.com/ralksta/immich-folio/releases"><img src="https://img.shields.io/github/v/release/ralksta/immich-folio" alt="Latest release" /></a>
</p>
## What's New in v0.10.0
The largest release so far — two new ways to present work, a seventh theme preset, and an admin panel rebuilt around visual controls.
- **New theme preset: Studio Modern** — the Leica language of `studio` rebuilt around the Archivo grotesque, with IBM Plex Mono for all photographic metadata, an indexed hero navigation with album counts, caption bars under album covers, and a film-edge EXIF strip in the lightbox → [Theming guide](docs/theming.md)
<p align="center">
<img src="docs/screenshots/theme-studio-modern-home-light.png" width="49%" alt="Studio Modern in light mode" />
<img src="docs/screenshots/theme-studio-modern-grid-light.png" width="49%" alt="Studio Modern album grid in light mode" />
</p>
<p align="center"><em>Studio Modern in light mode — every preset ships a light and a dark variant.</em></p>
- **Photo Essay mode** — storytelling pages that alternate text with fullbleed and paired image layouts, written in Markdown or assembled in the new visual block editor
- **Client proofing** — let clients pick favorites and export the selection; the picks live in the URL as a compact bitmask, so nothing is stored server-side and a selection can be shared as a link
- **Lightbox watermark** — configurable overlay on fullscreen images
- **Privacy-friendly analytics** — page and album view counts without cookies or third parties, switchable off entirely
- **Subpage on/off toggle** — take a page offline without deleting it
- **Admin panel overhaul** — slide-over page-builder drawer with search, visual preview cards for grid, theme, photo frame and hero style, a Google SEO snippet preview, light/dark toggle, subpage live preview, and a backup manager with one-click restore
<p align="center">
<img src="docs/screenshots/admin-page-builder.png" width="49%" alt="Visual page builder" />
<img src="docs/screenshots/admin-settings.png" width="49%" alt="Settings editor with visual preview cards" />
<img src="docs/screenshots/header.png" width="100%" alt="Immich Folio — the public gallery, an album grid, and the admin page builder" />
</p>
- **SEO** — configurable subpage title template and metadata descriptions, plus proper metadata on the about page
- **Resilience** — the gallery keeps serving the last known albums during an Immich outage instead of erroring, with loading skeletons while the round-trip is in flight
- **Fixed:** the map page ignored the configured theme fonts and borders (all presets), album order now mirrors the Immich timeline across time zones, the admin status panel no longer reports faults that are not faults, and the Docker health check no longer fails on IPv6-first `localhost` resolution
A self-hosted portfolio powered by [Immich](https://immich.app). It 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, while your albums are published as a gallery you control.
**Upgrading:** no migration, no configuration change — pull the new image and restart. One exception: if you copied `docker-compose.override.yml.example` in the past, its health check needs a one-line edit. See the [upgrade notes](CHANGELOG.md#upgrade-notes) for that and for the rollback caveat when switching presets.
## Contents
### Security releases
- [Features](#features)
- [Requirements](#requirements)
- [Quick Start](#quick-start)
- [First-Run Setup](#first-run-setup)
- [Configuration](#configuration)
- [Docker](#docker)
- [Admin Panel](#admin-panel)
- [What's New](#whats-new)
Security fixes ship in normal releases, so **running the latest release is the recommended baseline**:
- **v0.10.0** — `GET /api/admin/analytics` now requires an admin session; Docker images are scanned with Trivy on every publish
- **v0.9.2** — closed a timing side channel that leaked the admin password length ([#395](https://github.com/ralksta/immich-folio/pull/395))
- **v0.9.0** — fixed pre-auth bypasses and restored Immich 3.x compatibility ([#378](https://github.com/ralksta/immich-folio/pull/378))
Full history in the [GitHub releases](https://github.com/ralksta/immich-folio/releases) and [CHANGELOG.md](CHANGELOG.md).
**Guides:** [Gallery Configuration](docs/gallery-config.md) · [Theming](docs/theming.md) · [Journal & Photo Essays](docs/journal.md) · [Admin Panel](docs/admin-panel.md) · [Deployment](docs/deployment.md)
## Features
@@ -64,23 +35,45 @@ Full history in the [GitHub releases](https://github.com/ralksta/immich-folio/re
- **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
- **Showcase / filmstrip / editorial-flow layouts** — featured hero + grid, horizontal scroll strips, or alternating full-width and paired images
- **Per-subpage grid overrides** — each subpage can define its own columns, gap, aspect ratio, and layout mode
- **Justified rows** _(experimental)_ — every row fills the width at one shared height, aspect ratios intact, nothing cropped
- **Per-subpage grid overrides** — each subpage can define its own columns, gap, aspect ratio, and layout mode, and individual albums can override that again _(experimental)_
- **Cover focal points** _(experimental)_ — decide which part of a cover survives the crop
- **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
<p align="center">
<img src="docs/screenshots/grid-masonry.png" width="49%" alt="Masonry grid layout" />
<img src="docs/screenshots/grid-showcase.png" width="49%" alt="Showcase grid layout" />
</p>
<p align="center"><em>Masonry and showcase, both in Studio Modern — the same album, a different grid layout.</em></p>
<p align="center">
<img src="docs/screenshots/lightbox-exif.png" width="98%" alt="Fullscreen lightbox with the EXIF panel open" />
</p>
<p align="center"><em>The fullscreen lightbox with the EXIF panel — camera, lens and exposure for the frame on screen.</em></p>
### 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
- **Markdown about page** — `content/about.md` with frontmatter for portrait, name, location, and gear list, editable from the admin panel
- **Journal** — photo essays and travel stories at `/journal`, with drafts, per-entry passwords, cover images and reading times
- **Photo Essay mode** — long-form storytelling pages alternating text with fullbleed and paired image layouts
- **Unlisted subpages** _(experimental)_ — reachable by direct link, absent from the navigation
- **Subpage on/off toggle** — take a page offline without deleting it
- **External navigation links** _(experimental)_ — point the header at a shop, a blog, or a social profile
- **Client proofing** — clients favorite photos and export the selection; picks are encoded in the URL, nothing is stored server-side
- **Lightbox watermark** — configurable overlay on fullscreen images
- **Privacy-friendly analytics** — cookieless view counts, no third parties, can be switched off
- **Dynamic OG images** — auto-generated social preview images per album
<p align="center">
<img src="docs/screenshots/page-collection.png" width="98%" alt="Collection overview grouping several albums under one subpage" />
</p>
<p align="center"><em>A subpage grouping several albums into one collection.</em></p>
<p align="center">
<img src="docs/screenshots/page-about.png" width="49%" alt="About page rendered from content/about.md" />
<img src="docs/screenshots/page-map.png" width="49%" alt="Map page clustering photo locations worldwide" />
@@ -91,9 +84,12 @@ Full history in the [GitHub releases](https://github.com/ralksta/immich-folio/re
- **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
- **Settings editor** — configure theme, grid layout, footer, legal/impressum, SEO, image protection and the about page from a visual UI
- **Visual previews everywhere** — grid layout, theme, photo frame, hero style and the Google search snippet are picked from preview cards instead of text fields
- **Journal Studio** — split-screen block editor with a live preview of the real page
- **Essay block editor** — assemble photo essays block by block without touching Markdown
- **Photo order editor** — drag & drop a hand-picked opening sequence for any album
- **Favicon upload** — give the site its own icon in the browser tab
- **Backup manager** — every save is backed up automatically; restore any of them with one click
- **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
@@ -187,25 +183,12 @@ The token is stored in `content/.setup-token` (mode `0600`) so it survives a
restart, and is deleted once setup completes. From then on `/install` redirects
to the gallery and its API routes refuse to run again.
### What the wizard writes
The wizard writes `gallery.yaml`, `settings.yaml` and `content/install.json` —
the last of which **holds your Immich API key**. Environment variables override
everything in it, so credentials can be rotated without touching the file, and
setting them all up front means the wizard never appears at all.
| File | Contents |
| ----------------------- | -------------------------------------------------------------------- |
| `content/gallery.yaml` | The albums you picked |
| `content/settings.yaml` | Site title, subtitle, theme — only if it does not exist yet |
| `content/install.json` | Immich URL and API key, a generated site secret, admin password hash |
> **`content/install.json` holds credentials.** It is written with mode `0600`,
> and it is the reason a backup of your `content/` directory is now a backup of
> your Immich API key as well — treat it accordingly. The admin password is
> stored as an scrypt hash, not as you typed it; the API key and site secret have
> to stay readable to be usable.
**Environment variables always win.** `IMMICH_API_URL`, `IMMICH_API_KEY`,
`AUTH_SECRET` and `ADMIN_PASSWORD` override anything in `install.json`, so you
can rotate any of them by setting the variable — no need to touch the file. Set
all of them up front and the wizard never appears, which is the usual choice for
an infrastructure-as-code deployment.
→ **[What the wizard writes, and environment precedence](docs/deployment.md#what-the-setup-wizard-writes)**
## Configuration
@@ -223,12 +206,15 @@ CACHE_TTL=300 # seconds, default: 300
STALE_MAX_AGE=86400 # seconds an expired cache entry survives during an outage, default: 86400 (24h), 0 disables
IMMICH_TIMEOUT_MS=15000 # Immich response wait, default: 15000
IMAGE_CACHE_VERSION=1 # bump to bust browser image caches, default: off
RATE_LIMIT_RPM=1500 # requests/min/IP, default: 1500
RATE_LIMIT_RPM=1500 # requests/min/IP for images, default: 1500
AUTH_SECRET=long-random-string # required in production
TRUSTED_PROXY_HOPS=1 # reverse proxies in front, default: 0
ADMIN_PASSWORD=your-secure-password # enables /admin panel
WEBHOOK_SECRET=long-random-string # enables POST /api/webhook cache invalidation
```
> Login and setup endpoints have their own, much lower limits that `RATE_LIMIT_RPM` does not raise — see [Rate Limiting](docs/gallery-config.md#rate-limiting).
> 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).
### Immich API Key Permissions
@@ -251,12 +237,26 @@ All gallery structure — hero images, albums, subpages, grid layout, footer —
→ **[Gallery Configuration Guide](docs/gallery-config.md)**
### Journal & Photo Essays
Long-form storytelling with fullbleed photos, side-by-side pairs, quotes and captions — as a standalone `/journal` section, or as a single essay on one subpage.
→ **[Journal & Photo Essays Guide](docs/journal.md)**
### Theming
Seven 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.
**Studio Modern** is the default: Leica precision rebuilt around the Archivo grotesque, with IBM Plex Mono for every piece of photographic metadata, hairline rules, zero radius, and red as signal only.
<p align="center">
<img src="docs/screenshots/theme-studio-modern-home.png" width="49%" alt="Studio Modern in dark mode" />
<img src="docs/screenshots/theme-studio-modern-home-light.png" width="49%" alt="Studio Modern in light mode" />
</p>
<p align="center"><em>Studio Modern, dark and light — every preset ships both, and visitors switch with a toggle in the navigation bar.</em></p>
```yaml
theme: studio # or: studio-modern, minimal, editorial, classic, noir, monograph
theme: studio-modern # or: studio, minimal, editorial, classic, noir, monograph
```
→ **[View all Themes & Configuration Guide](docs/theming.md)**
@@ -287,59 +287,14 @@ docker compose up -d
The gallery will be available at `http://localhost:7211`.
<details>
<summary><strong>Advanced Docker (Standalone, Health Check, Proxy)</strong></summary>
> The `content/` volume must be **read-write** — the setup wizard, the admin
> panel, the journal and the backup rotation all write into it. A `:ro` mount
> leaves the wizard unable to complete and the admin panel unable to save.
### Standalone Docker
For standalone `docker run`, the health check, TLS behind a reverse proxy, and
what the gallery does while Immich is unreachable:
```bash
# 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 \
immich-folio
```
> **Note:** The `content/` volume mount lets you update `gallery.yaml` and
> `about.md` without rebuilding the image. It must be **read-write**: the setup
> wizard, the admin panel and the backup rotation all write into it. A `:ro`
> mount leaves the wizard unable to complete and the admin panel unable to save.
### Health Check
The container includes a built-in health check at `/api/health`:
```bash
curl http://localhost:7211/api/health
```
### Behaviour when Immich is unreachable
Immich Folio buffers your gallery rather than merely proxying it:
- Album and asset pages keep serving the last known good data for up to `STALE_MAX_AGE`, so a restarting or briefly unreachable Immich does not take the public site down with it.
- Once nothing cached is left, they return `503`, never `404` — a `404` would tell search engines to drop a URL for content that still exists.
- Outages are never cached, so the gallery recovers as soon as Immich does.
The cache lives in the process, so it is empty right after a container restart.
### Reverse Proxy
Put Immich Folio behind nginx / Caddy / Traefik with TLS. Example Caddy config:
```
photos.example.com {
reverse_proxy localhost:7211
}
```
</details>
→ **[Deployment Guide](docs/deployment.md)**
## Admin Panel
@@ -371,6 +326,25 @@ own password, separate from any album passwords, and writes straight to
→ **[Admin Panel Guide](docs/admin-panel.md)**
## What's New
v0.11.0 — a first-run wizard, a journal, and a set of experimental portfolio
features:
- **First-run setup wizard at `/install`** — configure a fresh deployment from the browser instead of writing config files → [First-Run Setup](#first-run-setup)
- **Journal** — photo essays and travel stories at `/journal`, with a split-screen block editor, drafts, per-entry passwords and cover images → [Journal guide](docs/journal.md)
- **About page editor** — portrait, name, location, gear and bio edited in the admin panel, plus a toggle to take the page offline
- **Custom favicon upload** — SVG, PNG, ICO or JPEG, stored in the content volume
- **Experimental portfolio features** — justified grid layout, `cover` hero splash screen, unlisted subpages, per-album grid overrides, cover focal points, external navigation links
- **Admin panel** — every area has its own URL, a floating save bar, and a drag & drop photo-order editor
No migration and no configuration change are required to upgrade.
Security fixes ship in normal releases, so **running the latest release is the
recommended baseline**.
→ Full history in [CHANGELOG.md](CHANGELOG.md) and the [GitHub releases](https://github.com/ralksta/immich-folio/releases)
## Tech Stack
- **Next.js 16** (App Router, standalone output)