Alle 6 etappen umgesetzt, live auf joerg-lohrer.de seit 2026-04-28.
README im archive aktualisiert, alter prerender-plan (2026-04-21,
nicht umgesetzt) als "abgeloest durch 2026-04-28" markiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Etappe 5 hatte einen bug: nach dem prerendered HTML hat svelteKit beim
ersten clientseitigen navigations-tick nochmal load() aufgerufen, das
hat im browser-pfad pauschal 404 geworfen. Sichtbarer effekt: seite
zeigt kurz den korrekten inhalt, springt dann auf "Post nicht
gefunden".
Fix-strategie D aus dem chat:
- snapshot/output/posts/*.json wird vor dem build nach
app/static/snapshot-data/posts/ kopiert (deploy-skript + workflow)
- +page.ts im browser-pfad fetcht dann /snapshot-data/posts/<slug>.json
- 404 nur noch wenn die JSON wirklich nicht existiert (= slug nicht
im snapshot)
Damit funktionieren sowohl hard-reload (svelteKit hydriert die
prerendered page-data direkt) als auch clientseitige navigation
zwischen detail-seiten.
app/static/snapshot-data/ ist gitignored — wird zur build-zeit aus
snapshot/output/ generiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
curl hat den ersten upload mit "URL using bad/illegal format"
abgebrochen. Wahrscheinliche ursache: trailing newline in einem
der FTP-secrets (passiert beim copy-paste in die GitHub-secrets-UI,
wenn der quell-string ein \n am ende hatte).
tr -d '\r\n' entfernt CR/LF aus allen vier FTP-vars, bevor sie in
die URL gehen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI-build hat mit "Some specified paths were not resolved, unable to
cache dependencies" abgebrochen, weil actions/setup-node@v4 mit
cache: 'npm' eine committed package-lock.json erwartet.
Lockfile war bisher in app/.gitignore ausgeschlossen — ungewoehnliche
konvention fuer SvelteKit-projekte; reproduzierbare builds (sowohl
CI als auch fresh-clone) brauchen den lockfile. Jetzt committed,
mit dem stand der nach den isomorphic-dompurify+@types/node-installs
existiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bewusst kein auto-trigger: kontrolle bleibt beim menschen, wer wann
auf welche subdomain deployt. Aufruf via "Actions" → "Build + Deploy
SPA" → "Run workflow", target ist 'svelte' (default), 'staging' oder
'prod'. 1:1 abbild der lokalen scripts/deploy-svelte.sh-logik:
1. snapshot ziehen (Deno)
2. SvelteKit bauen (Node)
3. __SITE_URL__-substitution
4. __HTML_LANG__-substitution pro detail-HTML aus snapshot/output
5. FTPS-upload pro datei via curl --tls-max 1.2 (All-Inkl-friendly)
6. live-check via curl
Voraussetzung: SVELTE_FTP_*, STAGING_FTP_* als github-secrets hinterlegen.
AUTHOR_PUBKEY_HEX + BOOTSTRAP_RELAY existieren bereits aus publish-pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sechs entkoppelte etappen, je rollback-bar:
1. renderMarkdown auf isomorphic-dompurify (node-faehig fuer prerender).
2. Neues snapshot/-modul (Deno) mit 32 tests — liest events von relays,
schreibt JSON-artefakte (NIP-09-aware mit zeitlicher reihenfolge,
plausibilitaetschecks, cover-probe, last-known-good-cache).
3. GitHub-Action zieht snapshot nach jedem publish als artifact.
4. SvelteKit-detail-route auf prerender=true mit <svelte:head> fuer
OG/Twitter/JSON-LD/hreflang. <html lang> + og:image-dimensionen
pro post; x-default zeigt auf DE-slug.
5. Runtime-relay-fetch fuer detail-route entfernt — quelle der wahrheit
ist jetzt der snapshot.
6. (Geskippt — lftp mirror in 3 phasen war optional.)
Plus toter code der pre-prerender-aera (PostView, LanguageAvailability,
loadPost, loadTranslations, translations.ts) entfernt; deploy-skript
zieht snapshot vor build; doku (CLAUDE/STATUS/HANDOFF) aktualisiert.
Live verifiziert auf svelte.joerg-lohrer.de — OG-tags, JSON-LD,
hreflang, multilingual-rendering korrekt.
Spec: docs/superpowers/specs/2026-04-21-prerender-snapshot-design.md
Plan: docs/superpowers/plans/2026-04-28-prerender-snapshot.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aus dem code-review der etappe (2026-04-28):
- CLAUDE.md: snapshot/-verzeichnis in hauptarbeitsbereiche-tabelle
ergaenzt; neue stolperfalle "snapshot-output muss vor npm run build
da sein" eingefuegt.
- STATUS.md: kurzfassung um den prerender-stand erweitert; repo-baum
zeigt snapshot/; erledigt-eintrag mit den sechs etappen.
- HANDOFF.md: stolperfalle "snapshot vor build" am anfang der liste —
haeufigster fallstrick fuer naechste session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Begleiter zu app.html-aenderung in bb9d350, die <html lang> auf
__HTML_LANG__ umgestellt hat. Deploy-skript leitet den lang-wert
pro <slug>/index.html aus snapshot/output/posts/<slug>.json ab
(grep nach "lang"-feld im JSON), faellt sonst auf 'de' zurueck.
Spec-§3.2: <html lang> aus snapshot.lang. Vorher hartcodiert
"de", was englische posts (z.B. bible-selfies) crawlern als
deutschsprachig auswies — google search console hat das als
lang-mismatch geflagt, screen reader sprechen englischen body
mit deutscher engine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime-fallback (loadPost + LoadingOrError + PostView) entfernt.
Detail-seite rendert jetzt ausschliesslich aus dem snapshot. Imports
und state, die nur fuer den fallback gebraucht wurden, sind weg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slugs ausserhalb des snapshots werfen jetzt 404 (server-side beim
build, browser-side via static-fallback). Vorher kam in dem fall
{ dtag, snapshot: null } zurueck — die svelte-seite versuchte dann
clientseitig via loadPost() zu laden.
Frische nostr-first-posts erscheinen ab jetzt erst nach dem
naechsten snapshot+build-lauf, nicht mehr live aus den relays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-feedback: kommentar-inhalt aus app.html erschien sichtbar im
body der detail-seiten. Ursache: SvelteKit ersetzt jeden vorkommen
von %sveltekit.head%, auch innerhalb von HTML-kommentaren. Mein
beschreibender kommentar erwaehnte das wort als beispiel — wurde
dann zur build-zeit durch tatsaechliche head-tags ersetzt, was den
kommentar-block aufbrach (eingefuegte tags enthalten >, das schliesst
den kommentar) und den rest des kommentar-textes in den body
wandern liess.
Fix: kommentar umformuliert, sodass der platzhalter-name nicht mehr
woertlich vorkommt ("via SvelteKit-head-injection" statt "via
%sveltekit.head%").
Live-verifiziert auf banksy-high-court-prophet — body startet jetzt
sauber mit <header>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Damit DEPLOY_TARGET=svelte/staging/prod immer mit aktuellem
snapshot/output baut. Ohne diesen step wuerde ein veralteter
snapshot ins HTML wandern, frische posts oder uebersetzungen
saessen erst beim naechsten deploy drin.
Schlaegt der snapshot fehl (relays down, env fehlt), bricht das
skript ab — bewusst hard-fail, damit kein verfaelschter build
hochgeladen wird.
Live-verifiziert auf https://svelte.joerg-lohrer.de/bibel-selfies/:
- HTTP 200, og-tags + hreflang + json-ld korrekt
- __SITE_URL__-substitution greift sauber
- <title>, og:title, json-ld stimmen mit snapshot ueberein
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drei build-blocker beim ersten prerender-versuch identifiziert und gefixt:
1. svelte.config.js: handleHttpError + handleMissingId fuer den
prerender-crawler. Der crawler folgt zur build-zeit allen hrefs/srcs
im HTML — sieht dabei
- __SITE_URL__-platzhalter in canonical/hreflang (werden im deploy
per sed ersetzt, sind keine echten routes)
- relative bild-paths in alten posts (z.B. h01-json-import.png)
- anchor-links auf headings ohne id-attribute (#ACF-JSON-Export)
Alle drei sind keine echten 404s — handlers ignorieren sie.
2. +page.svelte: <script type="application/ld+json">{jsonLd}</script>
in <svelte:head> rendert {jsonLd} als literalen string, weil svelte
den script-tag-inhalt nicht als expression evaluiert. Zurueck zu
{@html ...} mit </script>-escape-hardening, damit titel oder
beschreibungen mit </script> den output nicht aufbrechen koennen.
3. app.html behaelt seine homepage-defaults fuer og:title/og:url/
og:description/canonical — der prerender-crawler rendert nur
detail-routen (/<slug>/), die homepage bleibt SPA-only und braucht
die defaults im app.html-template, weil dort kein svelte:head greift.
Detail-routen ueberschreiben per <svelte:head>; last-wins greift bei
LinkedIn/Mastodon/Browser. Facebook/Twitter (first-wins) haetten
einen homepage-prerender-schritt noetig — folge-aufgabe.
Plus snapshot/deno.lock committed — deno empfiehlt lockfile-commit fuer
reproduzierbare CI-builds, analog package-lock.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot-pfad bekommt feature-paritaet mit dem runtime-fallback:
- Sprach-switcher (inline, gleiche optik wie LanguageAvailability,
ohne neue i18n-keys — verwendet snapshot.translations direkt)
- Tag-liste mit links auf /tag/<name>/
- Reactions, ExternalClientLinks, ReplyComposer, ReplyList
(alle dtag-basiert, brauchen keine NostrEvent-konstruktion)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bei der umsetzung von task 4.2 stellte sich heraus, dass svelte
<svelte:head> nicht in einem {#if}-block stehen darf. Plan-code
korrigiert von
{#if snapshot}<svelte:head>...</svelte:head>{/if}
zu
<svelte:head>{#if snapshot}...{/if}</svelte:head>
Semantisch identisch (head-content erscheint nur wenn snapshot da ist),
aber svelte-konform.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot-pfad: page+head komplett aus json, mit og/twitter/jsonld/hreflang.
Runtime-fallback: falls data.snapshot null, loadPost+PostView wie bisher.
Reactions/replies kommen im naechsten task.
svelte:head auf top-level verschoben (svelte-constraint: keine meta-tags
innerhalb von {#if}-bloecken erlaubt).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lokaler override des global ssr=false. entries() liest aus
snapshot/output/index.json, load() pro-slug aus posts/<slug>.json.
runtime-fallback bleibt fuer slugs ausserhalb des snapshots.
@types/node als devDependency ergaenzt, da node:fs/promises-Typen
fuer den SSR-Pfad benoetigt werden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Etappe 3 des prerender-snapshot-plans (variante 1: minimal — kein
neuer workflow, deploy bleibt manuell via scripts/deploy-svelte.sh):
- 'Snapshot'-step laeuft nach publish, ruft deno-snapshot-cli auf
- output (index.json + posts/*.json + .last-snapshot.json) wird als
github-actions-artifact fuer 30 tage aufgehoben — debug-pfad falls
ein deploy-bug nachvollzogen werden muss
- AUTHOR_PUBKEY_HEX + BOOTSTRAP_RELAY werden aus existierenden secrets
uebernommen, keine neuen secrets noetig
Reihenfolge "publish dann snapshot": neue events muessen erst auf den
relays sein, bevor sie gesnapshottet werden koennen. Bei publish-fail
laeuft snapshot nicht — gewollt, weil unklarer relay-stand zu
fehlerhaftem snapshot-output fuehren wuerde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review-feedback aus etappe 2.9/2.10:
1. cli.ts: deletedCoords im cache wird ab jetzt akkumuliert statt
ersetzt. Vorher wurden bei einem run nur die aktuell von relays
gelieferten kind:5-coords geschrieben — wenn ein relay beim
naechsten run die alten deletions nicht mehr liefert (GC,
relay-tausch), waere die geschichte verloren und newDeletionsCount
im naechsten lauf wieder "neu" -> false-positive hard-fail im
drop-check.
2. relays.ts: kommentar zum belt-and-suspenders-setTimeout neben dem
RxJS-timeout-operator, damit der zweck (handle-cleanup falls beide
subscribe-callbacks verschluckt werden) klar ist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher konnten malformed tags wie ['t'] (ohne second element)
undefined ins string[]-array werfen, das im JSON als null landete.
Code-review-feedback aus etappe 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per NIP-09 darf ein deletion nur events mit created_at <= deletion.created_at
loeschen. Vorher wurde ein re-publizierter post nach geloeschtem vorgaenger
stumm wegfiltern. Code-review-feedback aus etappe 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dead-code aus etappe 1 nachgezogen:
- dompurify + @types/dompurify aus package.json (jetzt isomorphic-dompurify
als einziger sanitizer, bringt eigene typen mit)
- design-rationale-kommentar fuer markedInstance zurueckgebracht
(Spec §3: lokale ersetzbarkeit der engine — nicht aus dem code ablesbar)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Funktioniert jetzt sowohl in Browser/jsdom als auch in Node (SvelteKit-Build).
Schritt 1 der prerender-snapshot-migration. Verhalten in der SPA unveraendert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-feedback: <script type="application/ld+json"> kann svelte direkt
rendern. Kein @html-Notausgang noetig, sauberere variante mit
auto-escape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frischer plan gegen die ueberarbeitete spec (fallback_url raus,
OG-default joerg-profil-2024.webp, prerender+runtime-fallback klar
als adapter-static-default). Sechs etappen, jede einzeln testbar
und rollback-bar:
1. renderMarkdown auf isomorphic-dompurify (node-faehig)
2. Snapshot-modul (deno) mit 9 testbaren cores + cli
3. Snapshot in CI
4. Detail-route auf prerender mit runtime-fallback
5. Runtime-fallback entfernen (snapshot ist quelle)
6. lftp mirror in drei phasen (optional, nicht-blockierend)
Alter plan bleibt im archive als geschichte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fallback_url im cover_image rausgenommen (YAGNI; primary url
reicht, blossom ist content-addressed). Beim ersten konkreten
bedarf nachreichen.
- Site-default-OG-bild auf app/static/joerg-profil-2024.webp
festgelegt (vorhandenes asset, nicht spekulativ).
- prerender + runtime-fallback klarer formuliert: das ist das
default-verhalten von adapter-static mit fallback, kein
workaround. ReplyList/ReplyComposer-hydration als
verifikations-punkt im plan-schritt 4 vermerkt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README/STATUS/HANDOFF/CLAUDE jeweils ein rollensatz oben:
- README = aussenseite
- STATUS = logbuch (stand + erledigt-chronologie)
- HANDOFF = konventions-handbuch (workflows + stolperfallen)
- CLAUDE.md = session-einstieg
HANDOFF erhaelt zusaetzlich eine Single-Source-of-Truth-sektion,
damit klar ist welche fakten wo gepflegt werden.
README-linkliste auf 3 specs eingedampft (SPA, pipeline, multilingual)
und auf das plan-archiv verlinkt statt einzelplaene. CLAUDE.md
verlinkt zusaetzlich die prerender-snapshot-spec mit kennzeichnung.
STATUS.md verzeichnis-baum spiegelt das archiv wider.
Wiki-entwuerfe + redaktions-doku + github-ci-setup-doku haben
explizite status-stempel (eingefroren / schnappschuss / aktuell-aber-zu-ersetzen),
damit man nicht mehr raten muss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Alle 6 plaene unter docs/superpowers/plans/archive/. Davon 5 abgearbeitet
(SPA, publish-pipeline, 3x multilingual), 1 eingefroren (prerender-snapshot).
Checkboxen wurden waehrend der umsetzung nicht gepflegt — verbindlicher
stand ist STATUS.md und die jeweilige spec, das archive/README.md macht
die zuordnung sichtbar.
plans/ bleibt mit .gitkeep als verzeichnis bestehen, damit neue plaene
sofort wieder dort landen koennen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status-Header aller specs nach neuer konvention:
Entwurf | In Umsetzung | Umgesetzt (live seit YYYY-MM-DD) | Eingefroren | Verworfen.
Die zwei aeltesten specs (SPA, publish-pipeline) standen formal noch
auf "ausstehende User-Freigabe", obwohl seit 2026-04-18 live —
nun korrekt als umgesetzt markiert. Bild-metadaten-spec als
Phase 1 umgesetzt mit Phase-2-vermerk. Multilingual-spec angeglichen.
Prerender-snapshot-spec von "Stand:" auf "Status: Entwurf" mit
hinweis auf eingefrorenen plan im archiv.
Cleanup-spec selbst dokumentiert die konvention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Snapshot liefert content_markdown, nicht content_html. Rendering
(marked + DOMPurify + highlight.js) passiert im SvelteKit-Prerender-
Schritt über das bereits existierende \$lib/render/markdown.ts —
keine Duplikation in Deno, kein gemeinsames Policy-Modul nötig.
Für Blaupausen-Nutzung ist rohes Markdown portabler: alternative
Renderer (Astro, Eleventy) bringen eigenen Markdown-Prozessor mit.
Konsequenz für Migration: Schritt 1 ist jetzt \"renderMarkdown
Node-kompatibel machen\" (isomorphic-dompurify) statt \"shared/
markdown-policy.ts ergänzen\".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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/<name>/-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) <noreply@anthropic.com>