⚙️ 🤖 SPA-Navigation: Player überlebt Seitenwechsel (nahtloses Weiterspielen, Ansatz B) #58

Closed
opened 2026-07-01 21:30:32 +02:00 by holm · 4 comments
Owner
Dimension Bewertung Einschaetzung
Aufwand ███████░░░ Hoch — JS-Router + History + Content-Swap + Re-Init
Nutzen ████████░░ Hoch — nahtloses Weiterspielen, Grunderwartung Podcast-Player
Bruchhaeufigkeit ██████░░░░ Mittel-Hoch — jeder Seitenwechsel betroffen
Nachhaltigkeit ████████░░ Hoch — loest #1 (Stocken) + #2 (Cross-Episode) strukturell
Dringlichkeit ███████░░░ Hoch — Holm-Entscheidung, Ansatz A reicht nicht

Ziel

Nahtloses Weiterspielen ueber Seitenwechsel. Ansatz A (statische Multi-Page + sessionStorage) kann das prinzipiell nicht — bei jedem Full-Page-Load stirbt das <audio> + Autoplay ist geblockt. Holm-Entscheidung: Ansatz B (SPA-Navigation). Loest zugleich #53 (nahtlose Persistenz) und macht den Cross-Episode-Bug (#2, sessionStorage-Rehydrate-Handler) obsolet.

Architektur — JS-Enhancement ueber statischer Basis

Kein No-JS-Player-Fallback noetig (Holm-Vorgabe): Fuer den rohen/statischen Zugang gibt es den Download-Button. Die Seiten sind statisch generierte .html mit echten <a href>-Links — Deep-Links, SEO und Direktaufruf funktionieren dadurch automatisch, ohne Extra-Aufwand. Ohne JS: normale Full-Page-Navigation, KEIN Player (Download deckt den Audio-Zugang ab). Player + nahtlose Navigation sind bewusst reines JS-Enhancement — kein Aufwand fuer No-JS-Player-UX. Mit JS: Klick-Interception + Content-Swap.

  1. Player-Shell: Sticky-Footer-Player + <audio> + Header liegen im <body> AUSSERHALB des geswappten Bereichs (bereits so: Sticky am Body-Ende in base.html.j2). Nur <main> wird ausgetauscht → Player-Element bleibt am Leben, Audio laeuft durch.
  2. Klick-Interception: globales Shell-Script faengt Klicks auf interne Links (gleiche Origin, Projekt-.html) ab: preventDefaultfetch(href) → aus der Antwort das neue <main> extrahieren → aktuelles <main> ersetzen → document.title + og/meta updaten → history.pushState.
  3. popstate (Back/Forward): fetch + Swap zur History-URL.
  4. Re-Init nach Swap: seitenspezifische Handler (Kapitel play-from, folge abspielen) fuer den NEUEN <main>-Inhalt neu verdrahten. Ein idempotentes initPage(root) statt einmaligem DOMContentLoaded. Episode-Daten (slug/src/title) aus data-Attributen im geswappten Content lesen (z.B. data-ep-src/-title/-slug am play-btn oder <main>).
  5. Player-Steuerung: folge abspielen + Kapitel steuern den durchlaufenden Sticky-Player direkt (src wechseln nur wenn andere Folge; sonst nur play/seek). Bei Wechsel auf andere Folge: src neu + currentTime=0 (sauber, da kein Rehydrate-Handler mehr).
  6. Kein sessionStorage-Rehydrate-Weiterspielen mehr noetig (Player lebt durch). sessionStorage darf als Deep-Link-Restore bleiben (Direktaufruf einer Episodenseite), aber OHNE den #2-Handler-Bug — am einfachsten: Restore-currentTime frisch aus State lesen, nicht aus Closure.
  7. Barrierefreiheit/UX: Fokus-Management nach Swap (Fokus auf neue Ueberschrift/<main>), aria-live optional; kein Scroll-Sprung-Bug.

Verify-PFLICHT (Worker + v00)

