# Geschichten — Reader Journey

Übersichtsseite `/geschichten` und Story-Detailansicht `/geschichten/[id]`. Leserinnen entdecken veröffentlichte Familiengeschichten, filtern nach Personen und lesen die vollständige Geschichte mit verlinkten Dokumenten und Personen.

Familienarchiv
Final Spec
2026-05-02 · @leonievoss
R

## Reader Journey

Alle eingeloggten Familienmitglieder können veröffentlichte Geschichten lesen und nach historischen Personen filtern.

/geschichten · /geschichten/[id]
Konzept

Die Reader Journey umfasst zwei Seiten: die editoriale Übersichtsseite `/geschichten` und die Story-Detailansicht `/geschichten/[id]`. Beide Seiten sind für alle angemeldeten Familienmitglieder zugänglich, unabhängig von Schreibrechten.

Die Übersichtsseite zeigt alle veröffentlichten Geschichten als editoriale Liste mit vollbreiten Zeilen. Links steht die Metaspalte (Autorin, Datum, verknüpfte Person), rechts Titel und Vorschautext. Eine Personenfilter-Pill-Leiste oberhalb der Liste schränkt die Anzeige auf Geschichten ein, die eine bestimmte historische Person erwähnen.

Die Detailansicht zeigt die vollständige Geschichte als zentrierten Artikeltext (max-w-3xl) mit Autorinnenzeile, Fließtext (HTML aus dem Backend), einem Abschnitt mit verlinkten Personenchips und Dokumentreferenzkarten, die direkt zu den Originalbriefen und Dokumenten verlinken. Autorinnen mit dem Recht `BLOG_WRITE` sehen zusätzlich Bearbeiten- und Löschen-Aktionen.

Screens — Übersichtsseite

### R-1 — Übersicht /geschichten

US-BLOG-004 · R-1

Editoriale Listendarstellung aller veröffentlichten Geschichten. Jede Zeile: linke Metaspalte (Avatar, Autorin, Datum, Personenchip), rechte Inhaltsspalte (Serientitel, Vorschautext). Personenfilter-Pills oberhalb der Liste. Schaltfläche „+ Neue Geschichte" rechts oben nur für BLOG_WRITERs sichtbar.

Varianten: Kein Filter aktiv (alle Geschichten) · Person-Filter aktiv (gefilterte Liste) · Leerer Zustand (Filter aktiv, keine Ergebnisse)

Desktop — 1040px
Geschichten
Alle Franz Raddatz Emma Müller Heinrich Kohl + Person wählen
MR
Maria Raddatz
14. März 2025
FR Franz Raddatz
Der Sommer in Breslau
Oma erzählte oft vom letzten Sommer vor dem Krieg, als die Familie noch vollständig zusammen war und niemand ahnte, was kommen würde. Es waren die letzten unbeschwerten Monate auf dem Gut…
KR
Klaus Raddatz
2. Jan. 2025
FR Franz Raddatz
Wie Opa Franz den Hof rettete
Es war ein kalter November, als der Notar anklopfte. Opa hatte nur wenige Stunden Zeit, alle Unterlagen zusammenzusuchen. Die Briefe aus dieser Zeit zeugen von einem Mann, der unter enormem Druck standhaft blieb.
GK
Gertrud Koch
18. Okt. 2024
EM Emma Müller
Die Hochzeit im Krieg
1943, mitten im Chaos — Emma bestand darauf, dass das Fest stattfand. Sie hatte das Brautkleid seit Jahren aufgehoben. Ihr Bruder kam auf Fronturlaub, drei Tage nur, aber es reichte für die Feier.
HR
Hans Raddatz
5. Sept. 2024
Erinnerungen an Weihnachten 1935
Der Geruch von Tannenzweigen und Kerzenwachs — so beschrieb Opa immer den Heiligabend auf dem Gut. Die Kinder durften erst nach dem Abendessen ins Zimmer, und das Warten war jedes Jahr das Schwerste.
Mobile — 320px
9:41●●●
Geschichten
Alle Franz Raddatz Emma Müller + Person…
MR
Maria Raddatz 14. Mrz. 2025
Der Sommer in Breslau
Oma erzählte oft vom letzten Sommer vor dem Krieg, als die Familie noch vollständig zusammen war…
KR
Klaus Raddatz 2. Jan. 2025
Wie Opa Franz den Hof rettete
Es war ein kalter November, als der Notar anklopfte. Opa hatte nur wenige Stunden Zeit, alle Unterlagen zusammenzusuchen…
GK
Gertrud Koch 18. Okt. 2024
Die Hochzeit im Krieg
1943, mitten im Chaos — Emma bestand darauf, dass das Fest stattfand. Sie hatte das Brautkleid seit Jahren aufgehoben…
Leerer Zustand — Filter aktiv, keine Ergebnisse
Alle Franz Raddatz Emma Müller
Keine Geschichten für Franz Raddatz gefunden.

