diff --git a/.dockerignore b/.dockerignore index ac00749..26604c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,3 +30,14 @@ eslint.config.mjs # OS metadata .DS_Store + +# Site content — gallery.yaml, settings.yaml, pages, journal entries, +# credentials (install.json, .setup-token), contact messages, proofing links, +# analytics and backups. The container mounts content/ as a volume, so none of +# it belongs in the build context: a local build from a checkout with a live +# content/ would otherwise bake all of it into the image layers, where the +# mounted volume hides it but `docker save` or a push hands it out. Only the +# tracked templates are let through; the Dockerfile copies them by name. +content +!content/*.example +!content/journal/*.example diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28339c..922ab52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v7 with: - node-version: '20' + node-version: '22' cache: 'npm' - name: Install dependencies @@ -115,7 +115,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v7 with: - node-version: '20' + node-version: '22' cache: 'npm' - name: Install dependencies diff --git a/.gitignore b/.gitignore index 142eb55..702f9f6 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,9 @@ content/.setup-token content/messages/ # Google Fonts cached by /api/fonts (lib/fonts.ts) content/.fonts/ +# Content pages (lib/admin/pages-service.ts) and the uploaded favicon +content/pages/ +content/favicon.svg # ── Internal Dev Docs ──────────────────────────────── docs/dev/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab7433..55d000f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,289 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Releases up to and including v0.9.2 are documented in the [GitHub releases](https://github.com/ralksta/immich-folio/releases). +## [0.20.0] — 2026-10-02 + +### Security + +- **Original downloads no longer carry the photo's location** + ([#828](https://github.com/ralksta/immich-folio/pull/828), + [GHSA-g4xf-4285-4cv4](https://github.com/ralksta/immich-folio/security/advisories/GHSA-g4xf-4285-4cv4), + low). The rest of the site never shows more than city and country, but the + single download, the album ZIP and the proofing ZIP handed out originals with + their GPS coordinates. JPEG, HEIC/HEIF and AVIF originals are now edited in + place as they stream: the GPS block in EXIF, every GPS or latitude/longitude + property in XMP, and place names below city level (XMP `Location`, + `Sublocation` and similar, IPTC 2:92) are overwritten. Camera, lens, + exposure, dates, copyright and IPTC, the ICC profile and the image data stay + exactly as they were, and so does the file length; nothing is re-encoded. + Extended XMP, C2PA manifests and unknown vendor segments, which can hold + their own location records, are blanked whole, and a file whose metadata + cannot be read safely is refused (`404`, or left out of the ZIP and logged) + rather than sent. RAW/DNG, PNG, TIFF, WebP and video are delivered as Immich + stores them, GPS included; a new doctor check, `download-metadata`, names the + download albums that contain such files and how many of them Immich has a + location for. + +- **Locally built Docker images no longer contain the checkout's `content/`** + ([#805](https://github.com/ralksta/immich-folio/pull/805)). A `docker build` + from a live checkout copied `content/` into the image three ways, so its + layers held `install.json` (Immich API key, site secret, admin password + hash), `.setup-token`, contact messages, proofing links, journal entries and + every backup. The running container hid them behind the mounted volume, but + `docker save` or a push to a registry handed them out. `.dockerignore` now + excludes `content/`, the builder drops the copy the standalone trace makes, + and the image ships only the `*.example` templates. The GHCR images are + built from a clean clone and were never affected. See the upgrade notes. + +- **View statistics are gated on a password-locked site** + ([#810](https://github.com/ralksta/immich-folio/pull/810)). + `POST /api/analytics/track` accepted hits while the site password was set, + so anyone could write paths of their choosing into `content/analytics.json`. + It now answers like every other gated route. Visitors who unlocked the site + are counted as before. + +### Added + +- **1:1 zoom in the lightbox** + ([#830](https://github.com/ralksta/immich-folio/pull/830), closes + [#467](https://github.com/ralksta/immich-folio/issues/467); requested in + [#587](https://github.com/ralksta/immich-folio/issues/587) by + [@ImScheinox](https://github.com/ImScheinox)). Visitors can zoom a photo to + one image pixel per screen pixel to judge sharpness: double-click or + double-tap, the magnifier button, pinch, Ctrl + scroll wheel, or `+`, `-` + and `0`. Zoom stops at 1:1, dragging pans, `Esc` returns to fit before it + closes the viewer, and swiping to the next photo is off while zoomed. The + full-resolution file is fetched on the first zoom gesture only, never on + opening the viewer or for neighbouring photos. JPEG and AVIF zoom into the + original, HEIC, RAW and other formats a browser cannot show into Immich's + full-size rendition, both with the location removed as for downloads; PNG, + WebP, GIF and video are not zoomable. On touch devices photos above 50 + megapixels get no zoom control, since decoding them takes 200 MB or more. + Zoom is off by default: `zoom: true` in `settings.yaml` (Settings → General + → _Photo zoom_) turns it on, and a subpage or album can override it either + way (album → subpage → site; _Inherit / On / Off_ in the page builder). It + is offered on album pages, not in essays, journal entries or proofing links. + `GET /api/zoom/…` re-checks the allowlist, every password gate and the zoom + setting of the route, is limited to 20 files a minute per visitor and is + never cached by a CDN. A new doctor check, `zoom-renditions`, warns when + Immich has no full-size previews for zoomable photos. See + `docs/gallery-config.md#photo-zoom` and the upgrade notes. + +- **Five new theme presets: Kunsthalle, Ma, Cyanotype, Salon and Birch** + ([#816](https://github.com/ralksta/immich-folio/pull/816), + [#824](https://github.com/ralksta/immich-folio/pull/824)). _Kunsthalle_ hangs + photos on a neutral grey gallery wall with a numbered wall label under each + work (album number, the Immich description where captions are published, + and camera · lens). _Ma_ is washi paper, sumi ink and one vermilion seal, + with wide negative space and a vertical hero subtitle. _Cyanotype_ sets the + site like a Prussian-blue drawing sheet with registration marks and ruled + title blocks. _Salon_ is an oxblood picture gallery with a fine fillet round + each work. _Birch_ is Scandinavian daylight in green-tinted greys, with no + uppercase anywhere. Each has a dark and a light palette whose text reaches + 4.5:1 on every surface, including the accent used as text. Under + `photoFrame: passepartout`, Kunsthalle and Salon get mats toned to their + wall instead of black or white. Recommended grid layouts per preset are in + `docs/theming.md`. Presets are now declared once in + `lib/config/presets.ts`, and a test checks every place that cannot import + it. + +- **Photos edited in Immich are shown as edited** + ([#832](https://github.com/ralksta/immich-folio/pull/832), closes + [#831](https://github.com/ralksta/immich-folio/issues/831)). Crops and + rotations made in Immich's editor were ignored everywhere: Immich keeps an + edit apart from the file and applies it only when asked. Folio now asks for + the edited rendition in the grid, the lightbox, the zoom, share images, the + map and downloads, and lays the grid out with the edited proportions. An + edited photo is downloaded as Immich renders the edit, a full-resolution + JPEG without EXIF, named `.jpg`. Only edited photos get a new image URL; see + the upgrade notes and `docs/deployment.md#edits-made-in-immich`. + +### Changed + +- **The Impressum, the privacy page and the contact form are reachable on a + password-locked site** + ([#819](https://github.com/ralksta/immich-folio/pull/819)). The site + password rewrote every page to the gate, but a German Impressum has to be + reachable directly. `/impressum`, `/privacy` and `/contact` now pass the + gate, render without header, navigation or footer, and contain no album + names or asset tokens; `POST /api/contact` accepts messages with its rate + limit, honeypot and validation unchanged. The gate page links the Impressum + and the privacy page when they exist. A page that is switched off still + answers `404`, and every other path stays gated. + +- **Unknown pages answer a real 404** + ([#812](https://github.com/ralksta/immich-folio/pull/812)). An unknown slug, + a subpage with `enabled: false`, a draft content page and an unknown album + under a subpage rendered the not-found page with status `200`, because the + loading skeleton was streamed before the page could decide. The decision is + now made first, with no extra Immich request, and the skeleton still covers + the slow album load. A locked subpage shows its gate for any album slug below + it, so a 404 does not reveal which albums sit behind the password. Every 404 + has the translated not-found title and a single `noindex`. Hidden subpages + and their albums are now `noindex`, and a subpage with several albums gets + its own description. On a client-side navigation to a subpage or album, the + skeleton now appears after one server round trip instead of at once. + +- **Grid photos show their passepartout mat** + ([#814](https://github.com/ralksta/immich-folio/pull/814)). + `photoFrame: passepartout`, the default frame of Studio, Classic and Noir, + never showed its mat: the photo covered it, and the EXIF line meant for the + mat sat on top of the photo. The photo now sits inside the mat, uncropped in + masonry, with the EXIF line printed in the band below it. Justified rows stay + aligned, and the row spacing is no longer doubled in the uniform, showcase, + filmstrip, editorial-flow and justified layouts. Mat colours are tokens per + mode and reach 4.5:1 for the caption in every preset. The `shadow` and + `none` frames are pixel-identical to before. + +- **Text and controls reach WCAG contrast** + ([#804](https://github.com/ralksta/immich-folio/pull/804), + [#818](https://github.com/ralksta/immich-folio/pull/818)). The accent is a + fill colour, and as text it fell below 4.5:1 on dark pages (Studio Modern's + red at 3.66:1). Accent-coloured text now uses a variant lightened or + darkened just until it passes on the mode's surfaces; an accent that already + passes, your own included, is unchanged, and fills and underlines keep the + accent itself. Form inputs have visible borders, every control shows the + same focus ring, the Minimal hero navigation is readable over the photo, the + journal draft badge reaches 4.5:1 in light mode, and scrollbars and form + controls follow the colour mode. Studio Modern's light muted text moves from + `#73736e` to `#6d6d68`. + +- **Settings the site would bend are refused on save** + ([#803](https://github.com/ralksta/immich-folio/pull/803)). Like the site + URL, accent and grid values in 0.19.0, `contact.retentionDays` outside + 1–365, `theme.radius` outside 0–64 and header links without a label or an + http(s) URL are now marked in the form and refused with `400`, instead of + being clamped or dropped silently when the site renders. `npm run doctor` + now runs the `settings-values` check, which it skipped before. + +- **The privacy facts cover proofing links** + ([#826](https://github.com/ralksta/immich-folio/pull/826)). The facts next to + the privacy editor said proofing selections are never sent to the server, + which has not been true since proofing links. A new _Proofing links_ fact + lists what a link stores in `content/proofing.json` and for how long, and + names the webhook host as a third party when `PROOFING_WEBHOOK_URL` is set. + +- **The admin has one content width** + ([#820](https://github.com/ralksta/immich-folio/pull/820)). Pages, Overview, + Diagnostics, Analytics and Messages ended at different edges; they now share + one 1280px column. The analytics headings and numbers use the admin font and + fit a phone, the favicon upload is a styled, keyboard-reachable button with a + preview, and the _Visual Blocks / Raw Markdown_ and _Desktop / Mobile_ + toggles show which mode is active. + +- **How colour profiles reach visitors is documented** + ([#823](https://github.com/ralksta/immich-folio/pull/823), closes + [#468](https://github.com/ralksta/immich-folio/issues/468)). Folio streams + Immich's renditions unchanged, and sRGB and Display P3 photos arrive + correctly. AVIF originals tagged only with a BT.2020 colour tag, typical of + Lightroom AVIF exports, are shown 13–37% less saturated, because Immich + ignores that tag when it builds its renditions; this is tracked in + [#827](https://github.com/ralksta/immich-folio/issues/827). See + `docs/deployment.md#colour-profiles`. + +### Fixed + +- **The colour mode is right on the first paint** + ([#808](https://github.com/ralksta/immich-folio/pull/808)). A visitor who + chose light mode, or a light OS with `mode: auto`, saw a dark page until the + scripts loaded. A small inline script now applies the stored choice or the + OS setting before the first paint, on every page including the password gate + and the admin, and `mode: auto` follows an OS switch while the page is open. + +- **Admin backups and unsaved-changes markers** + ([#807](https://github.com/ralksta/immich-folio/pull/807)). Renaming a page + or journal entry listed it in Backups as a deleted entry, and restoring that + brought the old slug back; the history now follows the renamed file. A save + that changes nothing no longer takes a backup and pushes out a real one. + _Unsaved changes_ in the settings and the page builder clears when an edit is + reverted. A journal text paragraph whose line starts with `![` is escaped, + so it is no longer turned into a broken photo block on the next load. + +- **The admin for keyboards and screen readers** + ([#813](https://github.com/ralksta/immich-folio/pull/813), + [#817](https://github.com/ralksta/immich-folio/pull/817)). Drag handles in + the page builder carry the sortable role and a name (_Reorder Pricing_), so + rows no longer nest a button inside a button, and drag announcements name + pages, albums and hero photos instead of internal IDs. The journal's + _+ Pick_ slot is a button. The site title field shows what an empty title + really falls back to. Theme card descriptions match the presets, the + _Typographic_ hero card no longer shows photos the hero never renders, and + _Cover_ has a preview. + +- **Map, proofing and lightbox details** + ([#811](https://github.com/ralksta/immich-folio/pull/811), + [#810](https://github.com/ralksta/immich-folio/pull/810)). Map markers are + named for screen readers ("Yufu, Japan, 46 photos"), and the map header + counts only the albums the viewer can open. Proofing bars follow the preset's + corner radius, grid hearts use the lightbox's favourite colour, and the + header and proofing bar no longer show through the lightbox. ZIP entries are + dated with the photo's capture time instead of the time the archive was + built. + +- **Share previews, journal and photo links** + ([#809](https://github.com/ralksta/immich-folio/pull/809)). `/about` shared + no image and now uses the portrait; `/journal` previewed as the home page and + now has its own card. A draft entry shows its _Draft_ badge to the admin, and + machine-readable journal dates are marked up as `