**Scope:** Ablösung der Hugo-Seite `joerg-lohrer.de` durch eine SvelteKit-SPA, die Blog-Posts live aus Nostr-Events rendert. Diese Spec beschreibt **nur die SPA und den Event-/URL-Kontrakt**. Publish-Pipeline (Markdown → Event → Relays + Assets-Upload) ist separate Spec.
Bilder (Altbestand) → All-Inkl unter Post-Permalink-Pfad
Bilder (neu) → Blossom-Server (Multi-Upload, single URL im Markdown)
```
### Kernprinzipien
- **All-Inkl hostet nur eine statische SPA-Shell** plus Assets: Altbilder (unter Post-Permalink-Pfaden), Site-Icons, `robots.txt`. Kein Post-Body, keine Stubs, kein Backend.
- **Posts existieren als signierte NIP-23-Events auf mehreren Public-Relays.** Die SPA holt sie zur Laufzeit.
- **URL-Struktur bleibt kompatibel zur bestehenden Hugo-Seite.** Backlinks brechen nicht.
- **Minimale eigene Infrastruktur jetzt** (keine eigene Relay-Instanz, kein eigener Blossom-Server). Alles nachrüstbar ohne Bruch.
- **Bewusst akzeptierte Kosten:** kein SEO, keine Social-Previews in Phase 1. Siehe Risiken und Evolutionspfad.
### Kostenübersicht Phase 1
- All-Inkl: unverändert (vorhandener Tarif).
- Public-Relays: 0 €.
- Public-Blossom-Server (nur Neu-Bilder ab Phase 2): 0 €.
- Domain `joerg-lohrer.de`: unverändert.
- **Zusatzkosten: keine.**
---
## 2. URL-Struktur, Routing, Event-Kontrakt
### URL-Schema (kompatibel zur bestehenden Hugo-Struktur)
**Datum in der URL** dient nur der Backlink-Kompatibilität. Die SPA benötigt zur Event-Abfrage nur den `dtag`; sie fragt Relays mit `kinds:[30023], authors:[<pubkey>], #d:[<dtag>]`. Das Datum ist URL-Dekoration.
**Content:** Markdown. Bilder als Markdown-Syntax mit absoluten URLs.
**Bild-URL-Policy pro Post-Ära:**
- Migrations-Posts (bestehende 18 Posts, 2013–2024): Bild-URLs auf `joerg-lohrer.de` unter Post-Permalink-Pfad, z. B. `https://joerg-lohrer.de/2023/03/23/gleichnis-vom-saemann.html/bild1.jpeg`. Bilder liegen tatsächlich dort.
- Neue Posts (ab Phase 2): Bild-URLs auf Blossom-Server, z. B. `https://blossom.primal.net/abc123…def.jpeg`. Upload zu 2–3 Blossom-Servern parallel (BUD-03-Pattern), Markdown referenziert jeweils nur eine URL.
Die SPA unterscheidet die beiden Eras nicht — sie rendert Markdown, der Browser lädt die absolute URL, wo sie auch liegt.
### SPA-Routing
SvelteKit mit `adapter-static`, `ssr: false`, Fallback-Page `index.html`. Routen:
-`/impressum/` → eigener Impressum-Pfad (oder statisch außerhalb der SPA)
**Hinweis zum `.html`-Suffix im Routing:** SvelteKit unterstützt statische Dateiendungen in dynamischen Segmenten nicht direkt. Lösung: Entweder den `.html`-Suffix im Parameter-Wert mitbehandeln (`[dtag]` matched den String inklusive `.html`, davon wird beim Auslesen `.html` abgeschnitten) oder den Pfad als Catch-All-Route `[...slug]` aufnehmen und die Teile im Load-Handler selbst parsen. Details in der Implementation.
Relay-Liste kommt aus dem NIP-65-Outbox-Event (`kind:10002`) des Autors. Das Event wird einmalig manuell publiziert (siehe Publish-Spec, Abschnitt „Pre-Flight-Setup") und enthält die bevorzugten Read- und Write-Relays.
**Auflösung zur Laufzeit:**
1. SPA kennt genau einen hartcodierten **Bootstrap-Relay** (`wss://relay.damus.io`).
2. Beim Boot: SPA fragt Bootstrap-Relay nach `{ kinds:[10002], authors:[PUBKEY] }`.
3. Aus dem Event werden die `["r", <url>]`-Tags extrahiert (Read-Relays für die SPA-Abfragen).
4. Diese Liste wird für alle weiteren Nostr-Requests genutzt.
**Fallback:** Falls Bootstrap-Relay nicht antwortet oder `kind:10002` nicht existiert, nutzt die SPA eine hartcodierte Fallback-Liste.
**Vorteil:** Änderungen an der Relay-Liste (z. B. späteres Hinzufügen eines eigenen Relays) erfordern nur ein neues `kind:10002`-Event, keinen Code-Deploy.
Blossom-Server-Liste wird analog aus `kind:10063` (BUD-03) aufgelöst — siehe Publish-Spec für das normative Schema beider Events.
Keine separaten Svelte-Stores. Observable ist der Store.
### Fehler- und Loading-Zustände
- **Soft-Timeout 8s:** „noch am Suchen …".
- **Hard-Timeout 15s:** Fallback-Hinweis mit Habla-Deeplink (`https://habla.news/a/<naddr>`).
- **Kein Event gefunden:** 404-Komponente mit Habla-Link.
- **Alle Relays offline:** Banner + Retry-Button.
- **Replies/Reactions:** best-effort, Fehler silently — dürfen die Post-Anzeige nicht blockieren.
### Markdown-Rendering (Isolation für Zukunft)
Alles Rendering-Zeug in `src/lib/render/markdown.ts`. Externer Export genau eine Funktion:
```ts
export function renderMarkdown(md: string): string
```
Kein Import von `marked` oder `DOMPurify` außerhalb dieser Datei. Das macht Austausch (z. B. zu `svelte-markdown`, `markdown-it` oder `unified`) später zu einem lokal begrenzten Refactor.
Implementierung (Skizze):
```ts
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import bash from 'highlight.js/lib/languages/bash'
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('bash', bash)
marked.use({
breaks: true,
gfm: true,
renderer: {
code(code, lang) {
const highlighted = lang && hljs.getLanguage(lang)
- **DOMPurify für allen gerenderten Content** — Hauptposts (von dir signiert) und Replies (von Dritten).
- **Pubkey-Whitelist:** PostsLoader filtert strikt auf `authors:[PUBKEY]`. Fremde Events für Hauptinhalte nicht anzeigen. Replies/Reactions explizit ausgenommen, aber sanitized.
- **Privat-Key liegt nie im Bundle, nicht auf All-Inkl, nicht im Browser.** Signieren ausschließlich lokal (Publish-Spec) oder via NIP-07 (Besucher-Kommentare).
Häufige Sorge: „kann mein einfaches Webspace-Paket eine SvelteKit-App hosten, ich habe ja keinen vServer und kein SSH?" Antwort: ja, problemlos. Hier warum.
**SvelteKit produziert reine statische Dateien.** Mit dem `adapter-static` (siehe §3) erzeugt `npm run build` einen Ordner `build/`, der nichts anderes enthält als:
-`index.html` — eine HTML-Datei
-`_app/` mit JS/CSS-Bundles — kompilierte Dateien
- weitere statische Files (favicon etc.)
Das ist exakt das gleiche Material, das Hugo bisher in `public/` produziert hat — nur mit JS statt vielen einzelnen HTML-Seiten. **Beides ist statisches Hosting, beides funktioniert auf jedem Webspace, der HTML ausliefern kann.**
**Was du brauchst:**
- Ordner zum Hochladen ✅ (jeder Webspace)
- Webserver, der HTML/JS/CSS ausliefert ✅ (jeder Webspace)
- Apache mit `mod_rewrite` für SPA-Fallback (eine `.htaccess`) ✅ (All-Inkl-Standard)
**Was du nicht brauchst:**
- ❌ Node.js auf dem Server
- ❌ Datenbank
- ❌ vServer/SSH (für *Hosting* nicht; SSH wird nur für komfortablen *Upload* via rsync genutzt — FTP funktioniert genauso)
- ❌ irgendetwas, das dauerhaft serverseitig läuft
**Unterschied Hugo vs. SvelteKit aus Server-Sicht:**
- Hugo erzeugt **viele HTML-Dateien**, eine pro Post. Server liefert pro URL eine spezifische Datei.
- SvelteKit (SPA) erzeugt **eine HTML-Datei** und ein JS-Bundle. Server liefert immer dieselben Dateien, der Browser entscheidet per JavaScript, was angezeigt wird (basierend auf der URL).
Für den Server ist Variante 2 sogar **simpler** — er hat weniger Dateien zu verwalten und muss nichts dynamisch generieren.
**Was die `.htaccess` macht:** wenn jemand `https://joerg-lohrer.de/2025/03/04/dezentrale-oep-oer.html/` aufruft, gibt es diese Datei nicht physisch auf dem Server — der Pfad ist eine virtuelle SPA-Route. Apache würde 404 antworten. Eine kleine `.htaccess`-Datei sagt Apache: „wenn die angeforderte Datei nicht existiert, liefere `/index.html` aus." Browser bekommt die SPA-Shell, JavaScript liest die URL, lädt das richtige Event vom Relay, rendert den Post.
**Was am Ende auf dem Webspace liegt** (siehe Dateistruktur weiter unten): ungefähr 30–80 Dateien, zusammen 100–200 KB. Weniger als ein einziges Foto. Komplett statisch, kein Backend.
Die SPA soll Events **wahrheitsgetreu** rendern und **nicht still Daten korrigieren**. Wenn ein Event Auffälligkeiten enthält (doppelte `t`-Tags, leeres `d`, inkonsistente Groß-/Kleinschreibung gegenüber anderen Events desselben Autors), soll die SPA das sichtbar lassen — nicht transparent wegdedupen. Grund: der Autor merkt sonst nicht, dass seine Events Daten-Mängel haben.
Daten-Bereinigung gehört in **separate Audit-Werkzeuge** (siehe Publish-Spec, z. B. künftiger `deno task audit`), die auf Basis von Relay-Queries einen Report erstellen und mögliche Korrektur-Commits in den Markdown-Quelltext vorschlagen.
Der Mini-SPA-Spike (`preview/spa-mini/`) dedup'te pragmatisch; die produktive SPA tut das nicht.