#### impl-ref — R-1 Übersicht /geschichten

Element | Wert | Hinweise
--- | --- | ---
Seitenstruktur
Page title | font-family:var(--font-display);font-size:24px;color:var(--navy) | Fraunces, nicht fett
Editorial list card | bg-white shadow-sm border border-brand-sand rounded-sm (implementiert: bg-surface border-line — semantische Tokens für Dark Mode) | wraps alle Zeilen
Listenzeile
List row | flex gap-0 border-b border-brand-sand last:border-0 hover:bg-surface | min-h-[44px] auf Mobile
Meta column | w-40 shrink-0 flex flex-col gap-1 p-3 border-r border-line-2 | feste Breite — breit genug für text-sm Namen ohne Umbruch
Author avatar | w-7 h-7 rounded-full text-[9px] font-bold text-white flex items-center justify-center | personAvatarColor(userId)
Author name | font-sans text-sm font-semibold text-ink |
Date | font-sans text-sm text-ink-3 | formatDate(publishedAt)
Person chip | inline-flex items-center gap-1 rounded-full bg-surface border border-line px-2 py-0.5 text-[10px] font-medium text-ink | links zu /persons/[id]; optional
Story title | font-serif text-lg text-ink leading-snug mb-1 hover:text-primary | link zu /geschichten/[id]
Excerpt | font-sans text-sm text-ink-3 line-clamp-2 | max. 150 Zeichen aus body
Filter
Filter pill (inaktiv) | rounded-full border border-line px-3 py-1 text-xs font-semibold text-ink-2 hover:bg-muted | aria-pressed="false"
Filter pill (aktiv) | rounded-full bg-primary text-primary-fg px-3 py-1 text-xs font-semibold | aria-pressed="true"
Person wählen | border-dashed text-ink-3 | öffnet Personen-Typeahead
Aktionen & Barrierefreiheit
Neue Geschichte btn | rounded border border-primary bg-primary text-primary-fg px-3 py-1.5 text-sm font-medium | nur BLOG_WRITE; auf Mobile ausgeblendet → Hamburger-Menü
Touch target (Mobile) | min-h-[44px] auf jeder Zeile | WCAG 2.2 AA
Leerer Zustand | py-12 text-center font-serif text-sm text-ink-3 italic | Zeigt Filtername im Text

Screens — Story-Detailansicht

### R-2 — Story-Detail /geschichten/[id]

US-BLOG-004 · R-2

Vollständige Leseansicht einer Geschichte. Zentrierter Artikel-Container (max-w-3xl). Zurück-Schaltfläche, großer Serientitel, Metazeile mit Avatar und Datum. Fließtext aus dem Backend (HTML). Personenchips-Sektion. Dokumentreferenzkarten. Autorinnen mit BLOG_WRITE sehen Bearbeiten/Löschen-Aktionen.

Varianten: Leserin ohne BLOG_WRITE (keine Aktionen) · BLOG_WRITER (Bearbeiten + Löschen sichtbar) · Mobile (Aktionen im … Menü)

Desktop — 1040px · BLOG_WRITER-Ansicht
Zurück zu Geschichten
Der Sommer in Breslau
MR
Maria Raddatz
veröffentlicht am 14. März 2025
Löschen

