release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not, so this is a clean sync. ## Headline changes since v0.10.0 - **Journal & Journal Studio** — photo essays at `/journal`, split-screen block editor with live preview, drafts, per-entry passwords, cover images - **First-run setup wizard at `/install`** — configure a fresh deployment from the browser; credentials are verified against Immich before anything is written, and the wizard is gated by a one-time token - **About page editor and toggle** in the admin panel - **Per-album photo order** independent of Immich, with a drag & drop editor - **Custom favicon upload** - **Experimental portfolio features** — justified grid, `cover` hero, unlisted subpages, per-album grid overrides, cover focal points, external nav links - **Security** — asset tokens length-capped before decoding (#423), journal author markdown escaped rather than filtered, journal file paths contained, admin session key derived with scrypt (#432), Next.js 16.3.0 for CVE-2025-13465 - **Fixed** — client proofing honours its own configuration again (#454); journal photo rendering (#432) Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15). ## Verified `npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run build` succeeds · lint 0 errors · `prettier --check` clean · local `docker build` of the release tree. ## After merge Tag `v0.11.0` on `main` and publish the release — that triggers `docker-publish.yml`, which pushes the image and moves `latest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,50 @@
|
||||
# Gallery Configuration
|
||||
|
||||
All gallery structure is defined in `content/gallery.yaml`. Copy the example to get started:
|
||||
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:
|
||||
|
||||
```bash
|
||||
cp content/gallery.yaml.example content/gallery.yaml
|
||||
cp content/settings.yaml.example content/settings.yaml
|
||||
```
|
||||
|
||||
`settings.yaml` is optional — without it, everything falls back to defaults.
|
||||
|
||||
> [!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.
|
||||
|
||||
**Contents:**
|
||||
|
||||
`gallery.yaml`
|
||||
|
||||
- [Hero Images](#hero-images)
|
||||
- [Standalone Albums](#standalone-albums)
|
||||
- [Subpages & Categories](#subpages)
|
||||
- [Per-Album Options](#per-album-options)
|
||||
- [Photo Order Within an Album](#photo-order-within-an-album)
|
||||
|
||||
`settings.yaml`
|
||||
|
||||
- [Grid Layout](#grid-layout)
|
||||
- [EXIF on Hover](#exif-on-hover)
|
||||
- [Site Behaviour](#site-behaviour)
|
||||
- [Navigation Links](#navigation-links)
|
||||
- [Client Proofing](#client-proofing)
|
||||
- [Image Protection & Watermark](#image-protection--watermark)
|
||||
- [SEO](#seo)
|
||||
- [Footer](#footer)
|
||||
|
||||
Other
|
||||
|
||||
- [About Page](#about-page)
|
||||
- [Journal & Photo Essays](journal.md)
|
||||
- [Finding UUIDs](#finding-uuids)
|
||||
- [System & Security](#system--security)
|
||||
|
||||
## Hero Images
|
||||
|
||||
@@ -82,6 +108,43 @@ subpages:
|
||||
- 33333333-3333-3333-3333-333333333333
|
||||
```
|
||||
|
||||
### Subpage Options
|
||||
|
||||
| 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 — 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 |
|
||||
|
||||
```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.
|
||||
|
||||
### 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.
|
||||
@@ -94,18 +157,7 @@ Each section can have:
|
||||
| `description` | string | ➖ | Optional subline under the heading |
|
||||
| `albums` | list | ✅ | Album UUIDs (same format as regular albums) |
|
||||
|
||||
Within the `albums` list you can use a plain UUID, a simple title override, or an object with any of `title`, `description`, `password`, `heroImage`, `sort` and `assetOrder`:
|
||||
|
||||
```yaml
|
||||
albums:
|
||||
- 'album-uuid' # → uses the Immich album name
|
||||
- 'album-uuid': My Title # → displays "My Title" instead
|
||||
- 'album-uuid':
|
||||
title: My Private Title
|
||||
password: 'secure-password' # → adds a password gate to this specific album
|
||||
heroImage: 'asset-uuid' # → overrides the album cover
|
||||
sort: filename # → see "Photo Order Within an Album" below
|
||||
```
|
||||
Within the `albums` list you can use a plain UUID, a simple title override, or an object — see [Per-Album Options](#per-album-options).
|
||||
|
||||
**Full example:**
|
||||
|
||||
@@ -135,6 +187,86 @@ subpages:
|
||||
> [!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.
|
||||
|
||||
## 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
|
||||
|
||||
A password can be set on a subpage, on a single album, and on a
|
||||
[journal entry](journal.md#password-protected-entries). Unlocking sets an
|
||||
`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).
|
||||
|
||||
## 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.
|
||||
@@ -178,37 +310,169 @@ The `/admin` page builder has a drag & drop editor for this (album → **Photo o
|
||||
|
||||
## Grid Layout
|
||||
|
||||
Configure the photo grid globally or per-subpage:
|
||||
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**
|
||||
|
||||
```yaml
|
||||
# Global defaults
|
||||
# settings.yaml — global defaults
|
||||
grid:
|
||||
columns: 3 # number of columns (default: 3)
|
||||
gap: 12 # gap in pixels (default: 12)
|
||||
aspectRatio: '1' # "1", "3/2", "auto", etc. (default: "1")
|
||||
layout: masonry # "masonry" | "uniform" | "showcase" | "filmstrip" | "editorial-flow"
|
||||
aspectRatio: '1' # "1", "3/2", "2/3", "16/9", "auto" (default: "1")
|
||||
layout: masonry
|
||||
```
|
||||
|
||||
```yaml
|
||||
# gallery.yaml — per-subpage override
|
||||
subpages:
|
||||
- name: Japan
|
||||
grid: # per-subpage override
|
||||
grid:
|
||||
columns: 4
|
||||
layout: uniform
|
||||
aspectRatio: '3/2'
|
||||
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)**.
|
||||
|
||||
## Site Behaviour
|
||||
|
||||
Feature toggles in `settings.yaml`:
|
||||
|
||||
```yaml
|
||||
title: 'My Portfolio'
|
||||
subtitle: 'A visual journal'
|
||||
lang: 'en'
|
||||
|
||||
exifOnHover: true # camera/lens/settings on photo grid hover
|
||||
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 |
|
||||
| --------------- | ------- | ------------------------------------------- |
|
||||
| `lang` | `en` | `<html lang>` and date formatting |
|
||||
| `exifOnHover` | on | |
|
||||
| `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 |
|
||||
|
||||
### 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
|
||||
albums:
|
||||
- ...
|
||||
```
|
||||
|
||||
## EXIF on Hover
|
||||
Precedence is **subpage → global**: a subpage's own `proofing:` wins, and an
|
||||
album reached without a subpage follows `proofing.enabled`.
|
||||
|
||||
Show camera/lens/settings when hovering over photos (enabled by default):
|
||||
> [!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
|
||||
exifOnHover: false # set to false to disable
|
||||
protection:
|
||||
disableRightClick: true # suppress the context menu on images
|
||||
disableImageDrag: true # block drag-to-desktop
|
||||
|
||||
watermark:
|
||||
enabled: true
|
||||
text: '© Your Name'
|
||||
opacity: 0.5
|
||||
position: 'bottom-right' # bottom-right | bottom-left | center
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
## SEO
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
|
||||
`titleTemplate` shapes the browser and search-result title of every subpage and
|
||||
album; the homepage uses `title` unchanged.
|
||||
|
||||
## Footer
|
||||
|
||||
Optional minimal footer with social links:
|
||||
Optional minimal footer with social links, in `settings.yaml`:
|
||||
|
||||
```yaml
|
||||
footer:
|
||||
@@ -218,6 +482,9 @@ footer:
|
||||
website: https://example.com
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## About Page
|
||||
|
||||
Create `content/about.md` with YAML frontmatter:
|
||||
@@ -235,6 +502,26 @@ gear:
|
||||
Your bio text here. Supports full Markdown.
|
||||
```
|
||||
|
||||
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)**
|
||||
|
||||
## Finding UUIDs
|
||||
|
||||
- **Album UUIDs**: In Immich, go to Albums → click an album → the UUID is in the URL bar
|
||||
@@ -246,9 +533,25 @@ Advanced system settings are configured via environment variables in your `.env`
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
To protect against brute-force attacks and resource exhaustion, Immich Folio includes an in-memory rate limiter.
|
||||
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`: Maximum requests per minute per IP (default: `1500` for general API, `120` for map data).
|
||||
- `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 |
|
||||
|
||||
> [!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.
|
||||
|
||||
### Trusted Proxies
|
||||
|
||||
|
||||
Reference in New Issue
Block a user