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
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>
2026-08-15 09:09:18 +02:00
Configuration is split across two files in `content/` :
| File | Contains |
| --------------- | ---------------------------------------------------------------------------- |
| `gallery.yaml` | **Structure ** — hero assets, albums, subpages, sections, per-album settings |
| `settings.yaml` | **Behaviour and identity ** — title, theme, grid defaults, footer, legal, SEO |
Copy the examples to get started:
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
```bash
cp content/gallery.yaml.example content/gallery.yaml
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>
2026-08-15 09:09:18 +02:00
cp content/settings.yaml.example content/settings.yaml
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
```
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>
2026-08-15 09:09:18 +02:00
`settings.yaml` is optional — without it, everything falls back to defaults.
2026-05-29 17:04:41 +02:00
> [!TIP]
> Prefer a visual interface? Enable the **[Admin Panel](admin-panel.md)** by setting `ADMIN_PASSWORD` in your environment. It provides a drag-and-drop page builder that writes to these same YAML files automatically.
2026-03-08 18:25:10 +01:00
**Contents:**
2026-03-12 11:30:45 +00:00
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>
2026-08-15 09:09:18 +02:00
`gallery.yaml`
2026-03-08 18:25:10 +01:00
- [Hero Images ](#hero-images )
- [Standalone Albums ](#standalone-albums )
- [Subpages & Categories ](#subpages )
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>
2026-08-15 09:09:18 +02:00
- [Per-Album Options ](#per-album-options )
- [Photo Order Within an Album ](#photo-order-within-an-album )
`settings.yaml`
2026-03-08 18:25:10 +01:00
- [Grid Layout ](#grid-layout )
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>
2026-08-15 09:09:18 +02:00
- [Site Behaviour ](#site-behaviour )
- [Navigation Links ](#navigation-links )
- [Client Proofing ](#client-proofing )
- [Image Protection & Watermark ](#image-protection--watermark )
- [SEO ](#seo )
2026-03-08 18:25:10 +01:00
- [Footer ](#footer )
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>
2026-08-15 09:09:18 +02:00
Other
2026-03-08 18:25:10 +01:00
- [About Page ](#about-page )
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>
2026-08-15 09:09:18 +02:00
- [Journal & Photo Essays ](journal.md )
2026-03-08 18:25:10 +01:00
- [Finding UUIDs ](#finding-uuids )
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>
2026-08-15 09:09:18 +02:00
- [System & Security ](#system--security )
2026-03-08 18:25:10 +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
## Hero Images
Single image or crossfade carousel on the homepage:
```yaml
# Single hero image
hero: 00000000-0000-0000-0000-000000000000
# Carousel (crossfade between multiple images)
hero:
- 00000000-0000-0000-0000-000000000000
- 11111111-1111-1111-1111-111111111111
- 22222222-2222-2222-2222-222222222222
```
## Standalone Albums
2026-03-12 11:30:45 +00:00
Albums shown directly on the homepage as cards.
2026-03-08 23:21:25 +01:00
> [!NOTE]
> The thumbnail image for each album grid card is automatically synced with the explicit **\"Cover Image\"** you select for that album inside the Immich Web UI.
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
albums:
- 11111111-1111-1111-1111-111111111111
- 22222222-2222-2222-2222-222222222222
```
## Subpages
2026-03-08 23:21:25 +01:00
Group multiple albums into named collections. URLs are auto-generated from the name.
The overall **Subpage Cover Image ** shown on the homepage is automatically inherited from the Cover Image of the **first album ** inside its list.
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
subpages:
2026-03-12 11:30:45 +00:00
- name: Japan # → /japan
title: 'Trip to Japan' # Optional: overrides the page heading
subtitle: '2024 adventures' # Optional: adds a subline under the heading
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
albums:
2026-03-12 11:30:45 +00:00
- 33333333-3333-3333-3333-333333333333 # Tokyo
- 44444444-4444-4444-4444-444444444444 # Kyoto
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-03-12 11:30:45 +00:00
- name: Wedding – Smith # → /wedding-smith
2026-04-07 23:04:03 +02:00
password: clientpass123 # Optional: subpage-level protection
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
albums:
- 55555555-5555-5555-5555-555555555555
2026-04-07 23:04:03 +02:00
- 66666666-6666-6666-6666-666666666666:
2026-04-21 22:51:31 +02:00
title: 'Private Highlights'
password: 'album-secret-123' # Optional: album-level protection
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-03-08 22:47:38 +01:00
Alternatively, you can use the object notation (recommended):
```yaml
subpages:
2026-03-12 11:30:45 +00:00
'Japan':
title: 'Trip to Japan'
subtitle: '2024 adventures'
2026-03-08 22:47:38 +01:00
albums:
- 33333333-3333-3333-3333-333333333333
```
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>
2026-08-15 09:09:18 +02:00
### Subpage Options
2026-08-23 23:18:14 +02:00
| Key | Type | Description |
| ----------- | ------- | -------------------------------------------------------------------------------------- |
| `name` | string | Navigation label; the URL slug is derived from it |
| `title` | string | Page heading, defaults to `name` |
| `subtitle` | string | Subline under the heading |
| `password` | string | Gates the whole subpage — see [Password Protection ](#password-protection ) |
| `albums` | list | Album UUIDs, optionally with [per-album options ](#per-album-options ) |
| `sections` | list | Named groups of albums — see [Sections ](#sections ) |
| `grid` | object | Grid override for this page, photos and album covers — see [Grid Layout ](#grid-layout ) |
| `proofing` | boolean | Enables [client proofing ](#client-proofing ) on this page |
| `essayFile` | string | Render a Markdown essay instead of a grid — see [Journal & Photo Essays ](journal.md ) |
| `essayText` | string | Essay Markdown inline; written by the admin block editor |
| `enabled` | boolean | `false` takes the page offline without deleting it |
| `hidden` | boolean | **Experimental. ** Reachable by direct link, but hidden from the navigation |
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>
2026-08-15 09:09:18 +02:00
```yaml
subpages:
- name: Client Preview
hidden: true # unlisted: not in the nav, still reachable at /client-preview
proofing: true # visitors can mark favourites and export the selection
password: 'clientpass123'
albums:
- 55555555-5555-5555-5555-555555555555
- name: Old Series
enabled: false # offline, keeps its configuration
albums:
- 66666666-6666-6666-6666-666666666666
```
> [!NOTE]
> `hidden` is unlisting, not access control. The URL is guessable — a slug is
> derived from the name — and nothing stops a visitor who has it. Combine it
> with `password` for anything that must stay private.
2026-03-08 23:52:23 +01:00
### Sections
When a subpage contains many albums you can split them into **named sections ** . A typographic table of contents with anchor links is automatically rendered above the albums. Sections are fully optional — omit them and you get the standard flat grid.
Each section can have:
2026-03-12 11:30:45 +00:00
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------- |
| `title` | string | ✅ | Section heading + anchor name |
| `description` | string | ➖ | Optional subline under the heading |
| `albums` | list | ✅ | Album UUIDs (same format as regular albums) |
2026-03-08 23:52:23 +01:00
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>
2026-08-15 09:09:18 +02:00
Within the `albums` list you can use a plain UUID, a simple title override, or an object — see [Per-Album Options ](#per-album-options ).
2026-03-08 23:52:23 +01:00
**Full example:**
```yaml
subpages:
- name: Japan
title: Japan
2026-03-15 07:32:42 +01:00
subtitle: 'Travel through a land full of contrasts.'
2026-03-08 23:52:23 +01:00
sections:
- title: Tokyo
2026-03-15 07:32:42 +01:00
description: 'Megacity, neon lights, silence in the noise.'
2026-03-08 23:52:23 +01:00
albums:
2026-03-12 11:30:45 +00:00
- '33333333-3333-3333-3333-333333333333'
2026-03-15 07:32:42 +01:00
- '44444444-4444-4444-4444-444444444444': Shinjuku at night
2026-03-08 23:52:23 +01:00
- title: Kyoto
2026-03-15 07:32:42 +01:00
description: 'Temples and bamboo forests.'
2026-03-08 23:52:23 +01:00
albums:
2026-03-12 11:30:45 +00:00
- '55555555-5555-5555-5555-555555555555': Fushimi Inari
- '66666666-6666-6666-6666-666666666666'
2026-03-08 23:52:23 +01:00
- title: Osaka
2026-03-12 11:30:45 +00:00
albums: # description is optional
- '77777777-7777-7777-7777-777777777777'
2026-03-08 23:52:23 +01:00
```
> [!NOTE]
> Each section title is automatically converted to a URL-safe anchor (`#tokyo`, `#kyoto`, …). The TOC appearance (separator character, numbering style, section rule) is fully controlled by the active theme.
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>
2026-08-15 09:09:18 +02:00
## Per-Album Options
Every album entry — standalone, on a subpage, or inside a section — accepts the
same three notations:
```yaml
albums:
- 'album-uuid' # → uses the Immich album name
- 'album-uuid': My Title # → displays "My Title" instead
- 'album-uuid':
title: My Private Title
description: 'Shot on 35mm film in Vienna, spring 2025.'
password: 'secure-password'
heroImage: 'asset-uuid'
sort: filename
```
| Key | Description |
| --------------- | ----------------------------------------------------------------------------- |
| `title` | Display name instead of the Immich album name |
| `description` | Shown below the album title |
| `password` | Gates this album alone — see [Password Protection ](#password-protection ) |
| `heroImage` | Asset UUID overriding the album cover |
| `sort` | Photo order — see [Photo Order Within an Album ](#photo-order-within-an-album ) |
| `assetOrder` | Pinned asset UUIDs for `sort: manual` |
| `grid` | **Experimental. ** Grid override for this album only |
| `coverPosition` | **Experimental. ** Focal point for the cover crop |
### Per-album grid (experimental)
An album can override the grid it is shown in. It is merged over the subpage
grid, which is merged over the global default — so an album only has to state
what differs:
```yaml
albums:
- 'album-uuid':
title: Panoramas
grid:
layout: justified
columns: 2
```
### Cover focal point (experimental)
Album covers are cropped to the card. `coverPosition` decides which part
survives the crop — the same values CSS `object-position` takes:
```yaml
albums:
- 'album-uuid':
coverPosition: '50% 25%' # or: "top", "center bottom", "left"
```
Useful when the automatic centre crop cuts off a horizon or a face.
## Password Protection
2026-08-23 23:18:14 +02:00
A password can be set on the whole site, on a subpage, on a single album, and on
a [journal entry ](journal.md#password-protected-entries ). Unlocking sets an
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>
2026-08-15 09:09:18 +02:00
`HttpOnly` cookie that is valid for 24 hours; nothing is stored server-side.
Three storage formats are accepted:
| Format | Status |
| ------------------ | --------------------------------------------------------------------- |
| `scrypt:salt:hash` | **Recommended. ** The password is not recoverable from `gallery.yaml` . |
| Plaintext | Works, but deprecated — logs a warning naming the recommended hash. |
| bcrypt (`$2a$…` ) | Rejected. |
To get the hash for an existing plaintext password, unlock the gallery once and
read the server log: the successful unlock prints the matching `scrypt:…` line
to paste back into `gallery.yaml` . The admin panel writes the hashed form
directly.
> [!NOTE]
> Album and subpage gates protect the **page**. Image URLs handed out while a
> gallery was unlocked keep working — see
> [Image Token Revocation](#image-token-revocation).
2026-08-23 23:18:14 +02:00
### Locking the whole site
`sitePassword` in `settings.yaml` puts everything public behind one password —
useful while a portfolio is still being built, or for a folio that is only ever
shown to clients.
```yaml
# settings.yaml
sitePassword: 'scrypt:...'
```
`SITE_PASSWORD` in the environment overrides the file, so the password can be
rotated without editing (or backing up) `settings.yaml` . It accepts the same
three storage formats as every other gate.
Unlike the per-page gates, this one is enforced in `proxy.ts` , before the
requested page renders at all. That matters: a gate that merely swapped the page
for a login form would still have let the page produce its payload, album names
and image tokens included. The public API routes carry the same check
themselves, since route handlers do not pass through the proxy.
Three things stay reachable on a locked site:
| Path | Why |
| ------------- | ------------------------------------------------------------------------ |
| `/api/health` | A health probe runs without cookies; gating it takes the container down. |
| `/admin` | It has its own password, and it is where `sitePassword` is set. |
| `/install` | A fresh deployment has to be configurable before it can be locked. |
A locked site also serves `noindex, nofollow` regardless of the SEO settings —
there is nothing there for a crawler.
2026-08-14 11:10:42 +02:00
## Photo Order Within an Album
By default a Folio album mirrors the Immich timeline. That is usually right, but a curated series has a narrative order that has nothing to do with capture time — and changing the sort on the Immich album would change it for the archive too. The optional `sort` key decouples the two.
| Mode | Order |
| ---------- | --------------------------------------------------------------------------------------- |
| `immich` | Default. Inherits the album's own `asc` /`desc` setting in Immich. |
| `newest` | Capture time, newest first, regardless of the Immich setting. |
| `oldest` | Capture time, oldest first. |
| `filename` | `originalFileName` in natural order — `IMG_2` before `IMG_10` . |
| `manual` | Pinned photos first, in the order you set; everything else follows in the Immich order. |
```yaml
albums:
- 'album-uuid' # no sort key → Immich order, exactly as before
- 'album-uuid':
sort: filename # an entry may carry only a sort, with no title override
- 'album-uuid':
title: Hokkaido, in sequence
sort: manual
assetOrder: # pinned, in this order
- 'asset-uuid-opening-frame'
- 'asset-uuid-second-frame'
```
`sort` works the same on standalone, subpage and section albums.
### About `manual`
`manual` is a pinned **prefix ** , not a full permutation: `assetOrder` lists only the photos you placed by hand, and everything else follows in the album's Immich order. Curating an opening sequence therefore costs a handful of UUIDs rather than one per photo in the album.
This also makes the album resilient to changes in Immich. Photos removed from the album are ignored, and photos added later appear at the end rather than being hidden — so `gallery.yaml` never has to be kept in sync by hand.
The `/admin` page builder has a drag & drop editor for this (album → **Photo order ** → **Manual ** → **Reorder photos ** ), which is considerably easier than writing asset UUIDs by hand. It only writes a cleaned-up `assetOrder` when you explicitly apply it.
> [!NOTE]
> Two consequences worth knowing. On a subpage with several albums, `sort` orders photos _within_ each album — the albums themselves stay in the order they are listed in. And lightbox permalinks (`#photo-3`) are positional, so changing an album's sort moves where a previously shared link lands.
> [!TIP]
> `immich` means "inherit the album's sort setting from Immich", which is capture-time order in one direction or the other. It is not the manual drag order from the Immich web UI — Immich does not expose that order through its API, so Folio cannot reproduce it. Use `manual` for a hand-picked sequence.
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
## Grid Layout
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>
2026-08-15 09:09:18 +02:00
The global default lives in `settings.yaml` ; subpages and individual albums can
override it in `gallery.yaml` . The three levels merge, most specific winning:
**global (`settings.yaml` ) → subpage → album**
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-08-23 23:18:14 +02:00
> [!NOTE]
> Leaving `gap` out is not the same as setting `12`. Each theme preset picks its
> own photo spacing (2px `minimal`, 32px `editorial`, 40px `monograph`), and an
> unset `gap` lets that stand. Setting one wins in every preset — which is what
> `minimal`, `editorial` and `monograph` used to ignore
> ([#513](https://github.com/ralksta/immich-folio/issues/513)).
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
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>
2026-08-15 09:09:18 +02:00
# settings.yaml — global defaults
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
grid:
2026-03-12 11:30:45 +00:00
columns: 3 # number of columns (default: 3)
2026-08-23 23:18:14 +02:00
gap: 12 # gap in pixels — unset means the theme preset decides
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>
2026-08-15 09:09:18 +02:00
aspectRatio: '1' # "1", "3/2", "2/3", "16/9", "auto" (default: "1")
layout: masonry
```
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
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>
2026-08-15 09:09:18 +02:00
```yaml
# gallery.yaml — per-subpage override
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
subpages:
- name: Japan
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>
2026-08-15 09:09:18 +02:00
grid:
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
columns: 4
layout: uniform
2026-03-12 11:30:45 +00:00
aspectRatio: '3/2'
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>
2026-08-15 09:09:18 +02:00
albums:
- '33333333-3333-3333-3333-333333333333'
```
Available layouts: `masonry` , `uniform` , `showcase` , `filmstrip` ,
`editorial-flow` , `justified` (experimental), and `essay`
(see [Journal & Photo Essays ](journal.md )). Each is illustrated in the
**[Theming guide ](theming.md#gridlayout )**.
2026-08-23 23:18:14 +02:00
### Album covers on a subpage
A subpage with more than one album shows a grid of album covers rather than
photos. That grid follows the same `columns` value — set it globally in
`settings.yaml` or per page in `gallery.yaml` , and the covers are tiled the same
way the photos are. Valid range is 1– 6; below 1024px viewport width at most two
covers share a row, and below 640px they stack.
`gap` is the deliberate exception: it does **not ** cascade from the global
setting to the covers. Every theme preset picks its own cover spacing as part of
its look — 1px in `monograph` , 2px in `minimal` and `editorial` , 20px in
`studio-modern` — and letting the site-wide photo gap through would flatten all
of them into one look. Only an explicit `gap` on the subpage overrides the
preset:
```yaml
# gallery.yaml — album covers on this page only
subpages:
- name: Editorial
grid:
columns: 3 # three covers per row (site-wide default otherwise)
gap: 24 # overrides the theme's own cover spacing
albums:
- '33333333-3333-3333-3333-333333333333'
- '44444444-4444-4444-4444-444444444444'
```
Both fields are editable per page in the admin panel under
**Pages › _ subpage _ › Album Cover Grid**; left empty they follow the site-wide
setting and the theme respectively.
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>
2026-08-15 09:09:18 +02:00
## Site Behaviour
Feature toggles in `settings.yaml` :
```yaml
title: 'My Portfolio'
subtitle: 'A visual journal'
lang: 'en'
2026-08-23 23:18:14 +02:00
mode: dark # dark (default) | light | auto — what a first-time visitor sees
exifOnHover: true # the summary overlay on photo grid hover
exif: # which groups may be shown at all
camera: true # body, lens, focal length
settings: true # aperture, shutter speed, ISO
location: true # city and country
caption: true # the description written in Immich
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>
2026-08-15 09:09:18 +02:00
map: true # the interactive world map at /map
transitions: true # smooth page transitions between routes
scrollToTop: true # floating back-to-top arrow on long pages
analytics: true # cookieless view counts, see below
about:
enabled: true # the /about page, rendered from content/about.md
```
| Key | Default | Notes |
| --------------- | ------- | ------------------------------------------- |
2026-08-23 23:18:14 +02:00
| `lang` | `en` | Interface language, `<html lang>` , dates |
| `mode` | `dark` | Visitor's starting colour mode, see below |
| `exifOnHover` | on | The grid hover overlay only, see below |
| `exif.*` | on | Per-group EXIF visibility, see below |
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>
2026-08-15 09:09:18 +02:00
| `map` | **off ** | Opt-in: must be set to `true` explicitly |
| `transitions` | on | |
| `scrollToTop` | on | |
| `analytics` | on | |
| `about.enabled` | on | Needs a `content/about.md` to show anything |
2026-08-23 23:18:14 +02:00
### Colour mode
`mode` decides what a visitor sees before they touch anything: `dark` (the
default the site has always had), `light` , or `auto` to follow their operating
system. It is rendered onto `<html>` server-side, so the first paint is already
right rather than flipping after the page loads.
A visitor's own choice from the header toggle is kept in their browser and
always wins over this — `mode` only sets the starting point. The light/dark
switch in the admin panel previews _ your _ view and does not change what
visitors get; that is what this setting is for
([#512 ](https://github.com/ralksta/immich-folio/issues/512 )).
### EXIF visibility
`exif` decides what the site may publish about a photo. It governs all three
places the data appears — the grid hover overlay, the lightbox info panel, and
the camera line in the album header — so they cannot disagree.
`caption` is the description written in Immich. It used to be shown regardless
of any setting, on the grounds that a caption is editorial rather than
metadata — which left it as the one field nobody could switch off, and Immich
descriptions are also where people keep private notes and whatever a decade of
cataloguing software left behind ([#506 ](https://github.com/ralksta/immich-folio/issues/506 )).
It is a group like any other now.
`exifOnHover` predates the block and stays: it decides whether the grid shows
its overlay at all, and it supplies the default for the three technical groups,
so an existing `exifOnHover: false` still means "no technical EXIF anywhere". An
explicit `exif` key always wins over it. Captions never followed that switch and
still do not — set `caption: false` to turn them off.
With every group off, the lightbox withdraws its info button and the `i` key
rather than opening an empty panel.
### Interface language
`lang` picks the language of the visitor-facing interface — navigation, the
lightbox, the password gate, the proofing dialog, the legal notice, error pages
— and is also emitted as `<html lang>` and used for date formatting.
| Value | Interface |
| ----- | ----------------- |
| `en` | English (default) |
| `de` | German |
Any other value (`fr` , `ja` , …) still reaches `<html lang>` and
`toLocaleDateString` , but the interface stays English: there is no dictionary
for it yet. That is deliberate — claiming `lang="en"` on a French site would be
worse for screen readers than an English interface on a page correctly marked
as French.
Two things stay in one language by design:
- **The admin panel** is always English, whatever `lang` says.
- **`/impressum` ** keeps its German headings when `lang: de` and the § 5 TMG
citation in both, because that is what the statute names. Under `lang: en`
the footer link reads "Legal Notice" rather than "Impressum" — the page is
optional (`legal.enabled` ), and an unexplained German word in the footer of
an English site is what drove this out of the backlog.
Adding a language means adding one file under `lib/i18n/locales/` and listing
it in `SUPPORTED_LOCALES` (`lib/i18n/index.ts` ). The dictionary is typed
against the English one, so a missing key fails the type-check rather than
falling back silently, and `lib/__tests__/i18n.test.ts` additionally catches
keys that were copied over but never translated.
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>
2026-08-15 09:09:18 +02:00
### Analytics
Page and album view counts, stored in `content/analytics.json` — no cookies, no
third party, nothing leaving your server. The numbers are shown in the admin
panel's **Analytics ** tab.
```yaml
analytics: false # stop counting entirely
```
With `analytics: false` the tracking endpoint refuses to record, so no data is
collected regardless of what the browser sends.
## Navigation Links
**Experimental.** External links appended to the header navigation. Only
`http(s)` URLs are accepted, and they open in a new tab:
```yaml
navLinks:
- label: 'Shop'
url: 'https://shop.example.com'
- label: 'Instagram'
url: 'https://instagram.com/your-handle'
```
## Client Proofing
Lets a visitor mark favourites and export the selection — for handing a client a
gallery and getting their picks back. A heart button sits on every photo in the
grid; once something is picked, a bar appears with a filter and an export
dialog.
The selection is encoded into the URL as a compact bitmask and mirrored into
`localStorage` , so **nothing is stored server-side ** and a set of picks can be
shared, bookmarked, or sent back as a plain link.
Configured in `settings.yaml` :
```yaml
proofing:
enabled: true # hearts, selection bar and export modal (default: true)
allowMailto: true # offer "send by email" alongside the copyable list (default: true)
```
A subpage overrides `enabled` in either direction — useful to keep proofing off
across a public portfolio and switch it on for a single client handover:
```yaml
# gallery.yaml
subpages:
- name: Wedding – Smith
password: clientpass123
proofing: true # or `false` to disable it on this subpage only
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
albums:
- ...
```
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>
2026-08-15 09:09:18 +02:00
Precedence is **subpage → global ** : a subpage's own `proofing:` wins, and an
album reached without a subpage follows `proofing.enabled` .
> [!NOTE]
> Photo essays are the exception. A published story is not an album handover,
> so an essay (`layout: essay`, `essayFile`, `essayText`) shows the proofing
> controls only when its subpage sets `proofing: true` **explicitly** — the
> global default does not reach into essays. Journal entries never show them.
## Image Protection & Watermark
```yaml
protection:
disableRightClick: true # suppress the context menu on images
disableImageDrag: true # block drag-to-desktop
watermark:
enabled: true
text: '© Your Name'
2026-08-23 23:18:14 +02:00
opacity: 0.5 # fraction, 0– 1: 0.5 is 50%
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>
2026-08-15 09:09:18 +02:00
position: 'bottom-right' # bottom-right | bottom-left | center
```
2026-08-23 23:18:14 +02:00
`opacity` is a fraction between 0 and 1, the same unit the admin slider writes.
A value above 1 is read as a percentage (`90` means 0.9), so configs written
that way before the unit was fixed keep working.
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>
2026-08-15 09:09:18 +02:00
> [!NOTE]
> Both are deterrents against casual copying, not protection. The image is in
> the browser, and anyone who wants it can take a screenshot. The watermark is
> an overlay on the lightbox, not burned into the file.
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
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>
2026-08-15 09:09:18 +02:00
## SEO
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
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>
2026-08-15 09:09:18 +02:00
seo:
title: 'My Portfolio | Photography'
description: 'A curated selection of my best photography work.'
titleTemplate: '%s | My Portfolio' # %s is the subpage or album title
noIndex: false # true → ask search engines not to index the site
noFollow: false # true → ask search engines not to follow links
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
```
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>
2026-08-15 09:09:18 +02:00
`titleTemplate` shapes the browser and search-result title of every subpage and
album; the homepage uses `title` unchanged.
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
## Footer
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>
2026-08-15 09:09:18 +02:00
Optional minimal footer with social links, in `settings.yaml` :
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
footer:
name: John Doe
instagram: johndoe
email: hello@example .com
website: https://example.com
```
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>
2026-08-15 09:09:18 +02:00
A `legal:` block with `enabled: true` adds an Impressum page at `/impressum` and
links it in the footer — see `content/settings.yaml.example` for every field.
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
## About Page
Create `content/about.md` with YAML frontmatter:
```markdown
---
portrait: asset-uuid-for-your-portrait
name: Your Name
location: City, Country
gear:
- Camera Body
- Favorite Lens
---
Your bio text here. Supports full Markdown.
```
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>
2026-08-15 09:09:18 +02:00
The page is served at `/about` and linked in the navigation. Turn it off in
`settings.yaml` without deleting the file:
```yaml
about:
enabled: false
```
The admin panel's **Settings → About ** section edits the same file — portrait,
name, location, gear list and bio — so `about.md` does not have to be written by
hand.
## Journal & Photo Essays
Long-form storytelling with fullbleed photos, side-by-side pairs, quotes and
captions lives in `content/journal/` , served at `/journal` . A subpage can also
be turned into a single photo essay.
→ * * [Journal & Photo Essays ](journal.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
## Finding UUIDs
- **Album UUIDs**: In Immich, go to Albums → click an album → the UUID is in the URL bar
- **Asset UUIDs**: Click any photo → the UUID is in the URL bar
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
2026-04-24 22:14:53 +02:00
## System & Security
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
2026-04-24 22:14:53 +02:00
Advanced system settings are configured via environment variables in your `.env` or `.env.local` file.
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
2026-04-24 22:14:53 +02:00
### Rate Limiting
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
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>
2026-08-15 09:09:18 +02:00
To protect against brute-force attacks and resource exhaustion, Immich Folio includes an in-memory rate limiter. Each endpoint has its own bucket, so heavy image traffic cannot exhaust the budget for the password endpoints.
- `RATE_LIMIT_RPM` : requests per minute per IP for image, video, EXIF and health requests (default: `1500` ).
The endpoints where a high limit would be the wrong default are fixed and not configurable:
| Endpoint | Limit (req/min/IP) |
| ---------------------- | ------------------ |
| `POST /api/admin/auth` | 5 |
| `POST /api/auth` | 10 |
| `/api/install` | 10 |
| `/api/og` | 30 |
| `/api/install/albums` | 30 |
| `/api/webhook` | 60 |
| `/api/map` | 120 |
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
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>
2026-08-15 09:09:18 +02:00
> [!IMPORTANT]
> The limiter lives in the process memory of a single instance. It does not
> coordinate across replicas, and it starts empty after a restart.
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
2026-04-24 22:14:53 +02:00
### Trusted Proxies
2026-07-20 18:57:57 +02:00
If you run Immich Folio behind a reverse proxy (nginx, Traefik, Caddy, Cloudflare), tell it how many proxies sit in front. Without this, the rate limiter reads a client-supplied header and an attacker can pick their own bucket — defeating brute-force protection on the password endpoints.
- `TRUSTED_PROXY_HOPS` : Number of reverse proxies in front of the app (default: `0` ).
2026-04-24 22:14:53 +02:00
```bash
2026-07-20 18:57:57 +02:00
# nginx / Traefik / Caddy directly in front of the app
TRUSTED_PROXY_HOPS=1
# Cloudflare in front of nginx
TRUSTED_PROXY_HOPS=2
2026-04-24 22:14:53 +02:00
```
2026-07-20 18:57:57 +02:00
The client IP is taken that many entries from the **right ** of `X-Forwarded-For` . Proxies append the address they actually observed, so everything to the left is client-supplied and ignored — a visitor sending `X-Forwarded-For: 1.1.1.1` cannot influence which bucket they land in.
> ⚠️ **The app must be reachable only through the proxy.** Bind it to localhost (`127.0.0.1:3000`) or keep it on an internal Docker network. If clients can connect directly they control the entire header, and no setting can recover the real IP.
Matching nginx config:
```nginx
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
`$proxy_add_x_forwarded_for` is the important one: it _ appends _ the real peer address rather than overwriting the header.
2026-07-20 18:57:57 +02:00
#### Migrating from `TRUSTED_PROXIES`
`TRUSTED_PROXIES` has been removed. It matched proxy IPs against the socket peer address, which a self-hosted Next.js server never exposes — so the check never passed. Setting it silently put **every visitor into one shared rate-limit bucket ** , letting a single client trip the limit for the entire site. If it is still set, the app assumes `TRUSTED_PROXY_HOPS=1` and logs a warning; replace it with an explicit value.
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397)
* docs: brainstorm resilience/adoption features and plan follow-up work
Brainstorm and deep-dive on where Immich Folio breaks under Immich
outages and what blocks self-hosted adoption, plus an implementation
plan for the remaining gaps after v0.9.2 (route-handler tests,
stale-while-error caching, loading skeletons).
* test: enable route-handler tests and cover admin guards and image content-type
vitest.config.ts only ran lib/__tests__/**, so route handlers were
structurally untestable. Two mechanisms live only at the route level and
cannot be extracted the way lib/imageSize.ts was: the admin auth guards
and the image proxy's Content-Type sanitisation (the stored-XSS fix from
c2fa8e7). Both were correct but unverified by any test.
Adds a table-driven test over all ten guarded /api/admin/* handlers,
including a filesystem walk that fails if a future route is added
without a row in the table. Adds a route-level test for
/api/image/[id] covering token validation, the six Content-Type
rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError)
distinction.
Verified both catch real regressions by temporarily removing the admin
guards and the Content-Type rewrite and confirming the expected tests
fail, then restoring and confirming green again.
134 new tests (244 total, up from 210), tsc 0 errors, build succeeds.
* feat: serve stale cache entries while Immich is unavailable
v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable"
(ImmichUnavailableError), and pages now render a 5xx instead of a false
404 during an outage — but that 5xx still replaced a gallery that had
loaded fine seconds earlier. lib/cache.ts deleted expired entries on
access, so nothing survived long enough to fall back to.
Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL,
after which get() still reports a miss) and hardExpiresAt (the new
STALE_MAX_AGE, default 24h, after which the entry is dropped entirely).
Between the two, getStale() can still return the data.
getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError
and fall back to getStale() before propagating. The two MISSING-sentinel
writes (definitive 404s) are deliberately left on the short cacheTtl
window — a corrected asset ID should take effect after the normal TTL,
not sit in the 24h stale window meant for outages.
STALE_MAX_AGE=0 disables the fallback. Verified the fallback only
engages on a real outage, not on a cache hit at the TTL boundary, and
that a definitive 404 still reports missing rather than falling back.
256 tests (up from 244), tsc 0 errors, build succeeds.
* feat: add loading skeletons for dynamic gallery routes
Every gallery route is force-dynamic and waits on a live Immich
round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling —
with nothing rendered until it resolves. loading.tsx renders instantly
while the page component awaits its data.
Not browser-verified against live data: this worktree has no
.env.local and only .example content files, so the app boots into
setup mode (confirmed the dev server starts cleanly and the setup
screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint
errors.
* docs: document stale cache window, image token revocation, route-test convention
- README: STALE_MAX_AGE env var and a "Behaviour when Immich is
unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404,
outages never cached).
- docs/gallery-config.md: new "Image Token Revocation" subsection under
System & Security — un-publishing an album does not revoke already
issued image URLs; rotating AUTH_SECRET does.
- CLAUDE.md: updated cache description to cover the stale window and
MISSING sentinel, and a new code-convention note that route-level
logic (guards, header sanitisation) should get a route-level test
rather than being extracted into lib/ just to reach the test runner.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
### Image Token Revocation
Asset tokens are deterministic — the same photo always yields the same token, which is what lets browsers cache images for a year. The image proxy validates the token but does not re-check album membership on every request.
Removing an album from `gallery.yaml` hides it from the site, but any image URL already handed out (bookmarked, embedded, cached by a browser) keeps working. To invalidate previously issued links, rotate `AUTH_SECRET` — this also signs out every password-protected gallery and admin session.