release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of [CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md) for the full list. Highlights: client proofing links (#683), custom content pages (#738), asset picker search (#740), concurrent-edit detection (#742), env-locked settings (#739), an accessibility baseline (#741), three security fixes with advisories (#737, #755, #759), and a pre-release quality pass (#780–#797). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
@@ -39,6 +39,7 @@ Each area has its own URL, so a section can be bookmarked and the browser's back
|
||||
| **Pages** | `/admin/pages` | `content/gallery.yaml` |
|
||||
| **Journal** | `/admin/journal` | `content/journal/<slug>.md` |
|
||||
| **Settings** | `/admin/settings` | `content/settings.yaml`, `content/about.md` |
|
||||
| **Proofing** | `/admin/proofing` | `content/proofing.json` |
|
||||
| **Analytics** | `/admin/analytics` | nothing — read-only |
|
||||
| **Diagnostics** | `/admin/diagnostics` | nothing — read-only |
|
||||
| **Help** | `/admin/help` | nothing — reference only |
|
||||
@@ -127,6 +128,14 @@ Theme presets, grid layouts, photo frames, hero styles and the Google search sni
|
||||
|
||||
**General → Favicon** accepts an SVG, PNG, ICO or JPEG. It is stored in the writable `content/` volume and served through `/api/favicon` under a policy that stops an uploaded SVG from executing script. **Reset** restores the bundled default.
|
||||
|
||||
## Proofing
|
||||
|
||||
Private client links: pick a client name and any Immich album — published or
|
||||
not — and optionally an expiry date and download rights. Each card shows the
|
||||
link's state (open, submitted, expired), what the client picked, and the export
|
||||
buttons. See [Client Proofing Links](gallery-config.md#client-proofing-links)
|
||||
for how the client side works.
|
||||
|
||||
## Analytics
|
||||
|
||||
View counts per page and album, read from `content/analytics.json`. No cookies, no third party, nothing leaving your server. Switch the collection off entirely in **Settings → General**; the tracking endpoint then refuses to record.
|
||||
|
||||
@@ -215,7 +215,7 @@ CDN_URL=https://cdn.example.com
|
||||
```
|
||||
|
||||
```
|
||||
https://cdn.example.com/api/image/v2:…?size=preview&w=1080&q=75
|
||||
https://cdn.example.com/api/image/v2:…?size=preview&w=1440
|
||||
```
|
||||
|
||||
The CDN uses Immich Folio as its origin. The first request for a photo is a
|
||||
@@ -230,7 +230,7 @@ Folio URL, then:
|
||||
|
||||
- **Cache `/api/image/*` and `/api/video/*`**, and nothing else. Pages are
|
||||
rendered per request, and other routes check cookies.
|
||||
- **Include the full query string in the cache key.** `size`, `w`, `q` and the
|
||||
- **Include the full query string in the cache key.** `size`, `w` and the
|
||||
`IMAGE_CACHE_VERSION` buster `v` all select a different file.
|
||||
- **Respect the origin's `Cache-Control`.** Photos come back
|
||||
`public, max-age=31536000, immutable`; errors (429, 503, 404) come back
|
||||
|
||||
@@ -35,6 +35,7 @@ cp content/settings.yaml.example content/settings.yaml
|
||||
- [Site Behaviour](#site-behaviour)
|
||||
- [Navigation Links](#navigation-links)
|
||||
- [Client Proofing](#client-proofing)
|
||||
- [Client Proofing Links](#client-proofing-links)
|
||||
- [Image Protection & Watermark](#image-protection--watermark)
|
||||
- [SEO](#seo)
|
||||
- [Footer](#footer)
|
||||
@@ -269,6 +270,12 @@ so an edited URL cannot reach anything else. The ZIP keeps the original
|
||||
filenames (deduplicated on collision) and is streamed, so album size does not
|
||||
drive memory use.
|
||||
|
||||
One visitor (by client IP, see `TRUSTED_PROXY_HOPS`) can have at most two ZIPs
|
||||
downloading at once, album and proofing downloads counted together; a third
|
||||
gets a "too many requests" page until one finishes. A ZIP whose download stops
|
||||
reading for 60 seconds is closed, so an abandoned or stalled download does not
|
||||
hold its connection to Immich open.
|
||||
|
||||
## Password Protection
|
||||
|
||||
A password can be set on the whole site, on a subpage, on a single album, and on
|
||||
@@ -617,8 +624,16 @@ Configured in `settings.yaml`:
|
||||
proofing:
|
||||
enabled: true # hearts, selection bar and export modal (default: true)
|
||||
allowMailto: true # offer "send by email" alongside the copyable list (default: true)
|
||||
email: proofs@example.com # recipient of that email (default: footer.email)
|
||||
```
|
||||
|
||||
The email draft goes to `proofing.email`, or to the footer contact email
|
||||
(`footer.email`) when that is unset. With neither configured the dialog hides
|
||||
the email button rather than opening a draft with an empty **To:**. The address
|
||||
is set in the admin panel under **Settings → General → Proofing email**, and it
|
||||
reaches the page encoded like the footer address, so it does not appear in the
|
||||
HTML in plain text.
|
||||
|
||||
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:
|
||||
|
||||
@@ -641,6 +656,59 @@ album reached without a subpage follows `proofing.enabled`.
|
||||
> controls only when its subpage sets `proofing: true` **explicitly** — the
|
||||
> global default does not reach into essays. Journal entries never show them.
|
||||
|
||||
## Client Proofing Links
|
||||
|
||||
The proofing above is anonymous: picks live in the visitor's browser and come
|
||||
back to you as a link or an email, if they come back at all. A **client
|
||||
proofing link** is the handover version. Create one per client in
|
||||
**Admin → Proofing**:
|
||||
|
||||
- **Client** — a name for your overview and the greeting on the page.
|
||||
- **Album** — any Immich album. It does not have to be in `gallery.yaml`; the
|
||||
link itself is what grants access to it.
|
||||
- **Valid until** — optional. From the day after, the link shows "This link has
|
||||
expired" and every action on it is refused.
|
||||
- **Downloads** — none, the client's selection as a ZIP, or the whole album as
|
||||
a ZIP, with an optional limit on how many ZIPs the link may start.
|
||||
|
||||
The client opens `https://your-site/proof/<token>` and hearts photos as usual.
|
||||
Every change is **saved on the server** within a second, so they can stop and
|
||||
come back later, from another device too. **Submit selection** locks it and
|
||||
notifies you. Until then, you can watch the picks come in under
|
||||
**Admin → Proofing**. **Reopen for changes** unlocks a submitted selection.
|
||||
|
||||
**Export.** Each link offers:
|
||||
|
||||
- **Copy file names (Lightroom)** — the selected file names without extension,
|
||||
comma-separated. Pasted into Lightroom's or Capture One's filename filter,
|
||||
`IMG_0412` finds the RAW as well as the JPEG the client saw.
|
||||
- **CSV** — position in the album, file name and capture time.
|
||||
- **TXT** — full file names, one per line.
|
||||
|
||||
**Notification.** Set `PROOFING_WEBHOOK_URL` and every first submit POSTs JSON
|
||||
to it. The body carries `content` (Discord), `text` (Slack, Mattermost),
|
||||
`title` and `message` (Gotify) and a structured `proofing` object (n8n, Home
|
||||
Assistant, your own script), so most receivers work without an adapter. With
|
||||
`SITE_URL` set, the message links straight to the selection in the admin panel.
|
||||
A failing webhook never fails the client's submit.
|
||||
|
||||
```env
|
||||
PROOFING_WEBHOOK_URL=https://discord.com/api/webhooks/…
|
||||
```
|
||||
|
||||
**Security notes.**
|
||||
|
||||
- The token in the link is the only credential: 192 random bits, never shown
|
||||
in the admin panel's own URLs. Treat the link like a password; delete it in
|
||||
the admin panel to revoke it at once.
|
||||
- Links are stored in `content/proofing.json` (mode `0600`), next to
|
||||
`analytics.json`, and are not part of the YAML backups.
|
||||
- A site-wide password still applies: on a locked site, the client needs it
|
||||
too.
|
||||
- Proofing pages are `noindex`, left out of the sitemap, and send no
|
||||
`Referer`.
|
||||
- A subpage slugged `proof` would be hidden behind this route.
|
||||
|
||||
## Image Protection & Watermark
|
||||
|
||||
```yaml
|
||||
@@ -744,6 +812,79 @@ The admin panel's **Settings → About** section edits the same file — portrai
|
||||
name, location, gear list and bio — so `about.md` does not have to be written by
|
||||
hand.
|
||||
|
||||
## Content Pages
|
||||
|
||||
Pages beyond the gallery — pricing and packages, workshops, press, a booking
|
||||
FAQ — are content pages. Each one is a file, `content/pages/<slug>.md`, served
|
||||
at `/<slug>`.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 'Pricing'
|
||||
description: 'Packages and rates for portrait and wedding sessions'
|
||||
draft: true
|
||||
---
|
||||
|
||||
## Portrait sessions
|
||||
|
||||
One hour on location, 25 edited photos.
|
||||
|
||||

|
||||
```
|
||||
|
||||
The frontmatter keys are `title`, `description` (used for search engines and
|
||||
link previews), `password` and `draft`. The body uses the journal's block
|
||||
syntax ([Journal & Photo Essays](journal.md)): headings, text and photo blocks
|
||||
(`:wide` or `:fullbleed`). A page has no date, no reading time and no index,
|
||||
and it has no map block.
|
||||
|
||||
### In the menu
|
||||
|
||||
A page appears in the header menu through a reference among the subpages in
|
||||
`gallery.yaml`, at the position it should take:
|
||||
|
||||
```yaml
|
||||
subpages:
|
||||
- name: Landscapes
|
||||
albums:
|
||||
- 'album-uuid-1'
|
||||
- page: pricing
|
||||
```
|
||||
|
||||
A page without a reference is still reachable at `/<slug>`, but it is not in
|
||||
the menu, which suits a page that is only shared by link. The menu stays flat:
|
||||
a page cannot sit inside a subpage's sections. A reference to a page whose file
|
||||
does not exist is skipped, and the config doctor reports it.
|
||||
|
||||
### Slugs
|
||||
|
||||
A page's slug must not be taken by a subpage, a standalone album, a journal
|
||||
entry or one of the built-in routes (`about`, `map`, `journal`, `impressum`,
|
||||
`privacy`, `install`, `admin`, `gate`, `api`, `proof`, `contact`). The admin
|
||||
panel refuses to save a page on such a slug, and the doctor reports a collision
|
||||
in a hand-edited file. Renaming a page's slug breaks old links: there are no
|
||||
automatic redirects.
|
||||
|
||||
### Draft and password
|
||||
|
||||
A draft is visible only to a signed-in admin, and it is left out of the menu and
|
||||
the sitemap. A `password` locks the page like a journal entry, with its own
|
||||
cookie (`lb_auth_page_<slug>`). The site password, when one is set, applies
|
||||
before either. Password-protected pages are not listed in the sitemap.
|
||||
|
||||
### In the admin panel
|
||||
|
||||
Admin → Pages lists pages next to the subpages, with a document icon and a
|
||||
PAGE tag, and a separate "Not in menu" group for pages without a reference.
|
||||
**+ New page** creates one (it starts blank and as a draft). Select a page to
|
||||
edit its title, URL, draft flag, password and description, and to switch
|
||||
"Show in menu"; dragging a page into or out of "Not in menu" does the same.
|
||||
The menu change is saved with **Save Changes**, like any other change to the
|
||||
structure. **Edit content →** opens the journal's block editor with a live
|
||||
preview. Every save keeps a backup in `content/pages/.backups/`, and deleting a
|
||||
page that is in the menu also removes its `gallery.yaml` reference; both files
|
||||
are backed up first.
|
||||
|
||||
## Journal & Photo Essays
|
||||
|
||||
Long-form storytelling with fullbleed photos, side-by-side pairs, quotes and
|
||||
|
||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 103 KiB |