release: v0.20.0
This commit is contained in:
@@ -8,7 +8,7 @@ a quick start.
|
||||
| Guide | Covers |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| **[Gallery Configuration](gallery-config.md)** | `gallery.yaml` and `settings.yaml` — albums, subpages, sections, photo order, passwords, grid, SEO, proofing, watermark |
|
||||
| **[Theming](theming.md)** | The seven presets, custom colours and fonts, hero styles, grid layouts, with screenshots |
|
||||
| **[Theming](theming.md)** | The twelve presets, custom colours and fonts, hero styles, grid layouts, with screenshots |
|
||||
| **[Journal & Photo Essays](journal.md)** | Long-form storytelling — file format, block syntax, drafts, per-entry passwords, Journal Studio |
|
||||
| **[Admin Panel](admin-panel.md)** | The visual editor at `/admin` — pages, journal, settings, analytics, backups, security |
|
||||
| **[Deployment](deployment.md)** | Setup wizard internals, Docker, health check, reverse proxy, behaviour during an Immich outage |
|
||||
|
||||
@@ -18,6 +18,8 @@ For the quick path — clone, `npm run dev`, open `/install` — see the
|
||||
- [Behaviour when Immich is Unreachable](#behaviour-when-immich-is-unreachable)
|
||||
- [Reverse Proxy](#reverse-proxy)
|
||||
- [CDN Mode](#cdn-mode)
|
||||
- [Edits made in Immich](#edits-made-in-immich)
|
||||
- [Colour Profiles](#colour-profiles)
|
||||
|
||||
## What the Setup Wizard Writes
|
||||
|
||||
@@ -230,8 +232,9 @@ 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` and the
|
||||
`IMAGE_CACHE_VERSION` buster `v` all select a different file.
|
||||
- **Include the full query string in the cache key.** `size`, `w`, the edit
|
||||
marker `e` 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
|
||||
`no-store` and must not be cached.
|
||||
@@ -256,7 +259,63 @@ links, not the image URLs themselves.
|
||||
|
||||
**Changing a photo.** A CDN keeps immutable responses for as long as a browser
|
||||
does. Bump `IMAGE_CACHE_VERSION` after regenerating thumbnails in Immich, which
|
||||
changes every URL at once, or purge the CDN.
|
||||
changes every URL at once, or purge the CDN. Edits made in Immich's editor need
|
||||
neither: see [Edits made in Immich](#edits-made-in-immich).
|
||||
|
||||
The Content-Security-Policy allows the CDN's origin for `img-src` and
|
||||
`media-src` automatically.
|
||||
|
||||
## Edits made in Immich
|
||||
|
||||
Photos edited in Immich's own editor (crop, rotate) appear as edited
|
||||
everywhere: in the grid, the lightbox, the zoom, share images, the map and
|
||||
downloads. Immich keeps an edit apart from the file and applies it only when
|
||||
asked to, so Folio always asks for the edited rendition (for a photo that is
|
||||
not edited, Immich returns the same file either way; the parameter exists
|
||||
since Immich 2.5, below the 3.0 that Folio requires), and lays the grid out
|
||||
with the edited proportions.
|
||||
|
||||
The image URL of an edited photo carries an extra `e` parameter, derived from
|
||||
when the photo last changed in Immich, to the second. Image responses are cached as
|
||||
`immutable` for a year and the asset ID stays the same through an edit, so
|
||||
without it browsers and CDNs would keep showing the photo as it was before.
|
||||
Only edited photos get a new URL; all other URLs stay as they are. A later
|
||||
edit changes the parameter again, once Folio's album cache has picked it up
|
||||
(`CACHE_TTL`).
|
||||
|
||||
Downloads of an edited photo are Immich's rendition of the edit: a
|
||||
full-resolution JPEG without EXIF, named `.jpg`. Should Immich hand out the
|
||||
edit in a format the location scrubber does not handle (WebP, PNG), the photo
|
||||
is not downloaded rather than sent unchecked; a ZIP goes on without it and the
|
||||
server log names it.
|
||||
|
||||
## Colour Profiles
|
||||
|
||||
Visitors only ever get Immich's own renditions: the `thumbnail` for grid tiles
|
||||
and the `preview` for everything larger. Folio streams those bytes unchanged.
|
||||
It never decodes or re-encodes a photo, so whatever colour profile Immich
|
||||
embeds is what the browser sees, and colour fidelity is decided by how Immich
|
||||
builds its renditions.
|
||||
|
||||
Immich's relevant setting is `image.colorspace` (Administration → Settings →
|
||||
Image Settings), `p3` by default. Measured on Immich 3.2.0 with `p3`, thumbnail
|
||||
as WebP and preview as JPEG:
|
||||
|
||||
| Original | Thumbnail | Preview | Colours |
|
||||
| ------------------------------------------------ | ---------------------------- | ---------------------------- | ------------------------ |
|
||||
| JPEG, sRGB ICC profile | sRGB | sRGB | correct |
|
||||
| HEIC from an iPhone, Display P3 ICC profile | Display P3 | Display P3 | correct, wide gamut kept |
|
||||
| AVIF, no ICC profile, CICP (`nclx`) BT.2020 tags | sRGB (P3 for an iPhone shot) | sRGB (P3 for an iPhone shot) | 13–37% less saturated |
|
||||
|
||||
The last row is a loss in Immich, not in Folio. Immich ignores the CICP colour
|
||||
description of these AVIF files and labels the BT.2020 pixel values as sRGB (or
|
||||
Display P3) without converting them, so the browser shows them flatter than the
|
||||
original (mean chroma, compared with the decoded original). The files measured
|
||||
were AVIF exports, two of them confirmably from Lightroom. Exporting with an
|
||||
embedded ICC profile, or as JPEG or HEIC, should avoid it, since ICC-tagged
|
||||
originals came through correctly, but that has not been tested. Adobe RGB
|
||||
originals were not part of the measurement.
|
||||
|
||||
After changing `image.colorspace` or replacing originals, regenerate thumbnails
|
||||
in Immich and bump `IMAGE_CACHE_VERSION`, since browsers and CDNs keep the old
|
||||
renditions as `immutable`.
|
||||
|
||||
@@ -36,6 +36,7 @@ cp content/settings.yaml.example content/settings.yaml
|
||||
- [Navigation Links](#navigation-links)
|
||||
- [Client Proofing](#client-proofing)
|
||||
- [Client Proofing Links](#client-proofing-links)
|
||||
- [Photo Zoom](#photo-zoom)
|
||||
- [Image Protection & Watermark](#image-protection--watermark)
|
||||
- [SEO](#seo)
|
||||
- [Footer](#footer)
|
||||
@@ -122,6 +123,7 @@ subpages:
|
||||
| `grid` | object | Grid override for the photos on this page — see [Grid Layout](#grid-layout) |
|
||||
| `coverGrid` | object | Grid override for the album covers on this page — see [Album covers](#album-covers-on-a-subpage) |
|
||||
| `proofing` | boolean | Enables [client proofing](#client-proofing) on this page |
|
||||
| `zoom` | boolean | [Photo zoom](#photo-zoom) for the albums on this page, over the site setting |
|
||||
| `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 |
|
||||
@@ -218,6 +220,7 @@ albums:
|
||||
| `grid` | **Experimental.** Grid override for this album only |
|
||||
| `coverPosition` | **Experimental.** Focal point for the cover crop |
|
||||
| `download` | Offer originals for download — see [Originals download](#originals-download) |
|
||||
| `zoom` | [Photo zoom](#photo-zoom) for this album, over its page and the site setting |
|
||||
|
||||
### Per-album grid (experimental)
|
||||
|
||||
@@ -270,6 +273,19 @@ 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.
|
||||
|
||||
Originals keep their metadata except their location. JPEG, HEIC/HEIF and AVIF
|
||||
files are delivered with camera, lens, exposure, copyright/IPTC and colour
|
||||
profile intact, but with GPS coordinates removed from EXIF and XMP; the image
|
||||
data itself is not touched. Other formats (RAW, DNG, PNG, TIFF, WebP, video) are
|
||||
delivered exactly as Immich stores them, including any GPS. The diagnostics page
|
||||
warns when a download album contains such files.
|
||||
|
||||
A photo edited in Immich's own editor (crop, rotate) is downloaded as edited,
|
||||
not as the camera file. Immich renders the edit as a full-resolution JPEG
|
||||
without EXIF, so that file has no camera data either, and its name gets a
|
||||
`.jpg` extension (`IMG_0001.HEIC` arrives as `IMG_0001.jpg`). It passes the
|
||||
same location scrubber.
|
||||
|
||||
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
|
||||
@@ -323,13 +339,17 @@ 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:
|
||||
These 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. |
|
||||
| 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. |
|
||||
| `/impressum`, `/privacy` | The password page is public and links them; a German Impressum must be reachable directly. Neither shows an album or the site's menu. |
|
||||
| `/contact`, `/api/contact` | The Impressum links the form as its second contact channel. Honeypot, fill-time check and rate limit apply as on an open site. |
|
||||
|
||||
A legal or contact page that is switched off is still a 404 on a locked site.
|
||||
|
||||
A locked site also serves `noindex, nofollow` regardless of the SEO settings —
|
||||
there is nothing there for a crawler.
|
||||
@@ -480,6 +500,7 @@ 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
|
||||
zoom: false # 1:1 zoom in the lightbox, see Photo Zoom
|
||||
|
||||
about:
|
||||
enabled: true # the /about page, rendered from content/about.md
|
||||
@@ -495,6 +516,7 @@ about:
|
||||
| `transitions` | on | |
|
||||
| `scrollToTop` | on | |
|
||||
| `analytics` | on | |
|
||||
| `zoom` | **off** | Opt-in, see [Photo Zoom](#photo-zoom) |
|
||||
| `about.enabled` | on | Needs a `content/about.md` to show anything |
|
||||
|
||||
### Colour mode
|
||||
@@ -709,6 +731,103 @@ PROOFING_WEBHOOK_URL=https://discord.com/api/webhooks/…
|
||||
`Referer`.
|
||||
- A subpage slugged `proof` would be hidden behind this route.
|
||||
|
||||
## Photo Zoom
|
||||
|
||||
Visitors can zoom into a photo in the lightbox to full resolution — one image
|
||||
pixel per screen pixel, which is what judging sharpness takes. Off by default:
|
||||
it hands out the full-resolution file, so it is switched on deliberately.
|
||||
|
||||
```yaml
|
||||
# settings.yaml — the site-wide default
|
||||
zoom: true
|
||||
```
|
||||
|
||||
A subpage and an album can each override it, in either direction. The most
|
||||
specific setting wins: **album → subpage → site**. An album's `zoom:` belongs to
|
||||
the entry it is written on: an album listed on two pages can have zoom on one
|
||||
and off on the other, and each page keeps its own choice. Listed twice on the
|
||||
same page (in two sections), `false` wins.
|
||||
|
||||
```yaml
|
||||
# gallery.yaml
|
||||
subpages:
|
||||
- name: Clients
|
||||
zoom: true # every album on this page, unless it says otherwise
|
||||
albums:
|
||||
- 'album-uuid':
|
||||
zoom: false # not this one
|
||||
```
|
||||
|
||||
In the admin panel the switch is under **Settings › General › Portfolio
|
||||
Features**, and the page and album drawers in the page builder have an
|
||||
_Inherit / On / Off_ choice.
|
||||
|
||||
**Using it.** Double-click (or double-tap) zooms to 1:1 around the point
|
||||
clicked and back to fit; so does the magnifier button in the lightbox bar.
|
||||
Pinch, or Ctrl + scroll wheel / trackpad pinch, zooms continuously between fit
|
||||
and 1:1 — never further, since enlarging past 1:1 only interpolates pixels and
|
||||
makes good focus look soft. Drag (or one finger) pans; `+` / `-` step, `0` and
|
||||
`Esc` go back to fit (a second `Esc` closes the viewer). Swiping to the next
|
||||
photo is off while zoomed, and changing photos resets the zoom.
|
||||
|
||||
**What is shown.** The full-resolution file is requested only when a visitor
|
||||
zooms — never on opening the viewer, and never for the neighbouring photos.
|
||||
Until it arrives the preview is shown enlarged, with a small spinner on the
|
||||
zoom button.
|
||||
|
||||
| Original | Zoom shows |
|
||||
| ---------------------------------- | ---------------------------------------------- |
|
||||
| JPEG, AVIF | The original, with its location removed |
|
||||
| HEIC/HEIF, RAW/DNG, TIFF, JPEG XL… | Immich's full-size rendition, location removed |
|
||||
| PNG, WebP, GIF | Not zoomable — their metadata is not scrubbed |
|
||||
| Edited in Immich (crop, rotate) | Immich's edited full-size rendition |
|
||||
| Video | Not zoomable |
|
||||
|
||||
Location is removed the same way as for [originals download](#originals-download):
|
||||
GPS coordinates and place names below city level are overwritten in place;
|
||||
camera data and colour profile stay. A file whose metadata cannot be read is
|
||||
refused rather than sent.
|
||||
|
||||
Formats a browser cannot display need Immich's **full-size preview**
|
||||
(_Administration › Settings › Image Settings › Full-size image_, JPEG format).
|
||||
Immich generates it only while that setting is on, so turn it on and run the
|
||||
_Generate Thumbnails_ job for existing photos. Without it, those photos show
|
||||
the zoom button but answer "Full resolution is not available for this photo",
|
||||
and the viewer stays on the preview. Immich does not say whether a rendition
|
||||
exists until it is asked, so Folio cannot hide the button in advance.
|
||||
|
||||
AVIF originals tagged only with a BT.2020 colour tag (typical of Lightroom AVIF
|
||||
exports) can look more saturated at 1:1 than in the preview: Immich's preview
|
||||
ignores that tag and comes out desaturated (#827), while the browser shows the
|
||||
original as tagged. The original is the accurate one.
|
||||
|
||||
A photo edited in Immich's own editor zooms into the edit, never into its
|
||||
original: Immich keeps the edit apart from the file, so the original is the
|
||||
unedited photo and whatever a crop removed would come back at 1:1. Immich has
|
||||
a full-resolution rendition of every edit, whether or not full-size previews
|
||||
are switched on, so edited photos do not depend on that setting.
|
||||
|
||||
On touch devices, photos above **50 megapixels** get no zoom control. Showing a
|
||||
photo at 1:1 means decoding all of it — 200 MB of memory at 50 MP, 240 MB for a
|
||||
60 MP frame — which phones and tablets do not reliably survive. With a mouse or
|
||||
trackpad there is no limit.
|
||||
|
||||
Zoom is offered on album pages, including the album pages under a photo-essay
|
||||
subpage (`/<page>/<album>`). The essay itself, journal entries and client
|
||||
proofing links do not have it.
|
||||
|
||||
The zoom route re-checks the album allowlist, every password gate on a route to
|
||||
the album, the zoom setting for that route, and that the photo is published and
|
||||
belongs to the album. Answers are `Cache-Control: private` (browser only, one
|
||||
hour, never a CDN), and each visitor can open 20 full-resolution files a minute.
|
||||
|
||||
**Bandwidth.** Every zoom sends a full-resolution file from Immich through
|
||||
Folio to the visitor — commonly 5–25 MB per photo (a 60 MP JPEG is about
|
||||
20 MB). At the rate limit of 20 a minute, one visitor can pull several hundred
|
||||
megabytes a minute, and none of it is cached at a CDN. That is why zoom is off
|
||||
by default; on a metered or slow uplink, switch it on only for the pages that
|
||||
need it.
|
||||
|
||||
## Image Protection & Watermark
|
||||
|
||||
```yaml
|
||||
@@ -915,6 +1034,7 @@ The endpoints where a high limit would be the wrong default are fixed and not co
|
||||
| `POST /api/admin/auth` | 5 |
|
||||
| `POST /api/auth` | 10 |
|
||||
| `/api/install` | 10 |
|
||||
| `/api/zoom` | 20 |
|
||||
| `/api/og` | 30 |
|
||||
| `/api/install/albums` | 30 |
|
||||
| `/api/webhook` | 60 |
|
||||
|
||||
@@ -24,6 +24,11 @@ you get `studio-modern`, the default.
|
||||
| **classic** | Gilded gallery, warm luxury | 🟡 `#c49a3c` | Cinzel + Crimson Pro | Minimal | Passepartout | ✗ |
|
||||
| **noir** | Darkroom analog, film noir | 🟠 `#ff6b35` | Libre Baskerville + Source Sans 3 | Fullbleed | Passepartout | ✓ |
|
||||
| **monograph** | Typographic, book-like | ⬛ `#333333` | Instrument Serif + Inter | Typographic | None | ✗ |
|
||||
| **kunsthalle** | Grey gallery wall, wall labels | 🔵 `#1d545c` | Libre Franklin | Mosaic | Shadow | ✗ |
|
||||
| **ma** | Japanese restraint, washi/sumi | 🔴 `#b0381f` | Cormorant + Hanken Grotesk | Split | None | ✗ |
|
||||
| **cyanotype** | Prussian blue, drawing sheet | 🔵 `#1a4d88` | Jost + DM Mono | Split | None | ✗ |
|
||||
| **salon** | Oxblood salon, fine fillet | 🟤 `#7d1f2c` | Gloock + Work Sans | Fullbleed | None | ✗ |
|
||||
| **birch** | Nordic daylight, soft corners | 🟢 `#2c5d44` | Albert Sans | Stacked | None | ✗ |
|
||||
|
||||
**`studio-modern` is the default** if no theme is specified.
|
||||
|
||||
@@ -82,6 +87,16 @@ their accent, fonts and framing in both modes.
|
||||
|
||||
**Monograph** — Type-first book design with no hero image. Features an 8rem serif title, numbered photo indices via CSS counters, slide-up EXIF captions, hairline dividers, and generous whitespace. Feels like an artist monograph.
|
||||
|
||||
**Kunsthalle** — Photographs hung on a neutral grey gallery wall, the surround ISO 3664 viewing booths use because white pushes photos darker and black pushes them lighter: lighter than every dark preset, darker than every light one. Libre Franklin in all three slots like museum signage, sentence-case titles, a wall label under every photo (its number in the album, “No. 07”, then the photo's caption when `exif.caption` publishes one, then camera · lens), a tight hung shadow and 56px of wall between works. With `photoFrame: passepartout` the mat is a grey board toned to the wall, and the camera line stays printed on the mat. The mosaic hero is a wall of works with the exhibition title below it; petrol is kept for wayfinding (links and hover). Suits fine-art series, documentary and black and white. Pairs best with `layout: masonry`.
|
||||
|
||||
**Ma** — Japanese restraint: negative space as the material. Warm washi-grey paper in light mode (not cream), sumi ink in dark, and a single vermilion accent shaped as a square hanko seal in the header, before the active link and the hero kicker. Cormorant Garamond 300 titles stand in for a Mincho, Hanken Grotesk 300 carries the text; the hero subtitle runs vertically (tategaki) on desktop, the split hero sits asymmetric with the photo inset in the paper, and album pages are offset to the right with 64px of air between photos. No EXIF in the grid, no hover transforms, slow fades. Suits landscape, film and black and white. Pairs best with `layout: masonry` and `columns: 2`; the washi look needs `mode: light`.
|
||||
|
||||
**Cyanotype** — A Prussian-blue print: ink-blue night in dark mode, and in light mode every piece of text in Prussian ink on cool paper instead of black. Set like an architectural drawing sheet: Jost (geometric, Bauhaus line) for type, DM Mono 300 for EXIF and numbers, a ruled title block for the hero EXIF chip and the album metadata, registration marks on the photo corners on hover, and hero links and album covers indexed as sheets (A-01, A-02). Suits architecture, urban and night work, blue hour and interiors. Pairs best with `layout: uniform` and `aspectRatio: "3/2"`.
|
||||
|
||||
**Salon** — The oxblood picture gallery of a 19th-century museum, done quietly: velvet-dark and plaster-rose walls, Gloock titles (never italic), Work Sans text, and a fine fillet line floating 6px outside every photo that brightens to the accent on hover. With `photoFrame: passepartout` the mat is an oxblood board in dark mode and plaster rose in light mode. The fullbleed hero uses a scrim tinted from the wall colour instead of black, with the title bottom left. A tinted surround changes how colour is judged: right for portrait, wedding and fashion work, wrong for colour-critical landscape work. Pairs best with `layout: showcase`.
|
||||
|
||||
**Birch** — Scandinavian daylight: birch-white and pine-night over green-tinted greys, with pine and lichen as the only accent. Albert Sans throughout and no uppercase or letter-spacing anywhere in the interface, where every other preset uses tracked caps. Soft 6px corners on photos and covers, EXIF as a pill chip on hover, a stacked hero with the photo inset and sentence-case strip labels. Suits landscape, outdoor, hiking, travel and family work. Pairs best with `layout: justified` (experimental) or `masonry`.
|
||||
|
||||
## Custom Theme
|
||||
|
||||
Start from any preset and override individual properties:
|
||||
@@ -242,6 +257,8 @@ The album-cover grid on a subpage is sized by two CSS custom properties on
|
||||
The column count follows the configuration, the spacing belongs to the look.
|
||||
`monograph` sets 1px, `minimal` and `editorial` 2px, `studio-modern` 20px;
|
||||
`classic` and `noir` set nothing and get the 4px default from `globals.css`.
|
||||
Of the later presets, `kunsthalle` sets 40px, `ma` 48px, `cyanotype` 24px,
|
||||
`salon` 32px and `birch` 16px.
|
||||
A custom preset does the same:
|
||||
|
||||
```css
|
||||
|
||||
Reference in New Issue
Block a user