Oma erzählte oft vom letzten Sommer vor dem Krieg, als die Familie noch vollständig zusammen war. Es war 1938, und die Hitze in Breslau schien endloser als in all den Jahren davor. Die Kinder liefen barfuß über die Steinplatten des Innenhofs, und die Erwachsenen saßen bis spät in die Nacht auf der Veranda.

Es war 1938, und Franz hatte gerade seinen 40. Geburtstag gefeiert. Die Briefe aus dieser Zeit zeigen einen Mann voller Zuversicht — man kann kaum glauben, was nur wenige Jahre später folgen würde. Er schrieb an seinen Bruder Heinrich: „Der Sommer gehört uns, der Winter kommt früh genug."

Personen in dieser Geschichte
FR
Franz Raddatz
EM
Emma Müller
Erwähnte Dokumente
Brief vom 12. Juli 1938
12. Juli 1938 · von Franz Raddatz
Postkarte aus Breslau, August 1938
22. Aug. 1938 · von Franz Raddatz an Emma Müller
Mobile — 320px · Leserin ohne Schreibrecht
9:41●●●
Zurück zu Geschichten
Der Sommer in Breslau
MR
Maria Raddatz
14. März 2025
···

Oma erzählte oft vom letzten Sommer vor dem Krieg, als die Familie noch vollständig zusammen war. Es war 1938, und die Hitze in Breslau schien endloser als in all den Jahren davor.

Es war 1938, und Franz hatte gerade seinen 40. Geburtstag gefeiert. Die Briefe aus dieser Zeit zeigen einen Mann voller Zuversicht — man kann kaum glauben, was nur wenige Jahre später folgen würde.

Personen in dieser Geschichte
FR
Franz Raddatz
EM
Emma Müller
Erwähnte Dokumente
Brief vom 12. Juli 1938
12. Juli 1938 · von Franz Raddatz
Postkarte aus Breslau, Aug. 1938
22. Aug. 1938 · Franz an Emma

#### impl-ref — R-2 Story-Detail /geschichten/[id]

Element | Wert | Hinweise
--- | --- | ---
Artikel-Container
Article container | max-w-7xl mx-auto px-4 py-8; innere Lesespalte: max-w-3xl mx-auto | Seite so breit wie Dokumente/Personen; Textspalte bleibt lesbar zentriert
Article sheet | rounded-sm border border-line bg-sheet shadow-sm px-5 py-6 sm:px-10 sm:py-10 | Lesebogen-Panel zwischen Canvas und weißen Karten (Token --color-sheet); BackButton bleibt außerhalb
Story title | font-family:var(--font-display);font-size:clamp(22px,4vw,32px);color:var(--navy) | Fraunces, nicht fett
Back button | <BackButton /> aus $lib/components/BackButton.svelte | history.back(); nicht <a href>
Metazeile
Meta bar | flex items-center gap-3 mt-4 mb-6 |
Author avatar | w-8 h-8 rounded-full text-xs font-bold text-white flex items-center justify-center | personAvatarColor(author.id)
Published date | font-sans text-xs text-ink-3 | „veröffentlicht am " + formatDate()
Bearbeiten btn | rounded border border-line px-3 py-1.5 text-sm font-medium text-ink hover:bg-muted | nur BLOG_WRITE; auf Mobile im … Menü
Löschen link | text-sm text-red-600 font-medium hover:underline | nur BLOG_WRITE; Confirm-Dialog davor
Fließtext
Body text | font-serif text-base leading-relaxed text-ink prose | rendered HTML aus Backend; kein XSS (DOMPurify)
Personen & Dokumente
Person chips section | flex flex-wrap gap-2 mt-8 | Abschnittsüberschrift xs uppercase muted
Person chip | inline-flex items-center gap-2 rounded-full border border-line bg-surface px-3 py-1.5 text-sm font-medium | links zu /persons/[id]
Doc reference card | flex gap-3 items-start bg-white border border-brand-sand rounded-sm p-3 hover:shadow-sm | links zu /documents/[id]
Doc icon | w-9 h-9 bg-surface rounded flex items-center justify-center shrink-0 | Dateisymbol SVG
Mobile
… Menü (Mobile) | ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen (implementiert: Bearbeiten/Löschen bleiben inline in der Metazeile auf allen Breiten — kein BottomSheet) | BLOG_WRITE-Aktionen auf Mobile
Person chips (Mobile) | flex-wrap, volle Breite | kein horizontales Scrollen
Doc cards (Mobile) | flex-col gap-2 | stapeln vertikal

