From 48d05f8d2df8c832368a9df826e33dba289ef21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Tue, 21 Apr 2026 17:05:43 +0200 Subject: [PATCH] docs: spec-review-eingearbeitet (HTML-render, Felder, Migration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nachschärfungen nach Review: - content_html als primary im Snapshot-JSON (marked + DOMPurify + highlight.js, gemeinsame Policy in shared/markdown-policy.ts) - content_markdown bleibt daneben (Debug + alternative Renderer) - translations[] um title ergänzt (SPA-Switcher ohne Relay-Fetch) - published_at vs. created_at semantisch klar getrennt (OG vs. Update) - cover_image.fallback_url mit dokumentiertem Nutzungsszenario - Fallback-Politik für fehlende summary/image/published_at - --allow-shrink-Flag und kind:5-gestützte automatische Override - Upload-Reihenfolge für Hash-benannte Bundles (Assets → HTML → Delete) - /tag//-Verhalten in Nicht-Zielen erwähnt - Edge-Case „Repo-Post + alle Relay-Events gelöscht" in Fehlertabelle - Migrations-Weg um shared/markdown-policy.ts als Schritt 1 erweitert - pro Migrations-Schritt explizite Rollback-Strategie Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-21-prerender-snapshot-design.md | 131 +++++++++++++++--- 1 file changed, 108 insertions(+), 23 deletions(-) diff --git a/docs/superpowers/specs/2026-04-21-prerender-snapshot-design.md b/docs/superpowers/specs/2026-04-21-prerender-snapshot-design.md index 985d114..92cfff1 100644 --- a/docs/superpowers/specs/2026-04-21-prerender-snapshot-design.md +++ b/docs/superpowers/specs/2026-04-21-prerender-snapshot-design.md @@ -51,9 +51,11 @@ Laufzeit-Funktionen (Sprach-Switcher, Navigation, Reply-Loader). HTTP-Request. - **Keine Edge-Function, kein VPS, kein PHP-Shim.** Lösung funktioniert auf jedem Static-Hoster. -- **Kein Prerender für Listen-Seiten** (Homepage, Archiv) in dieser - Iteration. Sie bleiben SPA-gerendert. Geteilt werden Artikel, nicht - Listen. +- **Kein Prerender für Listen-Seiten** (Homepage, Archiv, `/tag//`) + in dieser Iteration. Sie bleiben SPA-gerendert über den + `adapter-static`-`fallback: 'index.html'`-Mechanismus (Crawler auf + `/tag/nostr/` → bekommen `index.html`, Seite rendert nach Hydration). + Geteilt werden Artikel, nicht Listen. - **Keine Änderung am Publish-Flow.** `publish`-Pipeline bleibt exakt wie heute (Git-MD → Nostr-Event). @@ -108,6 +110,8 @@ Neues Deno-Modul. Verzeichnis: `snapshot/` als Geschwister zu `publish/`. - `--min-events ` (Plausibilitätsschwelle, absolute Zahl; ohne Flag: Last-known-good-Count aus Cache minus 2; ohne Cache: `1`) - `--cache ` (default: `/.last-snapshot.json`) +- `--allow-shrink` (Override des Drop-Checks, für Fälle in denen bewusst + massiv gelöscht wurde und kein `kind:5` als Signal existiert) **Algorithmus:** @@ -122,14 +126,38 @@ Neues Deno-Modul. Verzeichnis: `snapshot/` als Geschwister zu `publish/`. 5. **Plausibilitätscheck:** - mindestens `ceil(N × 0.6)` der N Read-Relays müssen geantwortet haben (bei 5 Relays: 3, bei 3 Relays: 2) → sonst Hard-Fail - - Event-Count ≥ `--min-events` → sonst Hard-Fail - - Event-Count-Drop > 20 % gegenüber Cache → Hard-Fail + - Event-Count ≥ `--min-events` → sonst Hard-Fail. Beim allerersten + Lauf ohne Cache und ohne explizites Flag ist die Default-Schwelle `1` + (d.h. mindestens ein Event muss vorhanden sein) — der Drop-Check + greift erst beim zweiten Lauf. + - Event-Count-Drop > 20 % gegenüber Cache → Hard-Fail, **außer**: + - seit letztem Snapshot neue `kind:5`-Deletions von genau so vielen + Events wurden erkannt (Drop ist bewusst) → Check wird übersprungen + - `--allow-shrink` ist gesetzt → Check wird übersprungen 6. **Cover-Bild-Probe.** HEAD-Request auf `og:image`-Kandidat. Bei 200: als `url` schreiben. Bei Fehler: Fallback-Blossom prüfen, als `url` schreiben wenn verfügbar. Beide tot: primäre URL trotzdem schreiben + Warnung loggen (Blossom ist content-addressed, URL wird später wieder erreichbar sein). -7. **JSON-Output schreiben.** +7. **Markdown-zu-HTML-Rendering.** Body des Events wird mit `marked` + gerendert, dann mit `DOMPurify` gemäß gemeinsamer Policy sanitized, + dann Code-Blöcke mit `highlight.js` hervorgehoben. Gemeinsame + Konfiguration (Allowlist, Syntax-Sprachen) liegt als Konstanten-Modul + in `shared/markdown-policy.ts` und wird von Snapshot **und** SPA + identisch importiert. Ergebnis wird als `content_html` ins JSON + geschrieben. Das rohe `content_markdown` bleibt ebenfalls im JSON + (Debuggability, alternative Renderer, die der HTML-Sanitization nicht + trauen). +8. **Fallback-Politik für fehlende Felder:** + - fehlt `summary` im Event → aus `content_markdown` die ersten 200 + Zeichen (Whitespace normalisiert, abgeschnitten an Wortgrenze, + Suffix `…`) extrahieren und als `summary` schreiben + - fehlt `image` im Event → `cover_image` ist `null`; der Prerender + nutzt ein Site-Default-OG-Bild (definiert in `app/static/`, z.B. + Profilbild oder Logo-Banner, als `og:image` bei null-Cover) + - fehlt `published_at`-Tag → `created_at` wird als + `published_at` übernommen +9. **JSON-Output schreiben.** **Output-Format:** @@ -170,12 +198,13 @@ Neues Deno-Modul. Verzeichnis: `snapshot/` als Geschwister zu `publish/`. "alt": "Alt-Text", "mime": "image/jpeg" }, - "content_markdown": "…full markdown…", + "content_html": "

…sanitized HTML with highlighted code blocks…

", + "content_markdown": "…full markdown, raw, for debugging or alternative renderers…", "tags": ["Nostr", "Bibel"], "naddr": "naddr1...", "habla_url": "https://habla.news/a/naddr1...", "translations": [ - { "lang": "en", "slug": "bible-selfies" } + { "lang": "en", "slug": "bible-selfies", "title": "Bible-Selfies" } ] } ``` @@ -184,8 +213,27 @@ Neues Deno-Modul. Verzeichnis: `snapshot/` als Geschwister zu `publish/`. - `url` → primäre Bild-URL, wird vom Prerender als `og:image`-Wert in den HTML-Head geschrieben. Crawler sehen nur diese URL. - `fallback_url` → zweiter Blossom-Server mit demselben Hash (Blossom - ist content-addressed). Informativ, kann von JS-Clients als Fallback - genutzt werden. Nicht Teil der OG-Tags. + ist content-addressed). Nicht Teil der OG-Tags. Nutzungsszenario: + Falls der Snapshot-HTML ein ``-Element im Post-Body erzeugt, das + auf `url` zeigt, kann ein client-seitiger `onerror`-Handler bei + Ladefehler auf `fallback_url` umschalten. Ist das nicht gewünscht + (YAGNI), wird das Feld entfernt — Entscheidung in der Planungsphase. + +**Semantik von `created_at` vs. `published_at`:** +- `published_at` → Redaktions-Zeitpunkt (menschlich), aus `published_at`- + Tag des Events. Ändert sich nicht bei Re-Publish. Wird als + `article:published_time` in OG-Tags gerendert. Hauptanzeige-Datum. +- `created_at` → technischer Event-Zeitstempel, ändert sich bei jedem + Update (z.B. bei Korrekturen). Kann als „zuletzt aktualisiert" + angezeigt werden. In OG nicht verwendet. +- Fehlt `published_at`-Tag im Event, wird `created_at` übernommen + (siehe Algorithmus, Schritt 8). + +**Semantik der `translations[]`-Einträge:** +- Jeder Eintrag enthält `lang`, `slug` **und** `title` der fremdsprachlichen + Version. Prerender nutzt `lang`/`slug` für `hreflang`-Links, und + `title` für den SPA-Sprach-Switcher (📖 DE | EN). Damit entfällt ein + Runtime-Relay-Fetch beim Switcher. **CLI:** ```sh @@ -194,6 +242,7 @@ deno task snapshot # default deno task snapshot --out ./out # alternatives Ziel deno task snapshot --min-events 20 # Schwelle deno task snapshot --cache ./.last.json # Vergleich +deno task snapshot --allow-shrink # Drop-Check aus ``` ### Stufe 3 — `build+deploy` @@ -241,9 +290,13 @@ Die Route rendert den Snapshot-Content statt Relay-Fetch. Im - `