From 5f831f719ae3de4ced6fa959918002e3fb11acc4 Mon Sep 17 00:00:00 2001 From: Ralksta <16941050+ralksta@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:14:23 +0200 Subject: [PATCH] release: v0.20.0 --- .dockerignore | 11 + .github/workflows/ci.yml | 4 +- .gitignore | 3 + CHANGELOG.md | 283 ++++ Dockerfile | 32 +- README.md | 27 +- app/{ => (home)}/loading.tsx | 8 +- app/{ => (home)}/page.tsx | 2 +- app/[...path]/EssayView.tsx | 19 +- app/[...path]/PathSkeleton.tsx | 28 + app/[...path]/PhotoGrid.tsx | 113 +- app/[...path]/SubpageGridView.tsx | 6 +- .../__tests__/not-found-status.test.ts | 181 +++ app/[...path]/__tests__/path-depth.test.ts | 14 +- .../__tests__/photo-deeplink-invalid.test.tsx | 73 + .../__tests__/photo-wall-label.test.tsx | 126 ++ .../__tests__/photoItems-edited.test.ts | 65 + .../__tests__/subpage-metadata.test.ts | 123 ++ app/[...path]/contentPage.tsx | 12 +- app/[...path]/essayPayload.ts | 4 +- app/[...path]/loading.tsx | 22 - app/[...path]/page.tsx | 119 +- app/[...path]/photoItems.ts | 19 +- app/[...path]/routeExists.ts | 70 + app/__tests__/gate-page.test.tsx | 72 + app/__tests__/not-found-metadata.test.ts | 29 + app/about/__tests__/about-meta.test.ts | 68 + app/about/page.tsx | 16 +- app/admin/__tests__/SortableTiles.test.tsx | 61 + .../album-entries-round-trip.test.ts | 12 + app/admin/__tests__/gallery-ops.test.ts | 2 +- app/admin/__tests__/gallery-yaml.test.ts | 13 + app/admin/__tests__/sameDraft.test.ts | 35 + app/admin/components/AnalyticsView.tsx | 9 +- app/admin/components/DiagnosticsView.tsx | 12 +- app/admin/components/PageBuilder.tsx | 35 +- app/admin/components/SettingsEditor.tsx | 19 +- .../__tests__/inflight-save-edits.test.tsx | 4 +- .../__tests__/inline-markdown-fields.test.tsx | 20 + .../__tests__/zoom-settings.test.tsx | 67 + app/admin/components/dndAnnouncements.ts | 28 + .../components/fields/ZoomOverrideField.tsx | 46 + app/admin/components/journal/BlockFields.tsx | 25 +- .../components/journal/JournalEditor.tsx | 19 +- .../components/journal/JournalPreview.tsx | 13 +- .../components/journal/JournalStudio.tsx | 9 +- .../components/journal/journal-studio.css | 5 +- .../components/page-builder/AlbumCard.tsx | 11 +- .../components/page-builder/AlbumDrawer.tsx | 9 + .../components/page-builder/SortableTiles.tsx | 40 +- .../components/page-builder/SubpageDrawer.tsx | 24 +- .../components/page-builder/albumEntries.ts | 7 +- .../components/page-builder/galleryOps.ts | 2 +- .../components/page-builder/galleryYaml.ts | 4 + app/admin/components/page-builder/types.ts | 10 + app/admin/components/sameDraft.ts | 31 + .../components/settings/FooterSection.tsx | 27 +- .../components/settings/GeneralSection.tsx | 62 +- .../components/settings/LegalSection.tsx | 11 +- .../components/settings/ThemeSection.tsx | 129 +- app/admin/components/settings/types.ts | 2 + app/admin/styles/analytics.css | 53 +- app/admin/styles/buttons.css | 7 + app/admin/styles/diagnostics.css | 4 +- app/admin/styles/favicon.css | 55 +- app/admin/styles/messages.css | 2 +- app/admin/styles/redesign.css | 36 +- app/admin/styles/settings-editor.css | 3 + app/admin/styles/shell.css | 2 +- app/admin/styles/theme-cards.css | 65 +- app/api/__tests__/site-gate.test.ts | 143 +- app/api/admin/__tests__/about-backup.test.ts | 27 +- app/api/admin/__tests__/doctor-zoom.test.ts | 102 ++ app/api/admin/about/route.ts | 12 +- app/api/admin/doctor/route.ts | 85 ++ app/api/admin/privacy/route.ts | 20 +- app/api/admin/settings/route.ts | 3 + app/api/admin/thumbnail/[id]/route.ts | 5 +- .../analytics/__tests__/track-route.test.ts | 2 + app/api/analytics/track/route.ts | 8 + app/api/contact/route.ts | 8 +- .../[album]/[id]/__tests__/route.test.ts | 185 +++ app/api/download/[album]/[id]/route.ts | 55 +- .../[album]/archive/__tests__/route.test.ts | 108 ++ app/api/image/[id]/route.ts | 21 +- app/api/image/__tests__/image-route.test.ts | 74 +- app/api/install/route.ts | 12 +- app/api/map/route.ts | 5 +- .../zoom/[album]/[id]/__tests__/route.test.ts | 279 ++++ app/api/zoom/[album]/[id]/route.ts | 181 +++ app/contact/contact.css | 10 +- app/gate/page.tsx | 27 +- app/globals.css | 179 ++- app/install/InstallWizard.tsx | 19 +- app/journal/[slug]/page.tsx | 20 +- .../__tests__/journal-meta-draft.test.tsx | 94 ++ app/journal/journal.css | 10 +- app/journal/page.tsx | 28 +- app/layout.tsx | 24 +- app/leaflet.css | 2 +- app/map/page.tsx | 18 +- app/not-found.tsx | 15 + app/themes/birch.css | 195 +++ app/themes/classic.css | 27 +- app/themes/cyanotype.css | 278 ++++ app/themes/editorial.css | 4 +- app/themes/kunsthalle.css | 370 +++++ app/themes/ma.css | 243 ++++ app/themes/minimal.css | 21 + app/themes/monograph.css | 4 - app/themes/noir.css | 9 +- app/themes/salon.css | 246 ++++ app/themes/studio-modern.css | 38 +- app/tokens.css | 255 +++- components/DevToolbar.tsx | 28 +- components/ExifText.tsx | 26 + components/FadeIn.tsx | 28 +- components/HeroCarousel.tsx | 3 +- components/LeafletMap.tsx | 13 + components/Lightbox.module.css | 137 +- components/Lightbox.tsx | 175 ++- components/MapView.tsx | 2 + components/PasswordGate.module.css | 28 +- components/PasswordGate.tsx | 22 +- components/ProofSessionControls.tsx | 4 +- components/ThemeToggle.tsx | 63 +- .../__tests__/LeafletMapLabels.test.tsx | 28 + components/__tests__/LightboxZoom.test.tsx | 259 ++++ components/__tests__/ThemeToggle.test.tsx | 97 ++ components/__tests__/colorTokens.test.ts | 15 +- components/__tests__/useSwipe.test.tsx | 74 + content/gallery.yaml.example | 6 + content/settings.yaml.example | 11 +- docs/README.md | 2 +- docs/deployment.md | 65 +- docs/gallery-config.md | 132 +- docs/theming.md | 17 + hooks/useSwipe.ts | 47 +- hooks/useZoom.ts | 484 +++++++ lib/__tests__/accent-text.test.ts | 148 ++ lib/__tests__/auth-zoom.test.ts | 139 ++ lib/__tests__/docker-content.test.ts | 121 ++ lib/__tests__/doctor.test.ts | 80 ++ lib/__tests__/downloadName.test.ts | 26 +- lib/__tests__/exif-lens.test.ts | 69 + lib/__tests__/fixtures/location.ts | 492 +++++++ lib/__tests__/immich.test.ts | 78 + lib/__tests__/journal-backup.test.ts | 33 + lib/__tests__/journal-text-escape.test.ts | 58 + lib/__tests__/journalDate.test.ts | 21 +- lib/__tests__/justified-row.test.ts | 93 ++ lib/__tests__/lightboxShortcuts.test.ts | 7 + lib/__tests__/locationScrub.test.ts | 612 ++++++++ lib/__tests__/mapService.test.ts | 59 +- lib/__tests__/pages-service.test.ts | 24 + lib/__tests__/passepartout-mat.test.ts | 100 ++ lib/__tests__/preset-registry.test.ts | 173 +++ lib/__tests__/privacy.test.ts | 59 +- lib/__tests__/proxy.test.ts | 58 +- lib/__tests__/settingValues.test.ts | 47 +- lib/__tests__/themeScript.test.ts | 86 ++ lib/__tests__/urls.test.ts | 107 ++ lib/__tests__/yaml-write.test.ts | 29 +- lib/__tests__/zipArchive.test.ts | 53 + lib/__tests__/zoom-config.test.ts | 164 +++ lib/__tests__/zoom.test.ts | 203 +++ lib/__tests__/zoomSource.test.ts | 143 ++ lib/admin/contentVersion.ts | 13 + lib/admin/doctor.ts | 143 +- lib/admin/journal-service.ts | 72 +- lib/admin/pages-service.ts | 59 +- lib/admin/proofing-view.ts | 2 +- lib/admin/yaml-service.ts | 53 +- lib/auth.ts | 37 +- lib/config/index.ts | 47 +- lib/config/presets.ts | 264 ++++ lib/config/schema.ts | 56 + lib/config/settingValues.ts | 86 +- lib/config/settingsSchema.ts | 8 +- lib/config/theme.ts | 128 +- lib/downloadName.ts | 31 + lib/exif.ts | 28 + lib/i18n/locales/de.ts | 10 +- lib/i18n/locales/en.ts | 12 +- lib/i18n/locales/es.ts | 10 +- lib/i18n/locales/fr.ts | 10 +- lib/i18n/locales/it.ts | 10 +- lib/i18n/locales/nl.ts | 10 +- lib/immich.ts | 113 +- lib/journal.ts | 30 +- lib/journalDate.ts | 23 + lib/justifiedRow.ts | 36 + lib/lightboxShortcuts.ts | 27 +- lib/locationScrub.ts | 1250 +++++++++++++++++ lib/mapService.ts | 54 +- lib/privacy.ts | 34 +- lib/themeScript.ts | 20 + lib/urls.ts | 78 +- lib/zipArchive.ts | 108 +- lib/zoom.ts | 234 +++ lib/zoomSource.ts | 108 ++ package-lock.json | 4 +- package.json | 2 +- proxy.ts | 30 +- scripts/__tests__/doctor-cli.test.ts | 43 + scripts/__tests__/doctor-image.test.ts | 186 +++ scripts/contrast-audit.mjs | 18 +- scripts/doctor.mts | 22 +- scripts/screenshots.ts | 3 +- 209 files changed, 13807 insertions(+), 803 deletions(-) rename app/{ => (home)}/loading.tsx (51%) rename app/{ => (home)}/page.tsx (99%) create mode 100644 app/[...path]/PathSkeleton.tsx create mode 100644 app/[...path]/__tests__/not-found-status.test.ts create mode 100644 app/[...path]/__tests__/photo-deeplink-invalid.test.tsx create mode 100644 app/[...path]/__tests__/photo-wall-label.test.tsx create mode 100644 app/[...path]/__tests__/photoItems-edited.test.ts create mode 100644 app/[...path]/__tests__/subpage-metadata.test.ts delete mode 100644 app/[...path]/loading.tsx create mode 100644 app/[...path]/routeExists.ts create mode 100644 app/__tests__/gate-page.test.tsx create mode 100644 app/__tests__/not-found-metadata.test.ts create mode 100644 app/about/__tests__/about-meta.test.ts create mode 100644 app/admin/__tests__/SortableTiles.test.tsx create mode 100644 app/admin/__tests__/sameDraft.test.ts create mode 100644 app/admin/components/__tests__/zoom-settings.test.tsx create mode 100644 app/admin/components/dndAnnouncements.ts create mode 100644 app/admin/components/fields/ZoomOverrideField.tsx create mode 100644 app/admin/components/sameDraft.ts create mode 100644 app/api/admin/__tests__/doctor-zoom.test.ts create mode 100644 app/api/download/[album]/[id]/__tests__/route.test.ts create mode 100644 app/api/zoom/[album]/[id]/__tests__/route.test.ts create mode 100644 app/api/zoom/[album]/[id]/route.ts create mode 100644 app/journal/__tests__/journal-meta-draft.test.tsx create mode 100644 app/themes/birch.css create mode 100644 app/themes/cyanotype.css create mode 100644 app/themes/kunsthalle.css create mode 100644 app/themes/ma.css create mode 100644 app/themes/salon.css create mode 100644 components/ExifText.tsx create mode 100644 components/__tests__/LightboxZoom.test.tsx create mode 100644 components/__tests__/ThemeToggle.test.tsx create mode 100644 components/__tests__/useSwipe.test.tsx create mode 100644 hooks/useZoom.ts create mode 100644 lib/__tests__/accent-text.test.ts create mode 100644 lib/__tests__/auth-zoom.test.ts create mode 100644 lib/__tests__/docker-content.test.ts create mode 100644 lib/__tests__/exif-lens.test.ts create mode 100644 lib/__tests__/fixtures/location.ts create mode 100644 lib/__tests__/journal-text-escape.test.ts create mode 100644 lib/__tests__/justified-row.test.ts create mode 100644 lib/__tests__/locationScrub.test.ts create mode 100644 lib/__tests__/passepartout-mat.test.ts create mode 100644 lib/__tests__/preset-registry.test.ts create mode 100644 lib/__tests__/themeScript.test.ts create mode 100644 lib/__tests__/zoom-config.test.ts create mode 100644 lib/__tests__/zoom.test.ts create mode 100644 lib/__tests__/zoomSource.test.ts create mode 100644 lib/config/presets.ts create mode 100644 lib/justifiedRow.ts create mode 100644 lib/locationScrub.ts create mode 100644 lib/themeScript.ts create mode 100644 lib/zoom.ts create mode 100644 lib/zoomSource.ts create mode 100644 scripts/__tests__/doctor-image.test.ts 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 `