Real testen (CDP/Browser), nicht statisch:

  • Folge A spielen → Link zu Folge B klicken → Audio laeuft OHNE Unterbrechung weiter bis man aktiv wechselt.
  • Wechsel auf Folge B + folge abspielen → B ab 0 (NICHT A's Position auf B — #2 darf nicht wiederkommen).
  • Back-Button → korrekte Seite, Player laeuft weiter.
  • Ohne JS: Links laden normal (statische .html), Download-Button vorhanden — kein Player erwartet.
  • Deep-Link direkt auf Episodenseite: Player sinnvoll (kein falscher Resume).

angelegt von Claude v00 (API/Token holm)

| Dimension | Bewertung | Einschaetzung | |---|---|---| | Aufwand | `███████░░░` | Hoch — JS-Router + History + Content-Swap + Re-Init | | Nutzen | `████████░░` | Hoch — nahtloses Weiterspielen, Grunderwartung Podcast-Player | | Bruchhaeufigkeit | `██████░░░░` | Mittel-Hoch — jeder Seitenwechsel betroffen | | Nachhaltigkeit | `████████░░` | Hoch — loest #1 (Stocken) + #2 (Cross-Episode) strukturell | | Dringlichkeit | `███████░░░` | Hoch — Holm-Entscheidung, Ansatz A reicht nicht | ## Ziel Nahtloses Weiterspielen ueber Seitenwechsel. Ansatz A (statische Multi-Page + sessionStorage) kann das prinzipiell nicht — bei jedem Full-Page-Load stirbt das `<audio>` + Autoplay ist geblockt. Holm-Entscheidung: **Ansatz B (SPA-Navigation)**. Loest zugleich [#53](../issues/53) (nahtlose Persistenz) und macht den Cross-Episode-Bug (#2, sessionStorage-Rehydrate-Handler) obsolet. ## Architektur — JS-Enhancement ueber statischer Basis **Kein No-JS-Player-Fallback noetig** (Holm-Vorgabe): Fuer den rohen/statischen Zugang gibt es den **Download-Button**. Die Seiten sind statisch generierte `.html` mit echten `<a href>`-Links — Deep-Links, SEO und Direktaufruf funktionieren dadurch automatisch, ohne Extra-Aufwand. Ohne JS: normale Full-Page-Navigation, KEIN Player (Download deckt den Audio-Zugang ab). Player + nahtlose Navigation sind bewusst reines JS-Enhancement — kein Aufwand fuer No-JS-Player-UX. Mit JS: Klick-Interception + Content-Swap. 1. **Player-Shell:** Sticky-Footer-Player + `<audio>` + Header liegen im `<body>` AUSSERHALB des geswappten Bereichs (bereits so: Sticky am Body-Ende in base.html.j2). Nur `<main>` wird ausgetauscht → Player-Element bleibt am Leben, Audio laeuft durch. 2. **Klick-Interception:** globales Shell-Script faengt Klicks auf interne Links (gleiche Origin, Projekt-`.html`) ab: `preventDefault` → `fetch(href)` → aus der Antwort das neue `<main>` extrahieren → aktuelles `<main>` ersetzen → `document.title` + og/meta updaten → `history.pushState`. 3. **popstate** (Back/Forward): fetch + Swap zur History-URL. 4. **Re-Init nach Swap:** seitenspezifische Handler (Kapitel `play-from`, `folge abspielen`) fuer den NEUEN `<main>`-Inhalt neu verdrahten. Ein idempotentes `initPage(root)` statt einmaligem `DOMContentLoaded`. Episode-Daten (slug/src/title) aus data-Attributen im geswappten Content lesen (z.B. `data-ep-src/-title/-slug` am play-btn oder `<main>`). 5. **Player-Steuerung:** `folge abspielen` + Kapitel steuern den durchlaufenden Sticky-Player direkt (src wechseln nur wenn andere Folge; sonst nur play/seek). Bei Wechsel auf andere Folge: src neu + currentTime=0 (sauber, da kein Rehydrate-Handler mehr). 6. **Kein sessionStorage-Rehydrate-Weiterspielen mehr noetig** (Player lebt durch). sessionStorage darf als Deep-Link-Restore bleiben (Direktaufruf einer Episodenseite), aber OHNE den #2-Handler-Bug — am einfachsten: Restore-currentTime frisch aus State lesen, nicht aus Closure. 7. **Barrierefreiheit/UX:** Fokus-Management nach Swap (Fokus auf neue Ueberschrift/`<main>`), `aria-live` optional; kein Scroll-Sprung-Bug. ## Verify-PFLICHT (Worker + v00) Real testen (CDP/Browser), nicht statisch: - Folge A spielen → Link zu Folge B klicken → **Audio laeuft OHNE Unterbrechung weiter** bis man aktiv wechselt. - Wechsel auf Folge B + `folge abspielen` → B ab 0 (NICHT A's Position auf B — #2 darf nicht wiederkommen). - Back-Button → korrekte Seite, Player laeuft weiter. - Ohne JS: Links laden normal (statische `.html`), Download-Button vorhanden — kein Player erwartet. - Deep-Link direkt auf Episodenseite: Player sinnvoll (kein falscher Resume). > angelegt von Claude v00 (API/Token holm)
Author
Owner

v00-Abnahme #58 — SPA nahtlos, ein Rest (bleibt offen)

CDP-verifiziert (echte Link-Klicks + Marker-Test, kein Page.navigate):

Funktioniert:

  • Episode-Card-Links (.html): SPA-Swap nahtloswindow-Marker ueberlebt (kein Reload), <main> getauscht, Player laeuft durch. Folgenwechsel + folge abspielen sauber (B ab 0, kein Cross-Episode-Bug). Damit sind das Stocken (#1) und die falsche Position (#2) zwischen Folgen geloest.
  • Seekbar-Hit-Area (#59): 18px. Live-Stream/Chat weiter im Player.

Rest-Bug:

  • Der Link „zurueck zur Uebersicht" (..//) macht einen Full-Page-Reload statt SPA-Swap (Marker WEG). Audio stockt nur beim Zurueckgehen zur Uebersicht.
  • Ursache: SPA-Router-Filter (base.html.j2, gerendert index.html:798) if (!/\.html$/.test(url.pathname)) return false; — der Index-Link endet auf /, nicht .html, wird also nicht als SPA-Ziel erkannt.
  • Fix laeuft (fix/58b-index-link-spa): Filter um Verzeichnis-Index (/$) erweitern bzw. navigateTo('/') das index-<main> fetchen lassen.

Bleibt offen bis der Index-Link ebenfalls per SPA laeuft.

angelegt von Claude v00 (API/Token holm)

## v00-Abnahme #58 — SPA nahtlos, ein Rest (bleibt offen) CDP-verifiziert (echte Link-Klicks + Marker-Test, kein Page.navigate): **Funktioniert:** - Episode-Card-Links (`.html`): **SPA-Swap nahtlos** — `window`-Marker ueberlebt (kein Reload), `<main>` getauscht, Player laeuft durch. Folgenwechsel + `folge abspielen` sauber (B ab 0, kein Cross-Episode-Bug). Damit sind das Stocken (#1) und die falsche Position (#2) zwischen Folgen geloest. - Seekbar-Hit-Area (#59): 18px. Live-Stream/Chat weiter im Player. **Rest-Bug:** - Der Link „zurueck zur Uebersicht" (`../` → `/`) macht einen **Full-Page-Reload statt SPA-Swap** (Marker WEG). Audio stockt nur beim Zurueckgehen zur Uebersicht. - Ursache: SPA-Router-Filter (base.html.j2, gerendert index.html:798) `if (!/\.html$/.test(url.pathname)) return false;` — der Index-Link endet auf `/`, nicht `.html`, wird also nicht als SPA-Ziel erkannt. - Fix laeuft (`fix/58b-index-link-spa`): Filter um Verzeichnis-Index (`/$`) erweitern bzw. `navigateTo('/')` das index-`<main>` fetchen lassen. Bleibt offen bis der Index-Link ebenfalls per SPA laeuft. > angelegt von Claude v00 (API/Token holm)
Author
Owner

SPA-Navigation gemerged — Player überlebt Seitenwechsel nahtlos (Ansatz B)

Gemerged nach dev (28ec849, Branch feat/58-spa-player).

Architektur:

  • Player-Shell (Sticky-Player + <audio> + Header) außerhalb <main#main-content> — nur <main> wird geswappt, Audio läuft durch.
  • Klick-Interception interner .html-Links → fetch() + DOMParser → <main>-Ersatz + document.title/OG-Meta-Update + history.pushState; popstate für Back/Forward (Fehler fallen sauber auf echte Navigation zurück).
  • Idempotentes initPage(root, isInitialLoad): Play/Kapitel via einmaliger Event-Delegation an document (keine Doppel-Handler); Episode-Daten via data-ep-src/-title/-slug. SPA-Swap lädt NICHT automatisch (würde laufende Wiedergabe abwürgen) — nur echter Doc-Load/Deep-Link bereitet vor.
  • <script> im Fragment (Commento) via reviveScripts() nach Swap neu ausgeführt.
  • Kein No-JS-Player-Fallback (Holm): Links bleiben echte .html (SEO/Deep-Link), Download-Button deckt statisch.

ECHTER CDP-Test (Headless-Chromium, node-Static-Server, Trusted-Input) — 14/14 grün:

  • Folge A spielt → Nav zu B → src bleibt A, paused:false, currentTime läuft über den Swap ohne Reset (0.16→0.99s).
  • B folge abspielensrc→B, currentTime:0 (NICHT A's Position — #2 kommt nicht wieder).
  • history.back() → korrekte Seite, Player läuft weiter (2.10s).
  • Deep-Link auf B während sessionStorage A führt → kein Fremd-Resume.
  • Kapitelklick lädt+seekt+spielt; Commento-Script nach Swap ausgeführt.

Für v00-CDP-Abnahme: dasselbe Szenario + Stream/Chat (temporär [stream]+chat_url in Demo). Hinweise: (a) 2. history.back() über echten Full-Load-Eintrag triggert Chrome-BFCache (Browser-Mechanik, kein Router-Bug); (b) updateMeta deckt nicht og:audio/ld+json (Crawler-irrelevant); (c) .zt-mainmain#main-content flex verschoben (Pixel-Gegencheck Index/Episode).

Grün → #58 + #59 + #53 (Dach) schließbar.

🤖 angelegt von Claude o00 (API/Token holm)

## SPA-Navigation gemerged — Player überlebt Seitenwechsel nahtlos (Ansatz B) Gemerged nach `dev` (`28ec849`, Branch `feat/58-spa-player`). **Architektur:** - Player-Shell (Sticky-Player + `<audio>` + Header) außerhalb `<main#main-content>` — nur `<main>` wird geswappt, Audio läuft durch. - Klick-Interception interner `.html`-Links → `fetch()` + DOMParser → `<main>`-Ersatz + `document.title`/OG-Meta-Update + `history.pushState`; `popstate` für Back/Forward (Fehler fallen sauber auf echte Navigation zurück). - Idempotentes `initPage(root, isInitialLoad)`: Play/Kapitel via **einmaliger Event-Delegation an `document`** (keine Doppel-Handler); Episode-Daten via `data-ep-src/-title/-slug`. SPA-Swap lädt NICHT automatisch (würde laufende Wiedergabe abwürgen) — nur echter Doc-Load/Deep-Link bereitet vor. - `<script>` im Fragment (Commento) via `reviveScripts()` nach Swap neu ausgeführt. - Kein No-JS-Player-Fallback (Holm): Links bleiben echte `.html` (SEO/Deep-Link), Download-Button deckt statisch. **ECHTER CDP-Test (Headless-Chromium, node-Static-Server, Trusted-Input) — 14/14 grün:** - Folge A spielt → Nav zu B → `src` bleibt A, `paused:false`, `currentTime` läuft **über den Swap ohne Reset** (0.16→0.99s). - B `folge abspielen` → `src`→B, `currentTime:0` (NICHT A's Position — #2 kommt nicht wieder). - `history.back()` → korrekte Seite, Player läuft weiter (2.10s). - Deep-Link auf B während sessionStorage A führt → kein Fremd-Resume. - Kapitelklick lädt+seekt+spielt; Commento-Script nach Swap ausgeführt. **Für v00-CDP-Abnahme:** dasselbe Szenario + Stream/Chat (temporär `[stream]`+`chat_url` in Demo). Hinweise: (a) 2. `history.back()` über echten Full-Load-Eintrag triggert Chrome-BFCache (Browser-Mechanik, kein Router-Bug); (b) `updateMeta` deckt nicht og:audio/ld+json (Crawler-irrelevant); (c) `.zt-main`→`main#main-content` flex verschoben (Pixel-Gegencheck Index/Episode). Grün → #58 + #59 + #53 (Dach) schließbar. > 🤖 angelegt von Claude o00 (API/Token holm)
Author
Owner

Gemerged nach dev (d3e82a7, Branch fix/58b-index-link-spa).

Root Cause: Der Router (isInternalNavLink, base.html.j2) erkannte SPA-Ziele nur per /\.html$/. „zurück zur Übersicht" + Logo zeigen auf base_href (..//./) → nach new URL() endet der Pfad auf / → fiel durch → Full-Reload.

Fix (Ein-Zeiler): if (!/\.html$/.test(url.pathname) && !/\/$/.test(url.pathname)) return false;/-Verzeichnis-Ziele zählen jetzt als SPA. swapMain() extrahiert main#main-content generisch (auch aus index.html) → kein Template-Change nötig. Origin-/Download-/Anker-Checks unverändert davor.

ECHTER CDP-Test (Headless-Chromium, HTTP-Server, Trusted-Input) — grün:

  • window.__marker überlebt Klick auf „zurück zur Übersicht" ('alive', kein Reload) ✓
  • audio.paused:false, currentTime nahtlos 1.43→2.40→3.60s (kein Reset) ✓
  • <main> nach Swap = Index-Grid (gridCount:6), URL/Title=Index ✓
  • Von geswapptem Index erneut Episode-Card → weiter nahtlos ✓
  • Stream+Chat nach Index-Swap intakt (kind:stream erhalten, Chat-iframe-src erhalten, Toggles funktional) ✓

Bereit zur finalen v00-CDP-Abnahme → grün: #58 + #53 (Dach) schließbar.

🤖 angelegt von Claude o00 (API/Token holm)

## P07 gemerged — Index-Link ist jetzt SPA (kein Reload mehr) Gemerged nach `dev` (`d3e82a7`, Branch `fix/58b-index-link-spa`). **Root Cause:** Der Router (`isInternalNavLink`, base.html.j2) erkannte SPA-Ziele nur per `/\.html$/`. „zurück zur Übersicht" + Logo zeigen auf `base_href` (`../`/`./`) → nach `new URL()` endet der Pfad auf `/` → fiel durch → Full-Reload. **Fix (Ein-Zeiler):** `if (!/\.html$/.test(url.pathname) && !/\/$/.test(url.pathname)) return false;` — `/`-Verzeichnis-Ziele zählen jetzt als SPA. `swapMain()` extrahiert `main#main-content` generisch (auch aus index.html) → kein Template-Change nötig. Origin-/Download-/Anker-Checks unverändert davor. **ECHTER CDP-Test (Headless-Chromium, HTTP-Server, Trusted-Input) — grün:** - `window.__marker` überlebt Klick auf „zurück zur Übersicht" (`'alive'`, kein Reload) ✓ - `audio.paused:false`, `currentTime` nahtlos 1.43→2.40→3.60s (kein Reset) ✓ - `<main>` nach Swap = Index-Grid (`gridCount:6`), URL/Title=Index ✓ - Von geswapptem Index erneut Episode-Card → weiter nahtlos ✓ - Stream+Chat nach Index-Swap intakt (`kind:stream` erhalten, Chat-iframe-src erhalten, Toggles funktional) ✓ **Bereit zur finalen v00-CDP-Abnahme** → grün: #58 + #53 (Dach) schließbar. > 🤖 angelegt von Claude o00 (API/Token holm)
Author
Owner

v00-Abnahme #58 — PASS (SPA vollständig, laufzeit-bewiesen)

Nach fix/58b (Index-Link als SPA-Ziel) CDP-verifiziert am Stand d3e82a7 (echte Link-Klicks + window-Marker, kein Page.navigate):

Navigation Marker Ergebnis
Episode-Card (.html) YES SPA-Swap, Player laeuft durch
„zurueck zur Uebersicht" (../) YES kein Reload mehr (war der Fix), Audio nahtlos
Back-Button YES korrekte Seite, Audio laeuft
Folgenwechsel + folge abspielen src wechselt, ab 0 (kein Cross-Episode)

marker=YES durchgehend = keine interne Navigation macht mehr einen Reload → echtes nahtloses Weiterspielen. #1 (Stocken) + #2 (Cross-Episode-Position) vollstaendig geloest.

Regression-Check gruen: Live-Stream (seek_disabled/times=live/kind=stream) + Chat (iframe lazy-load kiwiirc) funktionieren nach dem SPA-Umbau weiter. Seekbar (#59) 18px.

Kein Blocker. Geschlossen durch v00.

angelegt von Claude v00 (API/Token holm)

## v00-Abnahme #58 — PASS (SPA vollständig, laufzeit-bewiesen) Nach `fix/58b` (Index-Link als SPA-Ziel) CDP-verifiziert am Stand `d3e82a7` (echte Link-Klicks + `window`-Marker, kein Page.navigate): | Navigation | Marker | Ergebnis | |---|---|---| | Episode-Card (`.html`) | YES | SPA-Swap, Player laeuft durch | | „zurueck zur Uebersicht" (`../`) | **YES** | **kein Reload mehr** (war der Fix), Audio nahtlos | | Back-Button | YES | korrekte Seite, Audio laeuft | | Folgenwechsel + `folge abspielen` | — | src wechselt, ab 0 (kein Cross-Episode) | `marker=YES` durchgehend = keine interne Navigation macht mehr einen Reload → echtes nahtloses Weiterspielen. **#1 (Stocken) + #2 (Cross-Episode-Position) vollstaendig geloest.** Regression-Check gruen: Live-Stream (`seek_disabled`/`times=live`/`kind=stream`) + Chat (iframe lazy-load `kiwiirc`) funktionieren nach dem SPA-Umbau weiter. Seekbar (#59) 18px. Kein Blocker. Geschlossen durch v00. > angelegt von Claude v00 (API/Token holm)
holm 2026-07-01 23:18:05 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Zentonic/zentonic-publisher#58
No description provided.