Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
19 KiB
Feature-Brainstorm: Kundengeschäft, Handwerk, Vertrauen
Stand: 2026-08-15 · Basis: v0.11.0 (dev, nach #431 und #460) · Fokus: was ein Fotograf im Alltag mit dieser Software tut
Dieses Dokument ergänzt ideas.md (Präsentation),
brainstorm.md (Mai 2026) und
brainstorm-2026-08-05.md (Verbreitung & Betrieb).
Es wiederholt deren Ideen nicht. Wo es einen bereits gelisteten Punkt aufgreift,
steht es ausdrücklich dabei (Abschnitt D).
Die Perspektive hier ist eine andere als in den Vorgängern: nicht „was fehlt der Software", sondern „was tut ein Fotograf, das Folio heute nicht begleitet" — Bilder an Kunden übergeben, eine Serie sequenzieren, entscheiden, was die Welt über den Aufnahmeort erfahren darf.
Alle Befunde sind gegen den Code-Stand geprüft; Aufwand: XS (Stunden), S (1 Tag), M (2–5 Tage), L (Wochen).
Was seit den alten Listen dazugekommen ist
Damit klar ist, warum die folgenden Ideen neu sind: seit #431 und #460 leben bereits
Fokuspunkt fürs Cover, Bildunterschriften aus der Immich-Description,
justified-Layout, unlisted Subpages, externe Nav-Links, Cover-Splash-Hero und
per-Album-Grid. Früher kamen Wasserzeichen, Rechtsklick-Schutz, Client-Proofing
inklusive Share-Link und Mailto-Export, Map, Video, Journal/Essays, Setup-Assistent,
Error-Boundaries und der Stale-Cache dazu.
Weiterhin offen aus den alten Listen und hier bewusst nicht nochmal ausgeführt: Slideshow, Fullscreen, Download-Button, Copy-Link, Photo of the Day, ISR, EXIF-Statistikseite, Before/After-Slider, i18n, Timeline, Smart Search, PWA, Demo-Modus, Config-Doctor, Platten-Cache, Metriken, Multi-Site.
A. Kundengeschäft
Folio kann Kunden heute Bilder zeigen (Passwort, Proofing) — aber der Weg davor und danach fehlt: wie der Kunde hereinkommt und wie er sich zurückmeldet.
A1. Ablaufende, widerrufbare Client-Links — M, hoher Impact
Befund: Kundenzugang ist heute ein statisches Passwort in gallery.yaml, geprüft
gegen ein HMAC-Cookie (lib/auth.ts:44-46, lb_auth_album_<slug>). Daraus folgt
dreierlei: Das Passwort läuft nie ab. Es lässt sich nicht für einen Empfänger
zurückziehen, sondern nur für alle gleichzeitig ändern. Und der Kunde muss es tippen —
in der Praxis steht es deshalb ohnehin in derselben Mail wie der Link, was die
Zugangskontrolle auf ein Feigenblatt reduziert.
Idee: Zugangs-Links, die im Admin erzeugt werden:
https://folio.example/hochzeit-mueller?k=<signiertes-token>
Das Token trägt Slug, Ablaufdatum und eine Link-ID; signiert mit derselben Mechanik
wie die Admin-Session (lib/admin/auth.ts). Beim ersten Aufruf wird es gegen genau
das Cookie eingetauscht, das heute schon existiert — der gesamte Rest der
Zugangsprüfung bleibt unangetastet. Im Admin: eine Liste mit Label („Brautpaar",
„Location"), Ablaufdatum und einem Widerrufen-Knopf.
Warum das zieht: Es ist der Unterschied zwischen „hier ist ein Passwort" und dem, was Kunden von Pixieset oder Pic-Time kennen. Für Hochzeits- und Auftragsfotografen ist die Galerie-Übergabe der eigentliche Arbeitsschritt, nicht das Portfolio.
Zwei Trade-offs, die benannt gehören:
- Widerruf braucht Zustand. Ein rein signiertes Token kann man nicht
zurücknehmen, nur ablaufen lassen. Echter Widerruf verlangt eine Liste ausgegebener
Link-IDs in
content/— sonst ist das Wort „widerrufbar" gelogen. Das ist der erste Fall, in dem Folio Besucher-bezogenen Zustand speichert; die Liste sollte entsprechend klein und selbstaufräumend sein. - Bild-URLs bleiben unberührt. Asset-Tokens sind bewusst deterministisch, damit
Browser cachen können. Ein widerrufener Zugang macht bereits ausgelieferte
Bild-URLs also nicht ungültig — derselbe Trade-off, der in
deep-dive-2026-08-05.mdfür die Allowlist notiert ist. Er muss hier dokumentiert werden, sonst erwartet jemand mehr, als das Feature hält.
A2. Anfrage-Formular statt mailto: — S–M
Befund: Es gibt kein Formular. Kontakt läuft über mailto: im Footer
(components/Footer.tsx:60) und im Proofing-Modal
(components/ProofingModal.tsx:43). Das hat zwei Kosten, die Fotografen real treffen:
Die Adresse steht im Klartext im HTML und wird abgegrast, und auf Geräten ohne
konfigurierten Mail-Client passiert beim Klick schlicht nichts — der Kunde denkt, der
Link sei kaputt.
Idee: Eine rate-limitierte Route (lib/rate-limit.ts ist da, eigener Namensraum
contact:, niedriges Limit in der Größenordnung der Auth-Routen) plus Honeypot-Feld.
Zustellung wahlweise per SMTP oder per Webhook. Und — das ist der eigentliche Wert —
ein „Zu diesem Bild anfragen" direkt im Lightbox: Print-Anfrage, Lizenzanfrage,
Buchung, mit Bildreferenz vorausgefüllt. Aus einer Galerie wird damit ein
Verkaufskanal, ohne dass ein Shop dranhängt.
Ehrlich zu benennen: SMTP wäre die erste ausgehende Verbindung des Projekts
überhaupt und die erste ernsthafte neue Dependency (das Projekt hat heute sieben
Runtime-Dependencies und keine Mail-Bibliothek). Der Webhook-Weg vermeidet beides und
passt besser zur bestehenden Linie — POST /api/webhook zeigt, dass das Muster im
Haus schon verstanden ist. Empfehlung: mit Webhook anfangen, SMTP optional
nachziehen.
A3. Proofing v2 — Notizen statt nur Herzen — S
Befund: Die Auswahl ist eine Bitmaske von Indizes (lib/proofing.ts), exportiert
als Link, als nummerierte Liste oder per Mailto. Sie transportiert also genau ein Bit
pro Foto: gewählt oder nicht.
Idee: Ein kurzes Notizfeld pro ausgewähltem Foto. Kunden sagen selten nur „dieses", sondern „dieses, aber enger geschnitten" oder „das für die Danksagung". Heute landet das in einer separaten Mail mit Bildnummern, die der Fotograf von Hand zuordnet — der unangenehmste Teil des ganzen Ablaufs.
Der Trade-off ist hier die eigentliche Designfrage: Freitext passt nicht in eine Bitmaske im URL-Parameter. Zwei saubere Auswege:
- Klein: Notizen bleiben in
localStorageund wandern nur in den Export-Text. Der Share-Link trägt weiterhin nur die Auswahl. Kein Server, kein neuer Zustand — passt exakt zur heutigen Architektur („nichts wird serverseitig gespeichert"). - Groß: Ein Endpunkt, der die Auswahl entgegennimmt. Das wäre der erste Schreibzugriff durch Besucher — mit allem, was daran hängt: Rate-Limiting, Größenbegrenzung, Spam, Aufbewahrung. Nur sinnvoll zusammen mit A1, weil erst ein identifizierbarer Client-Link die Rückmeldung zuordenbar macht.
Empfehlung: die kleine Variante. Sie kostet einen Tag und löst 90 % des Problems.
B. Handwerk & Präsentation
Die Zielgruppe sind laut den bisherigen Doks „Fotografen mit Gestaltungsanspruch". Die drei folgenden Punkte zielen genau darauf — und einer davon ist ein Befund, der mich beim Lesen überrascht hat.
B1. Sequenzierung wie im Fotobuch — M, der stärkste Differenzierer hier
Befund: Ein Layout gilt für ein ganzes Album (grid.layout, seit #431 auch pro
Album überschreibbar). Die Reihenfolge ist frei wählbar (assetOrder,
lib/config/schema.ts:224, mit Drag & Drop im Admin) — aber jedes Foto bekommt
dieselbe Behandlung wie sein Nachbar.
Ein Fotobuch funktioniert anders. Es lebt von Paaren, die sich auf einer
Doppelseite antworten, von einem Bild, das allein über die volle Breite geht, und
von Pausen. Genau diese Mittel besitzt die Essay-Ansicht bereits — die
Journal-Syntax kennt fullbleed, wide und Foto-Paare (lib/journal.ts). Das Album
kennt sie nicht.
Idee: Optionale Layout-Hinweise entlang des vorhandenen assetOrder:
- 'album-uuid':
sort: manual
assetOrder:
- 'asset-opening': fullbleed # allein, volle Breite
- 'asset-a': pair # diese beiden nebeneinander,
- 'asset-b': pair # in gleicher Höhe
- 'asset-c'
- break # bewusste Lücke, dann geht es weiter
Renderer-seitig ist das kein neues Layout-System, sondern eine Segmentierung des
bestehenden Grids: PhotoGrid gruppiert die Assets vor dem Rendern in Blöcke und
rendert je Block das passende Muster. Die Bausteine — Aspect-Ratio pro Asset
(assetAspectRatio), Zeilenmathematik aus justified — liegen schon da.
Warum das zieht: Es ist das einzige Feature auf dieser Liste, das kein anderes Immich-Portfolio-Tool hat, und es spricht die Zielgruppe an ihrer empfindlichsten Stelle an: Reihenfolge ist Handwerk. Eine Serie, die als gleichförmiges Raster ausgespielt wird, ist eine Serie, deren Rhythmus verloren geht.
Trade-off: Das Feature ist im Admin nur so gut wie seine Vorschau. Layout-Hinweise
in einer Liste zu setzen, ohne das Ergebnis zu sehen, ist frustrierend — der
AssetOrderEditor müsste die Blöcke andeuten. Das ist die halbe Arbeit an dieser
Idee, nicht das YAML-Schema.
B2. 1:1-Zoom im Lightbox — S (mit einem Nebenbefund)
Befund — und der ist präziser, als er zunächst aussieht: Der Lightbox hat keinerlei
Zoom; die einzige Transformation ist eine Mount-Transition von scale(0.96) auf 1
(components/Lightbox.module.css). So weit erwartbar.
Interessanter ist, was er anzeigt. Der Dateikopf verspricht
„Full-resolution image display" (components/Lightbox.tsx:5), gerendert wird aber
current.previewUrl (components/Lightbox.tsx:237). Und imageUrl() steht per
Default auf 'preview' (lib/urls.ts:29) — eine Suche über app/, components/
und lib/urls.ts findet keine einzige Stelle, die 'original' anfordert. Die
oberste Größenstufe aus lib/imageSize.ts ist im gesamten Frontend unerreichbar.
Das ist als Default vollkommen richtig — Originale an jeden Besucher auszuliefern wäre
verschwenderisch, und resolveImageSize() verhindert das aus gutem Grund. Aber es
heißt: Der Lightbox zeigt heute Immichs Preview, nicht die Aufnahme. Wer Schärfe
beurteilen will — der Fotograf selbst, und jeder Kunde, der bei der Auswahl auf den
Fokus schaut — kann das nicht.
Idee: Klick oder Pinch schaltet auf original um und erlaubt Pan. Erst bei dieser
Geste wird die große Datei geholt, nie vorher. Bewusst getrennt von den bereits
gelisteten Punkten Fullscreen und Download: Zoom ist Beurteilung, nicht Präsentation
und nicht Übergabe.
Nebenbei zu korrigieren: der Kommentar in Lightbox.tsx:5. Er beschreibt eine
Absicht, nicht das Verhalten.
B3. Farbmanagement — eine offene Frage, keine Behauptung, S für die Messung
Aus B2 folgt eine Frage, die ich mit Codelesen allein nicht beantworten kann und die deshalb hier als Messaufgabe steht, nicht als Befund:
Fotografen arbeiten in AdobeRGB oder Display-P3. Wenn Folio ausschließlich Immichs
Preview-Tier ausliefert (siehe B2), dann hängt die Farbtreue des gesamten Portfolios
daran, was Immich beim Erzeugen dieser Previews mit dem Farbprofil macht — und ob
streamAsset() das Profil mit durchreicht. Fehlt es, rendert der Browser die Bytes
als sRGB: Wide-Gamut-Material wirkt dann flau oder, umgekehrt eingebettet,
übersättigt. Und zwar ausgerechnet auf dem guten Display, an dem der Fotograf sitzt.
Testweg: Eine Datei mit sichtbar breitem Gamut (gesättigtes Rot/Cyan) in AdobeRGB nach Immich laden, in Folio öffnen, die ausgelieferten Bytes auf ein eingebettetes ICC-Profil prüfen und gegen dieselbe Datei lokal im Browser vergleichen.
Wenn sich ein Verlust zeigt, sind die Antworten der Reihe nach: Profil im Proxy
mit durchreichen (Header und Bytes bleiben unangetastet — streamAsset() streamt
ohnehin unverändert); und für die Zoom-Ansicht aus B2 gleich das Original nehmen, das
sein Profil mitbringt.
Wenn sich keiner zeigt, ist der Abschnitt einen Satz in der Doku wert — „Folio liefert Immichs Previews unverändert aus, inklusive Farbprofil" — und damit erledigt. Auch das ist ein Ergebnis: Farbtreue ist bei dieser Zielgruppe ein Vertrauensargument, und ein belegter Satz dazu ist mehr wert als ein Feature.
C. Vertrauen & Privatsphäre
C1. Ortsangaben pro Album steuern — S
Befund, sorgfältig gelesen: Die Map ist datenschutzseitig deutlich besser gebaut,
als man befürchten würde. Passwortgeschützte Alben werden vor der Aggregation
herausgefiltert (app/api/map/route.ts:63-66), und lib/mapService.ts:5-11 hält
Alben ausdrücklich getrennt, damit genau das möglich ist. Die öffentliche EXIF-Route
gibt Koordinaten gar nicht erst heraus.
Die Lücke ist eine andere und feiner: Für öffentliche Alben wird ein Marker aus dem Mittelwert der tatsächlichen Koordinaten aller Fotos an diesem Ort gebildet. Bei einem Reisealbum über eine ganze Stadt ist das unkritisch. Bei einem Album, das an einem Ort entstanden ist — der eigene Garten, das Wohnzimmer eines Kunden, das Studio, ein empfindlicher Naturstandort —, ist der Mittelwert genau dieser eine Ort. Der Marker heißt zwar nach der Stadt, steht aber auf dem Grundstück.
Niemand trifft diese Entscheidung heute bewusst; sie ergibt sich daraus, dass die Kamera GPS mitschreibt.
Idee: Eine Granularität pro Album und pro Subpage:
location: exact # heutiges Verhalten
location: city # auf das Stadtzentrum runden, nicht mitteln
location: country # nur im Land verorten
location: hidden # gar nicht auf der Karte
Der Eingriff sitzt in lib/mapService.ts, wo bereits pro Album aggregiert wird — die
Stelle, an der man die Auflösung reduziert, existiert also schon.
Warum das zieht: Es macht aus einer stillschweigenden Preisgabe eine bewusste Entscheidung. Für ein Projekt, dessen Verkaufsargument „dein Immich bleibt privat" lautet, ist das kein Nebenfeature, sondern eine Konsequenz derselben Haltung.
C2. Terminierte Veröffentlichung — S
Befund: draft: true gibt es nur für Journal-Einträge (app/journal/page.tsx:33,
Drafts bleiben für den eingeloggten Admin sichtbar — ein gutes Muster). Alben und
Subpages sind live, sobald sie in der YAML stehen. Wer eine Seite vorbereiten will,
muss sie entweder verstecken (hidden: true, seit #431) und im richtigen Moment von
Hand umstellen, oder sie erst im letzten Moment anlegen.
Idee: Ein publishAt-Datum für Alben, Subpages und Journal-Einträge. Vor dem
Termin verhält sich der Eintrag wie ein Draft: für den Admin sichtbar, für alle
anderen nicht vorhanden.
Warum das billig ist: Alle Seiten sind force-dynamic. Es braucht keinen
Scheduler, keinen Cron, keinen Hintergrundjob — nur einen Datumsvergleich beim
Ableiten der Galerie, an derselben Stelle, an der heute schon enabled und hidden
ausgewertet werden.
Warum es trifft: Es ist ein realer Ablauf, kein hypothetischer. Die Hochzeitsgalerie geht Freitag um 18 Uhr auf, wenn das Paar aus den Flitterwochen zurück ist. Die Serie erscheint zum Ausstellungstermin. Das Journal zum Buchstart. Heute heißt das: Wecker stellen und selbst am Rechner sitzen.
Ein Detail, das mitgedacht gehört: Der RSS-Feed aus D darf einen terminierten Eintrag nicht vorab ausliefern — dieselbe Prüfung, dieselbe Stelle.
D. RSS/Atom-Feed — konkretisiert
Steht seit Mai als Einzeiler in brainstorm.md („/feed.xml mit
neuen Alben", Zeile 23) und dort auf Platz 1 der Reihenfolge. Verifiziert: eine
Feed-Route existiert nicht. Hier zum ersten Mal durchdacht — und der interessante
Teil ist nicht das XML.
Der eigentliche Inhalt dieses Features ist die Ausschlussliste. Ein Feed sammelt genau das ein, was die Zugangskontrolle sonst verbirgt: Titel, Beschreibungen, Cover-Bilder, Veröffentlichungsdaten. Er ist damit die perfekte Leak-Fläche, und zwar eine, die niemandem auffällt, weil sie im Browser nie sichtbar wird. Draußen bleiben müssen:
- passwortgeschützte Subpages und Alben (
password:auf beiden Ebenen) hidden: true-Subpages — sie sind bewusst nur per Direktlink erreichbar, und ein Feed ist das Gegenteil eines Direktlinks- Journal-Drafts (
draft: true) - alles mit einem
publishAtin der Zukunft, falls C2 kommt - Alben, die per Client-Link aus A1 zugänglich sind
Zwei Quellen, ein Feed: Journal-Einträge (haben Datum, Titel, Excerpt und Cover —
alles schon da) und neu hinzugekommene Alben. Cover-Bilder über den bestehenden
/api/og-Weg, damit kein neuer Bildpfad entsteht.
Was es absichert: ein Test, der die Ausschlussregel festschreibt — analog zu
app/api/admin/__tests__/admin-guards.test.ts, also tabellengetrieben über die
Kategorien, nicht als Sammlung von Einzelfällen. Dann schlägt er auch bei einer
künftig hinzugefügten Sichtbarkeitsregel an, die jemand im Feed zu berücksichtigen
vergisst.
Aufwand: S für den Feed. Die Sorgfalt steckt komplett in der Sichtbarkeitsprüfung — und die ist der Grund, warum dieser Punkt hier nochmal auftaucht, statt als Einzeiler stehenzubleiben.
Anhang: zwei Kleinigkeiten zur Auffindbarkeit
Kurz gehalten, weil Auffindbarkeit diesmal nicht der Schwerpunkt war — aber beide sind XS und stehen in keiner der alten Listen:
- Es gibt weder
app/sitemap.tsnochapp/robots.ts. Robots-Direktiven existieren nur auf Meta-Ebene (app/layout.tsx:37-48). Next.js erzeugt beide Dateien aus je einer Funktion; die Album- und Subpage-Liste liegt ingetConfig()bereit. Dieselbe Ausschlussliste wie beim Feed gilt auch hier. - Kein JSON-LD. Für ein Fotografie-Portfolio ist
ImageObjectmitcreatorundlicensedie eine strukturierte Angabe, die sich wirklich auszahlt — Google Bilder zeigt Lizenzhinweise an, und es ist die einzige Stelle, an der ein Fotograf seine Urheberschaft maschinenlesbar an die Bilder heftet. Passt inhaltlich zu A2: wer Lizenzanfragen will, sollte auffindbar machen, dass Lizenzen zu haben sind.
Empfohlene Reihenfolge
Erst — klein, sofort spürbar:
- C2 Terminierte Veröffentlichung · S — ein Datumsvergleich, ein echter Ablauf
- C1 Ortsangaben pro Album · S — die Stelle im Code existiert bereits
- B2 1:1-Zoom · S — und der falsche Kommentar in
Lightbox.tsx:5fällt mit weg
Dann — die zwei mit dem größten Hebel:
- B1 Sequenzierung wie im Fotobuch · M — das Alleinstellungsmerkmal
- A1 Ablaufende Client-Links · M — das Feature, das aus dem Portfolio ein Arbeitswerkzeug macht
Danach:
- D RSS-Feed · S — mit der Ausschlussliste als eigentlicher Arbeit
- A2 Anfrage-Formular · S–M — mit Webhook beginnen, SMTP später
- A3 Proofing-Notizen · S — kleine Variante
- B3 Farbmanagement messen · S — Ergebnis offen, Erkenntnis in jedem Fall wertvoll
- Anhang: Sitemap,
robots.ts, JSON-LD · XS
Roter Faden
Drei Sätze halten die Auswahl zusammen.
Ein Portfolio ist Arbeit, nicht nur Ausstellung. A1–A3 begleiten den Weg, den ein Auftragsbild ohnehin nimmt — Übergabe, Rückmeldung, Anfrage. Folio zeigt heute nur den mittleren Teil davon.
Reihenfolge ist Handwerk. B1 ist der einzige Punkt hier, den kein vergleichbares Werkzeug bietet, und er trifft die Zielgruppe genau dort, wo sie anspruchsvoll ist.
Privatsphäre gilt nach außen genauso wie nach innen. Das Versprechen lautet „dein Immich bleibt privat". C1 und C2 ziehen dieselbe Linie für das, was die veröffentlichte Seite über Orte und Zeitpunkte verrät — und D stellt sicher, dass ein Feed sie nicht hintenherum wieder aufmacht.