Release v0.18.0 — merges `dev` into `main`. ## Thanks - [@lancetm714](https://github.com/lancetm714) for the accent-contrast fix (#680 → #724). - [@RichKidsDev](https://github.com/RichKidsDev) for reporting the outdated Impressum (#697). ### Security - **Journal drafts and password-protected entries no longer render publicly as a subpage essay** ([#684](https://github.com/ralksta/immich-folio/pull/684), [GHSA-987w-cpwm-c9g9](https://github.com/ralksta/immich-folio/security/advisories/GHSA-987w-cpwm-c9g9), medium). A subpage's `essayFile` read the journal entry straight from disk, skipping the draft and password rules `/journal/<slug>` applies. It now resolves through the same rules: a draft reads as missing for visitors, and a protected entry shows its password gate, sharing the `lb_auth_journal_<slug>` cookie. - **Immich descriptions are escaped in generated subpage essays** ([#684](https://github.com/ralksta/immich-folio/pull/684), [GHSA-8ppx-39jv-q5xm](https://github.com/ralksta/immich-folio/security/advisories/GHSA-8ppx-39jv-q5xm), low). The essay built from a subpage's albums (`layout: essay` without markdown) put each asset's Immich description into the page as raw HTML. Captions are now HTML-escaped, and they go through `assetCaption()`, so `exif.caption: false` is respected there as well. Tracked in [#689](https://github.com/ralksta/immich-folio/issues/689). - **Admin drafts no longer keep a typed password, and the contact form's email check runs in linear time** ([#727](https://github.com/ralksta/immich-folio/pull/727)). The admin's session drafts wrote a password as typed to `sessionStorage`; a draft holding a plaintext password is now not kept (the unsaved-changes guard still warns). The email pattern could backtrack polynomially on a crafted domain; its labels now exclude the dot and the length is checked first. Both found by CodeQL before release. ### Added - **A built-in contact form with an inbox in the admin** ([#703](https://github.com/ralksta/immich-folio/pull/703), closes [#702](https://github.com/ralksta/immich-folio/issues/702)). `/contact` needs neither SMTP nor a form service: each message is one file in `content/messages/` (mode `0600`), read under the new _Messages_ tab, where _Reply by email_ opens the owner's mail client with the message quoted. Messages older than `contact.retentionDays` (default 90) are deleted. An optional ntfy push goes to `contact.notifyUrl` or `CONTACT_NOTIFY_URL`, and since [#709](https://github.com/ralksta/immich-folio/pull/709) it carries a fixed "new message" line and nothing about the sender — no name, no address, no text. Spam is kept out without a captcha: an off-screen honeypot, a minimum fill time, three submissions a minute per IP and a cap of 500 stored messages. With the Impressum on and no `legal.contactUrl`, the Impressum links the form as its second contact channel. A new doctor check warns when the form is on but nobody is notified. - **A privacy policy page, next to a list of what the site processes** ([#708](https://github.com/ralksta/immich-folio/pull/708)). `/privacy` renders `content/privacy.md`, edited under Settings → Legal, and the footer links it once the file has text; `privacy.enabled: false` hides it. Folio writes no legal text. Beside the editor it lists what _this_ installation does, read off the configuration — the map, statistics, contact form, password cookies, CDN — and marks every entry that involves a third party. _Insert headings_ adds the matching section headings and nothing else. The doctor warns while the Impressum is on and there is no policy. - **French, Spanish, Italian and Dutch interface translations** ([#681](https://github.com/ralksta/immich-folio/pull/681)), selected with `settings.yaml: lang` as before. The admin panel stays English. - **CDN mode for photos and videos** ([#681](https://github.com/ralksta/immich-folio/pull/681)). With `CDN_URL` set, image and video URLs point at a pull CDN whose origin is this server, so repeat views reach neither the server nor Immich. A site password switches the mode off, since a CDN would hand out cached photos without checking the unlock cookie. The CDN origin is added to the CSP, and a doctor check covers the password and `TRUSTED_PROXY_HOPS` cases. See `docs/deployment.md#cdn-mode`. - **A reworked admin panel** ([#712](https://github.com/ralksta/immich-folio/pull/712), [#713](https://github.com/ralksta/immich-folio/pull/713), [#714](https://github.com/ralksta/immich-folio/pull/714), [#716](https://github.com/ralksta/immich-folio/pull/716), [#717](https://github.com/ralksta/immich-folio/pull/717), [#718](https://github.com/ralksta/immich-folio/pull/718), [#719](https://github.com/ralksta/immich-folio/pull/719), [#721](https://github.com/ralksta/immich-folio/pull/721)): - **A sidebar instead of the header row**, grouped by task, with an unread count on Messages and a dot on Diagnostics while Immich is down. It collapses to an icon rail, with names shown on hover and focus, and remembers that per browser. - **`/admin` is an overview**: what needs attention (unread messages, doctor warnings), published pages, journal entries and recent views. - **The site's own visual language** — the preset's fonts, hairline borders, the square marker — and one page header on every screen. The accent is kept for the current place and the primary action; switches are now pills reading ON or OFF, in green rather than the accent, so Studio Modern's red no longer makes an enabled setting look like an error. - **The page builder is a structure list and a panel.** Hero, subpages and standalone albums sit in one list on the left, subpages drag-sorted in place; the selected entry is edited on the right instead of in an overlay. - **Dialogs only where a change is permanent.** Deleting a message or a journal entry, and resetting the favicon (which used to delete the file without asking), go through one confirm dialog in the admin's own look. Removals in the page builder, which only touch unsaved state, happen at once and offer _Undo_. No `window.confirm()` is left. - **Feature switches are grouped by meaning** as one-line rows under Settings → General; page transitions and scroll-to-top moved to Theme → Look & motion. No setting keys changed. ### Changed - **Fonts and the Leaflet stylesheet are served from the site itself** ([#700](https://github.com/ralksta/immich-folio/pull/700)). Every page used to link `fonts.googleapis.com`, which sends each visitor's IP address to Google. The server now fetches a theme's Google fonts once, rewrites the stylesheet and caches the files in `content/.fonts/`; custom fonts in `theme.fonts` keep working. If Google cannot be reached before a font is cached, the page falls back to the theme's system fonts. `leaflet.css` comes from the npm package instead of unpkg. The CSP drops `fonts.googleapis.com`, `fonts.gstatic.com` and `unpkg.com`. - **Email addresses are kept out of the served HTML** ([#705](https://github.com/ralksta/immich-folio/pull/705)). The Impressum and footer render the address only after hydration, from an encoded form, so harvesters reading raw HTML find nothing. Without JavaScript the Impressum shows `name [at] example [dot] de`. This defeats bulk harvesting, not a person with a browser. - **The Impressum cites § 5 DDG and links its contacts** ([#698](https://github.com/ralksta/immich-folio/pull/698), reported in [#697](https://github.com/ralksta/immich-folio/issues/697) by [@RichKidsDev](https://github.com/RichKidsDev)). The TMG was replaced by the DDG in May 2024; `legal.heading` replaces the line for other laws (e.g. § 5 ECG in Austria). `legal.contactUrl` and `legal.contactLabel` add a contact form as the second channel, so a phone number stays optional. Email, phone and contact URL are now links, and the "Created with the Impressum generator by eRecht24" line, shown on every site whether true or not, is gone. Settings → Legal gains fields for heading, contact URL, VAT ID and tax number, and a doctor check flags missing details. - **Missing pages answer `404`** ([#704](https://github.com/ralksta/immich-folio/pull/704)). Every missing page used to answer `200`, because streaming had started before `notFound()` ran. `/contact`, `/impressum`, `/map` and `/privacy` while switched off, and `/journal/<slug>` without an entry, now return a real 404 with the same not-found page. Album and subpage slugs still depend on Immich and stay soft 404s. - **Passwords set in the admin are stored as scrypt hashes** ([#706](https://github.com/ralksta/immich-folio/pull/706), closes [#690](https://github.com/ralksta/immich-folio/issues/690)). Subpage, album, journal-entry and site passwords were written as typed. The save routes now hash them; a stored hash reads _Protected_ with _Change_ and _Remove_ and is never put into an input. Visitors who unlocked a page stay signed in across unrelated saves. Existing plaintext passwords are hashed the next time their file is saved in the admin. - **One notification surface in the admin** ([#707](https://github.com/ralksta/immich-folio/pull/707)) replaces every `alert()`: errors stay until dismissed, successes clear themselves, and screen readers announce both. ### Fixed - **Map tiles load again** ([#701](https://github.com/ralksta/immich-folio/pull/701)). CARTO started answering every tile with an "API KEY REQUIRED" image, leaving `/map` and journal map blocks as a grey grid. Tiles now come from OpenStreetMap; the dark look is a CSS filter, so the map now follows the colour mode. - **The page builder no longer drops a subpage's `proofing:` flag on save** ([#711](https://github.com/ralksta/immich-folio/pull/711)). A page switched off for proofing by hand reverted after any unrelated edit. - **Unsaved edits in Settings and About survive switching admin tabs, and a backup restore is no longer overwritten by the next save** ([#687](https://github.com/ralksta/immich-folio/pull/687)). Editors that were open during a restore now reload and offer unsaved edits back as a conflict. Also: "Restore my changes" after a conflict found nothing, removing an album left its drawer showing the next album, and the journal saved (rotating a backup) without changes. - **Accent-backed buttons stay readable with a light or malformed accent** ([#724](https://github.com/ralksta/immich-folio/pull/724), supersedes [#680](https://github.com/ralksta/immich-folio/pull/680)) — contributed by [@lancetm714](https://github.com/lancetm714). A white accent made the proofing dialog's _Download selected (.zip)_ button white on white. `theme.accent` is now accepted as hex only and normalised to `#rrggbb`, anything else falls back to the preset, and the proofing dialog and filter pills use the mode-aware `--on-accent` text colour from #688. - **Every preset's accent is visible in both colour modes** ([#688](https://github.com/ralksta/immich-folio/pull/688)). Presets had one accent for both modes; minimal's dark mode drew black buttons on black. Presets now carry a light and a dark accent, and text on accent-filled buttons picks black or white for contrast. A custom accent stays the owner's choice in both modes. - **Visitor-side inconsistencies** ([#685](https://github.com/ralksta/immich-folio/pull/685)): after visiting `/impressum`, every subpage header turned centred and uppercase; back links announced "Back to Back to …"; the hero EXIF chip ignored `exif.location`/`camera`/`settings`; videos in journal entries opened as stills; the setup screen named a non-existent `.env.example`. - **An essay's lightbox walks only the photos the story shows** ([#686](https://github.com/ralksta/immich-folio/pull/686)), not every photo of the subpage's albums, and the proofing bar in essays shows the selection count instead of a filter button that did nothing. - **The page panel scrolls with the mouse wheel** ([#720](https://github.com/ralksta/immich-folio/pull/720)). - **The journal editor's top bar wraps on phones** ([#723](https://github.com/ralksta/immich-folio/pull/723), closes [#675](https://github.com/ralksta/immich-folio/issues/675)), so the title stays editable and Save stays on screen. - **Every admin form field has a name for assistive technology** ([#715](https://github.com/ralksta/immich-folio/pull/715)). Only two inputs had a linked label; 47 are linked now, and clicking a label focuses its field. ### Internal - **`SettingsEditor` and `PageBuilder` are split up** ([#710](https://github.com/ralksta/immich-folio/pull/710), [#711](https://github.com/ralksta/immich-folio/pull/711)). The 2,200-line settings editor is a shell with one file per section; the page builder's YAML handling and edits are pure functions under test. Both were checked side by side against `dev` for identical output. - **`useAdminFetch()`** ([#707](https://github.com/ralksta/immich-folio/pull/707)) gives Analytics, Messages, Backups and the journal list one loading and error pattern. - README brought up to v0.17.0 ([#678](https://github.com/ralksta/immich-folio/pull/678), [#679](https://github.com/ralksta/immich-folio/pull/679)). ### Upgrade notes **`content/` gains two directories the app writes to:** `content/.fonts/` for the cached fonts and `content/messages/` for contact-form messages. Both are created on demand, so a writable `content/` is enough, as before. **The server needs outbound HTTPS to Google once per font** to fill the font cache. Until then pages fall back to the theme's system fonts. Visitors' browsers no longer contact Google. **A custom CSP or reverse-proxy allowlist** must now allow `tile.openstreetmap.org` for images instead of `*.basemaps.cartocdn.com`, and no longer needs `fonts.googleapis.com`, `fonts.gstatic.com` or `unpkg.com`. The built-in CSP is updated. **`theme.accent` must be hex** (`#rgb`, `#rrggbb` or `#rrggbbaa`). Anything else, including a value without `#`, falls back to the preset's accent. **Plaintext passwords in `gallery.yaml`, `settings.yaml` or journal entries** are hashed the next time that file is saved in the admin; one save clears the doctor's warning. **New, optional settings:** `contact.*` (the form is off by default), `privacy.enabled`, `legal.heading`, `legal.contactUrl`, `legal.contactLabel`, and the `CDN_URL` and `CONTACT_NOTIFY_URL` environment variables. See `content/settings.yaml.example` and `.env.local.example`. In the example file, `vatId` and `taxId` had swapped sample values; check yours if you copied them. No `IMAGE_CACHE_VERSION` bump is needed this time. --------- 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>
407 lines
22 KiB
Markdown
407 lines
22 KiB
Markdown
# Immich Folio
|
|
|
|
**Turn your Immich albums into a public photography portfolio — without ever exposing your Immich server to the internet.**
|
|
|
|
<p>
|
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/ralksta/immich-folio" alt="MIT License" /></a>
|
|
<a href="https://github.com/ralksta/immich-folio/releases"><img src="https://img.shields.io/github/v/release/ralksta/immich-folio" alt="Latest release" /></a>
|
|
</p>
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/header.png" width="100%" alt="Immich Folio — the public gallery, an album grid, and the admin page builder" />
|
|
</p>
|
|
|
|
A self-hosted portfolio powered by [Immich](https://immich.app). It acts as a **secure reverse proxy** between your visitors and your private Immich instance: your Immich server stays on your local network, completely invisible to the outside world, while your albums are published as a gallery you control.
|
|
|
|
**Latest: v0.18.0** — built-in contact form, privacy page and a reworked admin panel. → [What's New](#whats-new)
|
|
|
|
## Contents
|
|
|
|
- [Features](#features)
|
|
- [Requirements](#requirements)
|
|
- [Quick Start](#quick-start)
|
|
- [First-Run Setup](#first-run-setup)
|
|
- [Configuration](#configuration)
|
|
- [Docker](#docker)
|
|
- [Admin Panel](#admin-panel)
|
|
- [What's New](#whats-new)
|
|
|
|
**Guides:** [Gallery Configuration](docs/gallery-config.md) · [Theming](docs/theming.md) · [Journal & Photo Essays](docs/journal.md) · [Admin Panel](docs/admin-panel.md) · [Deployment](docs/deployment.md)
|
|
|
|
## Features
|
|
|
|
### Gallery & Layout
|
|
|
|
- **Configurable hero layouts** — split, fullbleed, minimal, stacked (image + thumbnail strip), typographic (text-only), or mosaic (multi-image grid)
|
|
- **Hero image carousel** — single image or crossfade carousel of multiple Immich assets
|
|
- **Masonry photo grid** — responsive layout with natural aspect ratios and configurable columns, gap, and aspect ratio
|
|
- **Uniform grid mode** — switch to a fixed-aspect uniform grid per-subpage or globally
|
|
- **Showcase / filmstrip / editorial-flow layouts** — featured hero + grid, horizontal scroll strips, or alternating full-width and paired images
|
|
- **Justified rows** _(experimental)_ — every row fills the width at one shared height, aspect ratios intact, nothing cropped
|
|
- **Per-subpage grid overrides** — each subpage can define its own columns, gap, aspect ratio, and layout mode, and individual albums can override that again _(experimental)_
|
|
- **Cover focal points** _(experimental)_ — decide which part of a cover survives the crop
|
|
- **Fullscreen lightbox** — keyboard and swipe navigation, EXIF panel, adjacent image preloading
|
|
- **EXIF metadata on hover** — camera body, lens, focal length, aperture, shutter speed, ISO shown directly on the grid
|
|
- **ThumbHash placeholders** — instant blurred previews while full images load
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/grid-masonry.png" width="49%" alt="Masonry grid layout" />
|
|
<img src="docs/screenshots/grid-showcase.png" width="49%" alt="Showcase grid layout" />
|
|
</p>
|
|
<p align="center"><em>Masonry and showcase, both in Studio Modern — the same album, a different grid layout.</em></p>
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/lightbox-exif.png" width="98%" alt="Fullscreen lightbox with the EXIF panel open" />
|
|
</p>
|
|
<p align="center"><em>The fullscreen lightbox with the EXIF panel — camera, lens and exposure for the frame on screen.</em></p>
|
|
|
|
### Content & Organization
|
|
|
|
- **Subpage grouping** — organize albums into named collections (e.g. `/japan/tokyo-2023`)
|
|
- **Auto-generated slugs** — URL slugs derived from album names automatically
|
|
- **YAML gallery config** — all gallery structure defined in a single `content/gallery.yaml` file
|
|
- **Markdown about page** — `content/about.md` with frontmatter for portrait, name, location, and gear list, editable from the admin panel
|
|
- **Journal** — photo essays and travel stories at `/journal`, with drafts, per-entry passwords, cover images and reading times; new entries can start from a template (wedding, hiking, travel, …)
|
|
- **Photo Essay mode** — long-form storytelling pages alternating text with fullbleed, paired and grid image layouts, facts lists, a slice of an album, and a map with the pins you choose — typed places, or photos placed by their GPS
|
|
- **Unlisted subpages** _(experimental)_ — reachable by direct link, absent from the navigation
|
|
- **Subpage on/off toggle** — take a page offline without deleting it
|
|
- **External navigation links** _(experimental)_ — point the header at a shop, a blog, or a social profile
|
|
- **Client proofing** — clients favorite photos and export the selection; picks are encoded in the URL, nothing is stored server-side
|
|
- **Originals delivery** — per-album opt-in lets visitors download a whole album or their proofing selection as a ZIP of the originals
|
|
- **Lightbox watermark** — configurable overlay on fullscreen images
|
|
- **Six interface languages** — English, German, French, Spanish, Italian and Dutch for everything visitors see
|
|
- **Privacy-friendly analytics** — cookieless view counts, no third parties, can be switched off
|
|
- **Dynamic OG images** — auto-generated social preview images per album
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/page-collection.png" width="98%" alt="Collection overview grouping several albums under one subpage" />
|
|
</p>
|
|
<p align="center"><em>A subpage grouping several albums into one collection.</em></p>
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/page-about.png" width="49%" alt="About page rendered from content/about.md" />
|
|
<img src="docs/screenshots/page-map.png" width="49%" alt="Map page clustering photo locations worldwide" />
|
|
</p>
|
|
<p align="center"><em>The Markdown about page and the GPS map, both generated from your Immich data.</em></p>
|
|
|
|
### Admin Panel
|
|
|
|
- **Visual page builder** — drag & drop interface to manage hero images, standalone albums, subpages, and sections
|
|
- **Album picker** — browse all shared Immich albums with search, see photo counts, and add them with one click
|
|
- **Settings editor** — configure theme, grid layout, footer, legal/impressum, SEO, image protection and the about page from a visual UI
|
|
- **Visual previews everywhere** — grid layout, theme, photo frame, hero style and the Google search snippet are picked from preview cards instead of text fields
|
|
- **Journal Studio** — split-screen block editor with a live preview of the real page; start from a template, drag blocks into order
|
|
- **Essay block editor** — assemble photo essays block by block without touching Markdown
|
|
- **Unsaved edits survive** — leave the page builder or the journal editor mid-edit, and your changes are waiting when you come back
|
|
- **Diagnostics** — a page that checks the Immich connection, config and security settings, and links every finding to its fix
|
|
- **Photo order editor** — drag & drop a hand-picked opening sequence for any album
|
|
- **Favicon upload** — give the site its own icon in the browser tab
|
|
- **Backup manager** — every save is backed up automatically; restore any of them with one click
|
|
- **Live YAML sync** — changes are written directly to `gallery.yaml` and `settings.yaml` with automatic backups
|
|
- **Password protected** — secured with its own admin password, separate from album passwords
|
|
|
|
<details>
|
|
<summary><strong>Security & Infrastructure</strong></summary>
|
|
|
|
<br>
|
|
|
|
| Concern | Protection |
|
|
| -------------------------- | ------------------------------------------------------------------------------------ |
|
|
| **Server exposure** | Immich URL never leaves your network — all requests proxy server-side |
|
|
| **API key** | Stored only in `.env.local`, never in client code |
|
|
| **Asset IDs** | Immich UUIDs encrypted (AES-256) into opaque tokens |
|
|
| **Album scope** | Only albums in `gallery.yaml` are accessible |
|
|
| **Password protection** | Per-subpage password support |
|
|
| **Rate limiting** | Per-IP sliding-window rate limiter (configurable RPM) |
|
|
| **Vulnerability scanning** | Docker image scanned with Trivy on every release, results in the GitHub Security tab |
|
|
|
|
- Health check endpoint at `GET /api/health`
|
|
- In-memory caching with configurable TTL
|
|
- Optional CDN mode — photos and videos served through a pull CDN, so repeat views never reach your server ([Deployment Guide](docs/deployment.md#cdn-mode))
|
|
- Standalone Docker image — multi-stage, non-root, ~150 MB
|
|
- Dependencies kept current via Dependabot (npm + GitHub Actions, weekly)
|
|
|
|
</details>
|
|
|
|
## Requirements
|
|
|
|
- **Immich 3.0 or newer.** Immich 3.0 changed how album assets are retrieved; earlier versions are not supported as of v0.9.0. On an older server, albums render with the correct title but no photos, and the map stays empty — the app logs a warning naming this as the likely cause.
|
|
- Node.js 20+ (or just use the Docker image)
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
git clone https://github.com/ralksta/immich-folio.git
|
|
cd immich-folio
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Then open `http://localhost:3000/install` and let the setup wizard connect you to
|
|
Immich — see [First-Run Setup](#first-run-setup) below. It needs a token from the
|
|
server log, which is printed on first access.
|
|
|
|
<details>
|
|
<summary><strong>Prefer to configure by hand?</strong></summary>
|
|
|
|
The wizard writes the same files you would write yourself, so the manual route
|
|
remains fully supported:
|
|
|
|
```bash
|
|
cp .env.local.example .env.local
|
|
# Edit .env.local with your Immich server URL and API key
|
|
|
|
cp content/gallery.yaml.example content/gallery.yaml
|
|
# Edit gallery.yaml with your album UUIDs
|
|
|
|
npm run dev
|
|
```
|
|
|
|
</details>
|
|
|
|
Something not coming up? `npm run doctor` checks the configuration from the
|
|
terminal — no running app and no admin password needed. See
|
|
[Config Doctor](docs/deployment.md#config-doctor).
|
|
|
|
## First-Run Setup
|
|
|
|
A deployment with no `gallery.yaml` and no Immich credentials serves a setup
|
|
screen. The wizard at **`/install`** fills both in from the browser: it connects
|
|
to Immich, lets you pick albums (optional — you can add them later in `/admin`),
|
|
and names the site. Nothing is written until your credentials have been verified
|
|
against your Immich server, so a typo cannot leave you with an "installed" site
|
|
that loads no photos.
|
|
|
|
**The wizard is gated by a one-time token** printed to the server log on first
|
|
access, because a fresh deployment is reachable before it has any configuration —
|
|
and without the gate, whoever finds the URL first could configure it:
|
|
|
|
```
|
|
══════════════════════════════════════════════════════
|
|
Immich Folio — First-run setup token
|
|
══════════════════════════════════════════════════════
|
|
Token: PTmUKFIdce2DwuqBG71RAExH7tVxceXX
|
|
```
|
|
|
|
Read it from your container logs (`docker logs immich-folio`, or
|
|
`docker compose logs lightbox` — `immich-folio` is the container name, `lightbox`
|
|
the service) and append it to the URL:
|
|
|
|
```
|
|
http://your-site/install?token=PTmUKFIdce2DwuqBG71RAExH7tVxceXX
|
|
```
|
|
|
|
The token is stored in `content/.setup-token` (mode `0600`) so it survives a
|
|
restart, and is deleted once setup completes. From then on `/install` redirects
|
|
to the gallery and its API routes refuse to run again.
|
|
|
|
The wizard writes `gallery.yaml`, `settings.yaml` and `content/install.json` —
|
|
the last of which **holds your Immich API key**. Environment variables override
|
|
everything in it, so credentials can be rotated without touching the file, and
|
|
setting them all up front means the wizard never appears at all.
|
|
|
|
→ **[What the wizard writes, and environment precedence](docs/deployment.md#what-the-setup-wizard-writes)**
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables (`.env.local`)
|
|
|
|
```env
|
|
# Required
|
|
IMMICH_API_URL=http://your-immich-server:2283
|
|
IMMICH_API_KEY=your-api-key
|
|
|
|
# Optional
|
|
SITE_TITLE=My Photography # default: "Gallery"
|
|
SITE_SUBTITLE=A visual journal # default: empty
|
|
CACHE_TTL=300 # seconds, default: 300
|
|
STALE_MAX_AGE=86400 # seconds an expired cache entry survives during an outage, default: 86400 (24h), 0 disables
|
|
IMMICH_TIMEOUT_MS=15000 # Immich response wait, default: 15000
|
|
IMAGE_CACHE_VERSION=1 # bump to bust browser image caches, default: off
|
|
RATE_LIMIT_RPM=1500 # requests/min/IP for images, default: 1500
|
|
AUTH_SECRET=long-random-string # required in production
|
|
TRUSTED_PROXY_HOPS=1 # reverse proxies in front, default: 0
|
|
ADMIN_PASSWORD=your-secure-password # enables /admin panel
|
|
WEBHOOK_SECRET=long-random-string # enables POST /api/webhook cache invalidation
|
|
CDN_URL=https://cdn.example.com # serve photos and videos through a pull CDN
|
|
CONTACT_NOTIFY_URL=https://ntfy.sh/x # push for new contact form messages, overrides settings.yaml
|
|
```
|
|
|
|
> Login and setup endpoints have their own, much lower limits that `RATE_LIMIT_RPM` does not raise — see [Rate Limiting](docs/gallery-config.md#rate-limiting).
|
|
|
|
> Behind a reverse proxy, set `TRUSTED_PROXY_HOPS` to the number of proxies in front of the app (nginx/Traefik/Caddy = 1; Cloudflare in front of nginx = 2). Without it the client IP is read from a header the client itself can set, which defeats the brute-force limits on the password endpoints. See [Trusted Proxies](docs/gallery-config.md#trusted-proxies).
|
|
|
|
### Immich API Key Permissions
|
|
|
|
Create a dedicated API key in Immich under **Account Settings → API Keys**. Immich Folio only needs **read access** — it never modifies your library.
|
|
|
|
| Permission | Required | Used for |
|
|
| ------------ | -------- | -------------------------------------------------------------------- |
|
|
| `album.read` | ✅ Yes | List and fetch album metadata & photo lists |
|
|
| `asset.read` | ✅ Yes | Fetch asset metadata, EXIF data, thumbnails, previews, and originals |
|
|
| `asset.view` | ✅ Yes | Stream image/video files (thumbnail, preview, video playback) |
|
|
|
|
> **No write permissions needed.** `album.create`, `asset.upload`, `asset.delete`, etc. can all be left **off**.
|
|
|
|
> **Tip (Admin Panel):** The Admin Panel also uses `POST /search/metadata` to browse your full library for the hero image picker. This is covered by `asset.read` — no additional permission required.
|
|
|
|
### Gallery Config
|
|
|
|
All gallery structure — hero images, albums, subpages, grid layout, footer — is defined in `content/gallery.yaml`.
|
|
|
|
→ **[Gallery Configuration Guide](docs/gallery-config.md)**
|
|
|
|
### Journal & Photo Essays
|
|
|
|
Long-form storytelling with fullbleed photos, side-by-side pairs and grids, quotes, facts lists, a map of the entry's own geotagged photos and captions — as a standalone `/journal` section, or as a single essay on one subpage. New entries can start from a template.
|
|
|
|
→ **[Journal & Photo Essays Guide](docs/journal.md)**
|
|
|
|
### Theming
|
|
|
|
Seven built-in presets with distinct visual identities — or mix and match with fine-grained control over colors, fonts, corners, photo frames, hero layout, and grid style.
|
|
|
|
**Studio Modern** is the default: Leica precision rebuilt around the Archivo grotesque, with IBM Plex Mono for every piece of photographic metadata, hairline rules, zero radius, and red as signal only.
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/theme-studio-modern-home.png" width="49%" alt="Studio Modern in dark mode" />
|
|
<img src="docs/screenshots/theme-studio-modern-home-light.png" width="49%" alt="Studio Modern in light mode" />
|
|
</p>
|
|
<p align="center"><em>Studio Modern, dark and light — every preset ships both, and visitors switch with a toggle in the navigation bar.</em></p>
|
|
|
|
```yaml
|
|
theme: studio-modern # or: studio, minimal, editorial, classic, noir, monograph
|
|
```
|
|
|
|
→ **[View all Themes & Configuration Guide](docs/theming.md)**
|
|
|
|
## Docker
|
|
|
|
### Docker Compose (recommended)
|
|
|
|
```yaml
|
|
services:
|
|
lightbox:
|
|
build: .
|
|
container_name: immich-folio
|
|
restart: unless-stopped
|
|
ports:
|
|
- '7211:7211'
|
|
env_file:
|
|
- .env.local
|
|
volumes:
|
|
- ./content:/app/content
|
|
```
|
|
|
|
Run with:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
The gallery will be available at `http://localhost:7211`.
|
|
|
|
> The `content/` volume must be **read-write** — the setup wizard, the admin
|
|
> panel, the journal and the backup rotation all write into it. A `:ro` mount
|
|
> leaves the wizard unable to complete and the admin panel unable to save.
|
|
|
|
For standalone `docker run`, the health check, TLS behind a reverse proxy, and
|
|
what the gallery does while Immich is unreachable:
|
|
|
|
→ **[Deployment Guide](docs/deployment.md)**
|
|
|
|
## Admin Panel
|
|
|
|
A built-in visual editor at `/admin` lets you manage your gallery without editing YAML files manually.
|
|
|
|
**Enable it** by setting `ADMIN_PASSWORD` in your environment:
|
|
|
|
```env
|
|
ADMIN_PASSWORD=your-secure-admin-password
|
|
```
|
|
|
|
Or set the password in the [setup wizard](#first-run-setup), which stores it as
|
|
an scrypt hash rather than in your environment. An `ADMIN_PASSWORD` variable
|
|
takes precedence over the stored one if both are present.
|
|
|
|
Then navigate to `http://your-site/admin` and log in. The panel is protected by its
|
|
own password, separate from any album passwords, and writes straight to
|
|
`gallery.yaml` / `settings.yaml` with automatic backups.
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/admin-login.png" width="49%" alt="Admin panel login screen" />
|
|
<img src="docs/screenshots/admin-page-builder.png" width="49%" alt="Visual page builder with hero images, standalone albums and subpages" />
|
|
</p>
|
|
<p align="center">
|
|
<img src="docs/screenshots/admin-album-picker.png" width="49%" alt="Album picker listing shared Immich albums with photo counts" />
|
|
<img src="docs/screenshots/admin-settings.png" width="49%" alt="Settings editor with site identity and feature toggles" />
|
|
</p>
|
|
<p align="center"><em>Login · page builder · album picker · settings editor</em></p>
|
|
|
|
→ **[Admin Panel Guide](docs/admin-panel.md)**
|
|
|
|
## What's New
|
|
|
|
v0.18.0 — a contact form, a privacy page and a reworked admin:
|
|
|
|
- **Built-in contact form** — no SMTP or form service: messages land in a new admin inbox, with an optional ntfy push that carries nothing about the sender
|
|
- **Privacy page and fewer third parties** — `/privacy` with a list of what your installation actually processes; fonts and map styles are served from your own server, and email addresses stay out of the page source
|
|
- **A reworked admin panel** — a collapsible sidebar, an overview page, the page builder as a structure list with Undo, and ON/OFF switches grouped by meaning
|
|
- **Four more languages and a CDN mode** — French, Spanish, Italian and Dutch, and `CDN_URL` to serve photos through a pull CDN
|
|
- **Two security fixes** — draft and password-protected journal entries used as subpage essays, and unescaped Immich descriptions ([advisories](https://github.com/ralksta/immich-folio/security/advisories))
|
|
|
|
**Upgrade note:** a custom CSP or proxy allowlist must now allow `tile.openstreetmap.org` for map tiles, and `theme.accent` must be a hex colour. Nothing else to migrate.
|
|
|
|
Security fixes ship in normal releases, so **running the latest release is the
|
|
recommended baseline**.
|
|
|
|
→ Full history in [CHANGELOG.md](CHANGELOG.md) and the [GitHub releases](https://github.com/ralksta/immich-folio/releases)
|
|
|
|
## Tech Stack
|
|
|
|
- **Next.js 16** (App Router, standalone output)
|
|
- **React 19**
|
|
- **TypeScript**
|
|
- **Vanilla CSS** (no framework)
|
|
|
|
## Contributors
|
|
|
|
Immich Folio is maintained by [@ralksta](https://github.com/ralksta) and made
|
|
better by the people below. Thank you — every one of these made the project
|
|
easier to live with.
|
|
|
|
- **[@lancetm714](https://github.com/lancetm714)** — built the setup wizard that
|
|
turns a fresh install into a few clicks in the browser instead of hand-written
|
|
config files. Also added custom favicons, so your portfolio gets its own icon
|
|
in the browser tab, and made a gallery with no albums yet a perfectly valid
|
|
starting point rather than an error. In v0.15.0, made client proofing work
|
|
properly: the selection bar floats again with page transitions on, and it is
|
|
readable in light mode. Also brought links to journal quote attributions. In
|
|
v0.17.0, built the ZIP download: a whole album, or just the photos a client
|
|
picked, as one archive of the originals — streamed, so even a large shoot
|
|
arrives complete. In v0.18.0, kept accent-coloured buttons readable when the
|
|
accent is light, and made a mistyped accent fall back to the theme instead of
|
|
turning every button invisible.
|
|
- **[@RichKidsDev](https://github.com/RichKidsDev)** — noticed that the
|
|
Impressum still cited the TMG, which the DDG replaced in 2024. That report
|
|
turned into a proper overhaul in v0.18.0: the right law by default, a heading
|
|
you can change for other countries, clickable contacts and a contact form as
|
|
a second channel.
|
|
- **[@ImScheinox](https://github.com/ImScheinox)** — found and fixed portrait
|
|
photos rendering as landscape tiles, because the grid ignored the camera's
|
|
EXIF orientation flag. That brought the masonry layout's stagger back for
|
|
anyone whose camera records portrait frames that way, which is most of them.
|
|
- **[Jules](https://jules.google.com)** — an automated reviewer that has been
|
|
quietly hardening the project in the background: better screen-reader support
|
|
in the photo grid, and a series of fixes keeping the public endpoints from
|
|
being overwhelmed by traffic.
|
|
- **[Dependabot](https://github.com/dependabot)** — keeps every dependency
|
|
current, which is most of the reason security fixes land here quickly.
|
|
|
|
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) to get
|
|
started. Pull requests target the `dev` branch.
|
|
|
|
## License
|
|
|
|
MIT License — free to use and modify for the Immich community.
|