Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
25 KiB
Portfolio-Features: Auffindbarkeit, Zoom, Sequenzierung, Ortsauflösung
For agentic workers: Steps use checkbox (
- [ ]) syntax for tracking. Die vier Tasks sind voneinander unabhängig und können einzeln umgesetzt, getestet und gemerged werden.
Goal: Vier Vorschläge aus brainstorm-2026-08-15.md umsetzen —
E (Auffindbarkeit), B2 (1:1-Zoom), B1 (Sequenzierung), C1 (Ortsauflösung). Reihenfolge nach
steigendem Risiko: erst zwei abgeschlossene Kleinigkeiten, dann der größte Eingriff, dann ein
Datenschutz-Feature mit klarer Angriffsfläche.
Architecture: Vier getrennte Eingriffe ohne gemeinsame Dateien — bis auf lib/config/schema.ts
und lib/config/index.ts, die von B1 und C1 beide angefasst werden (verschiedene Felder, kein
inhaltlicher Konflikt). Jede neue Per-Album-Option folgt dem etablierten Muster: ein
Record<albumId, X> in AppConfig, befüllt in deriveGallery(), genau wie albumGrids und
albumCoverPositions es seit #431 vormachen.
Tech Stack: Next.js 16 (App Router, React 19), TypeScript strict, Vitest 4 (node environment), Playwright, Vanilla CSS, keine neuen Laufzeit-Dependencies.
Global Constraints
- Keine neuen Runtime-Dependencies. Das Projekt hat sieben; das bleibt so.
- TypeScript strict — kein untypisiertes
any; Suppressions nur als@ts-expect-errormit Begründung. - Rohe Immich-UUIDs dürfen nie im Client-HTML/JS landen. Immer
imageUrl()/encodeAssetId(). - Vanilla CSS, Tokens aus
app/tokens.css. - Formatierung: nur geänderte Dateien (
npx prettier --write <datei>), nienpm run formatim Root. - Vor jedem Commit grün:
npm run test:unit,npx tsc --noEmit,npm run lint,npm run build. - Baseline vor Beginn: 443 Tests in 35 Dateien. Jede Task nennt die erwartete neue Summe.
- Commit-Stil: Conventional Commits. Experimentelle Optionen als
EXPERIMENTALin YAML-Kommentar und Admin-Label markieren, so wie #431 es gemacht hat. - Neue Env-Variablen gehören in
lib/env.tsund.env.local.example. - Vitest läuft im
node-Environment — es gibt kein jsdom. Komponentenlogik ist deshalb nur testbar, wenn die reine Berechnung in einem eigenen Modul liegt; die DOM-Verdrahtung deckt Playwright ab. Das ist bei B1 und B2 jeweils eingeplant und ausdrücklich kein Verstoß gegen die CLAUDE.md-Regel (die verbietet, Route-Logik nur der Testbarkeit wegen nachlib/zu ziehen — hier geht es um echte, wiederverwendbare Geometrie- bzw. Gruppierungsfunktionen).
Task E — Auffindbarkeit: Site-URL, robots.txt, sitemap.xml, JSON-LD
Aufwand: S (die Vorbedingung E1 und die Admin-UI machen daraus mehr als das im Brainstorm geschätzte XS.)
Befund vorab: es fehlt eine Site-URL
lib/env.ts kennt keine SITE_URL, settings.yaml keine url:, und app/layout.tsx setzt
keine metadataBase. OG-Bilder werden heute als relativer Pfad angegeben
(app/layout.tsx:57). Für sitemap.xml ist das ein hartes Hindernis: Der Sitemap-Standard
verlangt absolute URLs, und app/sitemap.ts läuft ohne Request-Kontext — der Host lässt sich also
nicht aus Headern ableiten.
Entscheidung: url in settings.yaml, überschreibbar durch SITE_URL. Env gewinnt — dasselbe
Präzedenzmuster wie bei Titel und Credentials. In settings.yaml, weil der Wert im Admin-Panel
setzbar sein muss: Wer per docker run startet, soll die Site-URL nicht in einer Env-Variable
nachtragen und neu starten müssen, sondern sie dort eingeben, wo er ohnehin Titel und SEO pflegt.
Die Falle dabei — und warum E1b im Plan steht
Env gewinnt über YAML. Setzt jemand SITE_URL und trägt im Admin etwas anderes ein, speichert
das Panel brav, ändert aber nichts — der Wert wird stillschweigend überstimmt. Genau dieser Zustand
existiert heute schon unbemerkt bei SITE_TITLE/SITE_SUBTITLE: lib/env.ts:98-99 schlägt
settings.yaml und das Admin-Panel sagt es nirgends. Eine repo-weite Suche findet in
SettingsEditor.tsx, AdminDashboard.tsx und /api/admin/status keinen einzigen Hinweis auf
Env-Überschreibungen.
Weil dieses Feld nun ausdrücklich über die UI bedienbar sein soll, wird die Anzeige mitgebaut — klein, generisch und für die bestehenden Felder gleich mitverwendet.
Schritte
- E1 — Site-URL einführen
SITE_URLinlib/env.ts(interface Env+parseEnv()), validiert übernew URL(), trailing Slash entfernt — analog zur bestehenden Behandlung vonIMMICH_API_URL:26-32.url?: stringinSettingsYaml(lib/config/schema.ts) undsiteUrl: stringinAppConfig.- Auflösung in
lib/config/index.ts:env.SITE_URL || settings.url || ''. metadataBaseinapp/layout.tsxsetzen, wennsiteUrlnicht leer ist.- In
settings.yaml.exampledokumentieren, inklusive des Hinweises, dass ohne diesen Wert Sitemap und JSON-LD entfallen.
- E1a — Feld im Admin-Panel (
app/admin/components/SettingsEditor.tsx, Sektionseo,SETTINGS_SECTIONS:197)- Textfeld „Site URL" als erstes Feld der SEO-Sektion, über „SEO Title" — es ist die
Voraussetzung für alles andere dort. Anbindung wie die Nachbarfelder:
update('url', …). (Achtung:urlliegt auf oberster Ebene, nicht unterseo.— sonst landet es beim Auflösen an der falschen Stelle.) - Placeholder
https://folio.example.com, Hilfetext: wofür der Wert gebraucht wird (Sitemap,robots.txt, JSON-LD, absolute OG-Bild-URLs). - Validierung im Feld, nicht erst beim Speichern: muss mit
http://oderhttps://beginnen und übernew URL()parsebar sein; trailing Slash beim Speichern abschneiden, damit aus Nutzersicht egal ist, ob er einen setzt. Ungültige Eingabe blockiert das Speichern mit einer Meldung am Feld — eine kaputte Site-URL erzeugt sonst eine Sitemap voller kaputter Links. - Leeres Feld ist ein gültiger Zustand (= Funktionen aus, wie heute), keine Fehlermeldung.
- Ein Hinweis in der SEO-Sektion, solange das Feld leer ist: „Ohne Site URL werden
sitemap.xmlund JSON-LD nicht ausgeliefert." Sonst sucht jemand den Fehler an der falschen Stelle.
- Textfeld „Site URL" als erstes Feld der SEO-Sektion, über „SEO Title" — es ist die
Voraussetzung für alles andere dort. Anbindung wie die Nachbarfelder:
- E1b — Env-Überschreibungen im Panel sichtbar machen (klein, generisch)
GET /api/admin/settings(app/api/admin/settings/route.ts:18-19) gibt zusätzlichenvLocked: string[]zurück — die Namen der Felder, die gerade aus der Umgebung kommen. Anfangs:url(wennSITE_URLgesetzt),title(SITE_TITLE),subtitle(SITE_SUBTITLE). Nur Feldnamen, keine Werte — die Route soll keine Env-Inhalte spiegeln, die sonst nirgends im Panel stehen.SettingsEditorrendert betroffene Felder deaktiviert mit dem Zusatz „Von der UmgebungsvariableSITE_URLgesetzt" statt eines editierbaren Eingabefelds. Damit ist der stille Fehlschlag weg — beim neuen Feld und bei Titel/Untertitel gleich mit.- Das ist die erste Stelle dieser Art im Projekt; entsprechend klein halten (eine Liste, ein
disabled, ein Hinweistext) und nicht zu einem Mechanismus ausbauen.
- E2 —
app/robots.tsDisallowfür/admin,/install,/api.seo.noIndex: true→Disallow: /für alle Agents.sitemap:-Verweis nur, wennsiteUrlgesetzt ist.
- E3 —
app/sitemap.ts- Gibt
[]zurück, wennsiteUrlleer ist oderseo.noIndexgilt — kein halber Zustand. - Enthalten:
/,/about(nur wennaboutEnabled),/map(nur wennconfig.map),/journalund je Eintrag/journal/<slug>, Subpages und Alben. - Ausschlussliste — der eigentliche Inhalt dieser Task:
Ausgeschlossen Quelle Subpage mit passwordSubpageConfig.passwordSubpage mit hidden: trueSubpageConfig.hiddenSubpage mit enabled: falseSubpageConfig.enabledAlbum mit eigenem Passwort config.albumPasswordsAlbum unterhalb einer geschützten/versteckten Subpage Vererbung, siehe unten Journal-Draft frontmatter.draftJournal-Eintrag mit Passwort Frontmatter /impressumträgt bereits robots: noindex(app/impressum/page.tsx:14) - Vererbung nicht vergessen: Ein öffentliches Album unter einer passwortgeschützten Subpage
ist über
/<subpage>/<album>nicht erreichbar. Die Prüfung muss den Pfad entlanglaufen, nicht nur das Album ansehen. lastModifiedausalbum.updatedAtbzw. dem Journal-Datum.
- Gibt
- E4 — JSON-LD
Personbzw.ProfilePageauf/about, gespeist aus dem Frontmatter voncontent/about.md.ImageObjectmitcreatorund optionallicenseauf Album-Detailseiten.- CSP-Falle:
proxy.tssetzt eine CSP mit Per-Request-Nonce und reicht sie über denx-nonce-Header an die Seiten. Ein<script type="application/ld+json">muss diese Nonce tragen, sonst blockt der Browser es still. Im Browser mit gesetzter CSP verifizieren, nicht nur im Markup nachsehen. licenseals optionales Feld insettings.yaml(seo.license), leer = weglassen.
- E5 — Tests
lib/__tests__/sitemap.test.ts: tabellengetrieben über die Ausschlusskategorien, nicht als Einzelfälle — dann schlägt der Test auch bei einer künftig hinzugefügten Sichtbarkeitsregel an. Vorbild:app/api/admin/__tests__/admin-guards.test.ts. Zusätzlich: leere Sitemap bei fehlendersiteUrl;noIndexunterdrückt alles.lib/__tests__/site-url.test.ts: PräzedenzSITE_URL>settings.url> leer; trailing Slash wird entfernt; ungültige URL führt nicht zum Absturz, sondern zu „nicht gesetzt".app/api/admin/__tests__/settings-env-lock.test.ts:envLockedenthälturlgenau dann, wennSITE_URLgesetzt ist — und die Antwort enthält nirgends den Env-Wert selbst.- Erwartet: +18 bis +24 Tests (461–467 gesamt).
Verifikation:
- Ohne
SITE_URL: Im Admin unter SEO die Site URL eintragen, speichern, dann/sitemap.xmlund/robots.txtabrufen — beide müssen ohne Neustart die neue Domain führen (invalidateConfigCache()läuft beim Speichern bereits). Feld leeren → Sitemap wird wieder leer, Hinweistext erscheint. - Ungültige Eingaben gegenprüfen:
folio.example.comohne Schema undhttps://allein müssen am Feld abgewiesen werden, nicht erst beim Rendern der Sitemap auffallen. - Mit gesetztem
SITE_URL: Das Feld muss deaktiviert sein und die Herkunft nennen — dasselbe für Titel/Untertitel bei gesetztemSITE_TITLE. - Eine passwortgeschützte Subpage anlegen und prüfen, dass weder ihr Slug noch ihre Alben in der Sitemap auftauchen.
- JSON-LD im Rich-Results-Test von Google gegenprüfen; Browser-Konsole auf CSP-Fehler ansehen.
Commits: feat(seo): add a site URL setting, editable in the admin panel ·
feat(seo): add robots.txt, sitemap.xml and JSON-LD
Task B2 — 1:1-Zoom im Lightbox
Aufwand: S
Befund
Der Lightbox rendert current.previewUrl (components/Lightbox.tsx:237), während sein Dateikopf
„Full-resolution image display" verspricht (:5). imageUrl() steht per Default auf 'preview'
(lib/urls.ts:29), und keine Stelle in app/, components/ oder lib/urls.ts fordert
'original' an — die oberste Stufe aus lib/imageSize.ts ist im Frontend unerreichbar.
Als Default ist das richtig. Für Schärfebeurteilung fehlt der Weg dorthin.
Schritte
- B2.1 —
originalUrldurchreichenoriginalUrl?: stringinPhotoItem(app/[...path]/PhotoGrid.tsx:18-33) — optional, damitJournalStudio.tsx:616-617(Admin-Thumbnails über eine andere Route) unverändert bleibt.- Befüllen an den zwei öffentlichen Stellen:
app/[...path]/page.tsx:117-118undapp/journal/[slug]/page.tsx:179-180, jeweilsimageUrl(a.id, 'original'). - Fehlt
originalUrl, wird der Zoom-Knopf nicht gerendert. Kein Fallback auf Preview — ein Zoom, der auf ein Preview zoomt, ist genau die Verwechslung, die es zu beheben gilt.
- B2.2 — Zoom-Geometrie als reines Modul (
lib/zoom.ts)clampPan(offset, scale, viewport, natural)→ begrenzt den Bildausschnitt auf die Bildkanten.zoomAt(point, scale, offset, factor)→ zoomt auf den Cursor/Pinch-Mittelpunkt statt auf die Bildmitte. Das ist der Unterschied zwischen brauchbar und ärgerlich.- Pure Funktionen, keine DOM-Abhängigkeit — direkt unit-testbar.
- B2.3 — Verdrahtung im Lightbox
- Zustand:
zoomed(bool),scale,offset. Auslöser: Doppelklick/Doppeltipp, Pinch, Tastez; zusätzlich ein sichtbarer Knopf neben dem Info-Toggle. - Das Original wird erst bei dieser Geste geladen, nie vorher. Die bestehende
Nachbar-Vorladung (
Lightbox.tsx:96-105) bleibt aufpreviewUrlund wird nicht angefasst — sonst zieht sich jeder Besucher unbemerkt Originale. - Ladeindikator, solange das Original unterwegs ist; das Preview bleibt so lange skaliert stehen.
- Verlassen:
Esc(erste Stufe verlässt den Zoom, nicht den Lightbox), Doppelklick, Knopf. - Zurücksetzen bei jedem Bildwechsel — sonst hängt der Zoomfaktor des vorherigen Bildes am nächsten.
- Konflikte prüfen:
useSwipe(Pinch/Pan darf nicht als Wisch durchgehen) undAssetProtection(dragstartist global unterdrückt — Pan muss über Pointer-Events laufen, nicht über natives Dragging). - Tastatur:
+/-/0,aria-labelam Knopf, Zoomzustand überaria-pressed.
- Zustand:
- B2.4 — Kommentar korrigieren
components/Lightbox.tsx:5beschreibt eine Absicht, kein Verhalten. Auf „Preview-tier image, with opt-in 1:1 zoom to the original" ändern.
- B2.5 — Tests
lib/__tests__/zoom.test.ts: Clamping an allen vier Kanten, Zoom auf Punkt, Rückkehr aufscale: 1zentriert das Bild wieder. Erwartet: +10 bis +12 Tests.e2e/lightbox-zoom.spec.ts: Zoom öffnet, lädt eine?size=original-URL (Request abfangen), Pan bleibt in den Grenzen, Bildwechsel setzt zurück.
Verifikation: Netzwerk-Tab — beim Öffnen des Lightbox darf keine size=original-Anfrage
stehen, erst nach der Zoom-Geste. Auf einem Touchgerät gegenprüfen, dass Pinch nicht mehr als Wisch
zum nächsten Bild interpretiert wird.
Commit: feat(lightbox): add opt-in 1:1 zoom against the original asset
Task B1 — Sequenzierung wie im Fotobuch
Aufwand: M — der größte Eingriff hier, und der einzige mit UI-Anteil im Admin.
Befund
PhotoGrid bildet die Assets flach auf ein einziges .photo-grid-Element ab
(app/[...path]/PhotoGrid.tsx:132-243); das Layout gilt für alle gleich. assetOrder
(lib/config/schema.ts:224) bestimmt nur die Reihenfolge. Die Essay-Ansicht kennt fullbleed,
wide und Paare bereits (lib/journal.ts) — das Album nicht.
Designentscheidung: Hinweise leben in assetOrder
Eine zweite, parallele Liste (sequence:) würde die Reihenfolge doppelt führen und damit
zwangsläufig auseinanderlaufen. Stattdessen wird assetOrder erweitert — Sequenzierung setzt
ohnehin sort: manual voraus:
- 'album-uuid':
sort: manual
assetOrder:
- 'asset-opening': fullbleed # allein, volle Breite
- 'asset-a': pair # diese beiden nebeneinander,
- 'asset-b': pair # auf gleiche Höhe gebracht
- 'asset-c' # unverändert im normalen Raster
- break # bewusste Lücke
deriveGallery() erzeugt daraus zwei Strukturen: albumManualOrders bleibt exakt wie heute
eine reine UUID-Liste (break herausgefiltert), damit lib/albumSort.ts unverändert
weiterfunktioniert — und neu albumSequences: Record<albumId, SequenceEntry[]> mit den Hinweisen.
Die Invariante, an der dieses Feature scheitern kann
Der Lightbox-Index und die Permalinks #photo-N sind positional über displayedAssets
(app/[...path]/PhotoGrid.tsx:51-107, openLightbox(index)). Blockbildung darf deshalb
ausschließlich gruppieren, niemals umsortieren, nichts auslassen und nichts doppeln. Wird das
verletzt, öffnet ein Klick das falsche Foto und jeder geteilte Permalink zeigt woanders hin — ein
Fehler, der beim Entwickeln kaum auffällt und beim Kunden sofort.
Schritte
- B1.1 — Schema & Parser
SequenceHint = 'fullbleed' | 'pair' | 'break';assetOrder?: Array<string | Record<string, string> | 'break'>inAlbumEntryObject.- In
deriveGallery()(lib/config/index.ts:162-236, direkt neben dercoverPosition-Validierung): unbekannte Hinweise werfen mit nennender Meldung — dieselbe Linie wie beimsort-Wert, wo ein Tippfehler bewusst nicht still durchgeht. albumManualOrdersbleibt unverändert UUID-only;albumSequenceskommt dazu (beides inAppConfig,GalleryDerivationund den drei Rückgabestellen inlib/config/index.ts).- Ein einzelnes
pairohne Partner fällt auf normales Verhalten zurück, statt zu werfen — beim Umsortieren im Admin ist dieser Zustand ein normaler Zwischenschritt.
- B1.2 — Blockbildung als reines Modul (
lib/sequence.ts)buildSequenceBlocks(assets, hints)→Array<{ kind: 'grid' | 'pair' | 'fullbleed' | 'break', items: Array<{ asset, index }> }>.- Jedes Item trägt seinen ursprünglichen Index mit. Das ist die technische Absicherung der Invariante oben.
- Aufeinanderfolgende Assets ohne Hinweis landen in einem gemeinsamen
grid-Block, damit das bestehende Layout (Masonry, Justified …) darin unverändert greift.
- B1.3 — Rendering
PhotoGridrendert Blöcke statt einer flachen Liste;openLightbox(item.index)stattopenLightbox(index).- CSS:
.photo-sequence__pair(zwei Spalten, auf gleiche Höhe normiert über die vorhandenen Aspect-Ratios),.photo-sequence__fullbleed,.photo-sequence__break. Mobil kollabieren Paare auf eine Spalte. - Ohne Hinweise entsteht genau ein
grid-Block — das Markup bleibt dann identisch zu heute. Das ist die Rückfallgarantie und gehört als Test festgeschrieben.
- B1.4 — Admin
AssetOrderEditor.tsxbekommt pro Foto eine Hinweis-Auswahl und einen Trenner-Einfügen-Knopf.- Eine Andeutung der Blöcke in der Sortierliste (Paare zusammengefasst, Trenner als Linie). Ohne diese Vorschau ist das Feature im Admin unbenutzbar — das ist die halbe Arbeit an der Task, nicht das YAML-Schema.
- B1.5 — Tests
lib/__tests__/sequence.test.ts: Indizes bleiben lückenlos und in Reihenfolge (die Invariante, als expliziter Test über ein Album mit allen Hinweisarten); leere Hinweise → genau ein Block; verwaistespair;breakam Anfang und am Ende; Hinweis auf ein Asset, das gar nicht im Album ist.lib/__tests__/config-sequence.test.ts: Parser akzeptiert die neue Form, wirft bei unbekanntem Hinweis,albumManualOrdersbleibt UUID-only.- Erwartet: +20 bis +25 Tests.
e2e/sequence.spec.ts: Klick auf das dritte Foto in einem sequenzierten Album öffnet das dritte Foto;#photo-3landet auf demselben.
Verifikation: Ein Album mit fullbleed, einem Paar und einem break anlegen, im Browser
ansehen, jedes Foto anklicken und den Lightbox-Zähler gegen die visuelle Position prüfen. Ein Album
ohne Hinweise gegen git stash vergleichen: das Markup muss identisch sein.
Commit: feat(album): sequence photos into pairs, spreads and breaks (EXPERIMENTAL)
Task C1 — Ortsauflösung pro Album
Aufwand: S
Befund, präzise
Die Map ist besser gebaut als erwartet: Passwortgeschützte Alben werden vor der Aggregation
gefiltert (app/api/map/route.ts:63-90), 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 heraus
— die Map ist die einzige öffentliche GPS-Fläche.
Die Lücke betrifft öffentliche Alben: Die Marker-Position ist der Mittelwert der tatsächlichen
Koordinaten (app/api/map/route.ts:97-104). Bei einem Album von einem Ort — Garten, Studio,
Wohnung eines Kunden, empfindlicher Naturstandort — ist dieser Mittelwert genau dieser Ort. Der
Marker heißt nach der Stadt und steht auf dem Grundstück.
Zwei Entwurfsentscheidungen, die vorab feststehen müssen
1. Keine Geodaten-Abhängigkeit. Für „Stadtzentrum" bräuchte es einen Ortsdatensatz. Stattdessen
wird die Koordinate gerastert: city auf 0,05° (~5 km), country auf 1° (~110 km). Das ist
dependency-frei, erklärbar und ausreichend — 5 km trennen zuverlässig ein Grundstück von einem
Stadtgebiet.
2. Marker fassen mehrere Alben zusammen. Liegen an einer Stadt ein exact- und ein
city-Album, gibt es trotzdem nur einen Marker. Regel: die strengste Stufe unter den
beitragenden Alben gewinnt. Sicherheitsentscheidungen fallen in die vorsichtige Richtung, und die
Alternative — Marker aufspalten — würde über die Kartenansicht verraten, dass ein Album
zurückhaltender eingestellt ist.
Schritte
- C1.1 — Konfiguration
location?: 'exact' | 'city' | 'country' | 'hidden'inAlbumEntryObjectund inSubpageObjectValue/SubpageConfig.albumLocationModes: Record<string, LocationMode>inAppConfig, befüllt inderiveGallery()nebenalbumCoverPositions; unbekannter Wert wirft.- Präzedenz wie beim Grid: global (neu:
map.locationDefaultinsettings.yaml, Defaultexact= heutiges Verhalten) < Subpage < Album.
- C1.2 —
lib/mapService.ts- Alben mit
hiddenüberspringen, bevor gebucketet wird — das ist die einzige Stufe, die gar nicht erst in den Cache soll. locationModeaufMapAlbumEntrymitführen. Nicht hier schon runden: gerundete Werte aufsummieren und danach mitteln ergibt wieder ungerundete Koordinaten.
- Alben mit
- C1.3 —
app/api/map/route.ts- Strengste Stufe der sichtbaren Alben je Marker bestimmen.
- Mittelwert bilden wie heute, danach rastern:
city→ 0,05°,country→ 1°,exact→ unverändert. - Bei
countrydas Stadtlabel weglassen (die Popup-Überschrift wäre sonst genauer als die Position). - Ist ein Marker nach dem Ausschluss leer, entfällt er — der Pfad existiert bereits (
allowedAlbums.length === 0).
- C1.4 — Admin & Doku
- Auswahlfeld pro Album und pro Subpage im
PageBuilder, Standard inSettingsEditor. gallery.yaml.exampleundsettings.yaml.exampleergänzen, mit einem Satz dazu, warum es das gibt.
- Auswahlfeld pro Album und pro Subpage im
- C1.5 — Tests (
lib/__tests__/map-location.test.ts)- Rasterung: eine Koordinate auf einem Grundstück landet bei
citysichtbar daneben, bleibt aber in derselben Stadt;exactverändert nichts. - Strengste Stufe gewinnt (alle sechs Paarungen).
hiddenerscheint in keiner Ausgabe — auch dann nicht, wenn ein anderes Album dieselbe Stadt beisteuert.- Präzedenz global < Subpage < Album.
- Erwartet: +14 bis +18 Tests.
- Rasterung: eine Koordinate auf einem Grundstück landet bei
Verifikation: Ein Album mit GPS-Daten von einem einzigen Ort anlegen, /api/map einmal mit
exact und einmal mit city abrufen und die Koordinaten vergleichen; auf der Karte gegenprüfen,
dass der Marker die Stadt trifft, aber nicht mehr die Adresse. Zusätzlich sicherstellen, dass ein
hidden-Album auch nach einem Cache-Durchlauf nicht auftaucht (POST /api/admin/reload).
Commit: feat(map): make location precision configurable per album
Reihenfolge und Zusammenhänge
E (Auffindbarkeit) ── unabhängig, bringt die Site-URL, die später auch ein Feed bräuchte
B2 (Zoom) ── unabhängig, erste Stelle die Originale ausliefert
B1 (Sequenzierung) ── größter Eingriff, berührt die Lightbox-Index-Invariante
C1 (Ortsauflösung) ── unabhängig, Datenschutz
↑ B1 und C1 fassen beide lib/config/{schema,index}.ts an,
aber verschiedene Felder — nacheinander mergen, nicht parallel
Vier PRs, in dieser Reihenfolge. E und B2 sind jeweils an einem Tag abgeschlossen und liefern sofort etwas Sichtbares; B1 braucht am meisten Sorgfalt und sollte nicht unter Zeitdruck; C1 ist klein, aber sicherheitsrelevant und verdient einen eigenen, ruhigen Review.
Erwartete Testsumme am Ende: 443 → etwa 516.
Was dieser Plan bewusst nicht enthält
Aus dem Brainstorm bleiben offen und sind hier nicht eingeplant: A1 (Client-Links), A2 (Anfrage-Formular), A3 (Proofing-Notizen), B3 (Farbmanagement-Messung), C2 (terminierte Veröffentlichung) und D (RSS-Feed). Falls C2 und D später kommen: Beide brauchen dieselbe Sichtbarkeitsprüfung, die in E3 entsteht. Sie sollte dort so geschnitten werden, dass ein zweiter Aufrufer sie wiederverwenden kann, statt sie zu kopieren.