## Implementation Guide — Reader Journey

### Views & Routen

View | Route | Component | Datenquelle
--- | --- | --- | ---
Index | /geschichten | GeschichtenIndex.svelte | GET /api/geschichten?status=PUBLISHED
Detail | /geschichten/[id] | GeschichteDetail.svelte | GET /api/geschichten/{id}

### Datenmodell je Story (Listenitem)

- `id` — UUID
- `title` — string
- `body` — string (HTML, serverseitig bereinigt mit DOMPurify vor dem Rendern)
- `status` — `DRAFT | PUBLISHED`
- `author` — AppUser (id, firstName, lastName)
- `persons` — Person[] (id, firstName, lastName) — für Chips & Filter
- `documents` — Document[] (id, title, documentDate, sender) — für Referenzkarten
- `publishedAt` — ISO-8601-Timestamp

### Wiederverwendbare Komponenten & Utilities

- `PersonChipRow` — bestehende Komponente aus `$lib/components/`; Props: `persons: Person[]`
- `personAvatarColor(userId)` — bestehende Utility; gibt Tailwind-Bg-Klasse zurück
- `formatDate(iso)` — bestehende Utility; gibt „14. März 2025" zurück
- `BackButton` — `$lib/components/BackButton.svelte`; ruft `history.back()` auf

### Personenfilter — Implementierungshinweise

- Filter-State in der URL als Query-Parameter: `?person=UUID`. So ist der gefilterte Zustand direkt verlinkbar und bookmarkbar.
- Server-side Load-Funktion übergibt `?personId=UUID` an die API. Die API filtert auf Stories, die diese Person in `persons[]` enthalten.
- Filter-Pills werden aus dem aggregierten Set aller Personen aus geladenen Stories gebaut — nicht als eigener API-Aufruf.
- „+ Person wählen" öffnet ein Inline-Dropdown (bestehende `PersonTypeahead`-Komponente), fügt die Person zu den Pills hinzu und navigiert mit `?person=UUID`.

### Berechtigung BLOG_WRITE

- Die Schaltflächen „+ Neue Geschichte", „Bearbeiten" und „Löschen" werden nur gerendert, wenn `currentUser.permissions.includes('BLOG_WRITE')` wahr ist.
- Nicht nur ausblenden — Backend-Endpunkte für Schreib-/Löschoperationen sind ebenfalls durch `@RequirePermission(Permission.BLOG_WRITE)` geschützt.
- Auf Mobile werden Bearbeiten/Löschen aus dem Layout entfernt und erscheinen in einem BottomSheet, das über das ··· Menü in der Metazeile geöffnet wird. (implementiert: Aktionen bleiben inline in der Metazeile — h-11 Touch-Targets, kein BottomSheet)

### Barrierefreiheit

- Alle Story-Titellinks tragen beschreibenden Text (Titel der Geschichte, kein „hier klicken").
- Personenfilter-Pills verwenden `aria-pressed="true|false"`.
- Touch-Targets auf Mobile ≥ 44px (`min-h-[44px]` auf jeder Listenzeile).
- Fokusring auf allen interaktiven Elementen (Tailwind `focus-visible:ring-2 focus-visible:ring-primary`).
- Löschen-Aktion zeigt einen Bestätigungs-Dialog (`window.confirm` oder Modal) vor dem API-Aufruf.

### Routing & Navigation

- Jede Listenzeile ist eine `<a href="/geschichten/{id}">` — kein JavaScript-onClick nötig.
- Zurück-Schaltfläche auf der Detailansicht nutzt `BackButton`-Komponente (history.back()), nicht einen festen Href.
- Personenchips auf der Detailansicht verlinken zu `/persons/{id}`.
- Dokumentreferenzkarten verlinken zu `/documents/{id}`.