**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).
### Bundle-Budget
Schätzung gzip:
- SvelteKit Runtime: ~15 KB
- applesauce-relay + -loaders + -signers: ~30 KB
- rxjs (tree-shaken): ~15 KB
- marked + DOMPurify: ~25 KB
- highlight.js Core + 3 Sprachen: ~15 KB
- App-Code: ~10 KB
- **Total: ~110 KB gzip** — vertretbar.
---
## 4. Hosting, Deployment, Migrationspfad
### Hosting bei All-Inkl
Webhosting-Paket, Standardfeatures:
- statische Dateiauslieferung
-`mod_rewrite` (Standard)
- HTTPS (Let's Encrypt inklusive, ggf. schon aktiv)
Keine PHP, kein MySQL, keine Cronjobs, kein Backend.
### Dateistruktur auf Webspace
```
/
├── index.html # SPA-Shell
├── _app/ # SvelteKit-Bundle
├── .htaccess
├── robots.txt
├── favicon.ico
├── impressum/
│ └── index.html # statische HTML-Datei
├── 2024/01/26/offenheit-das-wesentliche.html/
│ ├── bild1.jpeg # Altbilder
│ └── bild2.jpeg
├── 2023/03/23/gleichnis-vom-saemann.html/
│ └── …
└── …
```
### Upload-Mechanik
- **SvelteKit-Bundle:** `npm run build` → `build/` → rsync/FTP in Webspace-Root. npm-Script `deploy`.
- **Altbilder:** einmalig per rsync aus `content/posts/` an die Permalink-Pfade übertragen.
- **Neue Bilder:** nicht auf All-Inkl (Phase 2, Blossom-Upload via Publish-Spec).
**Credentials:** FTP-Login in `.env` des lokalen Repos, nicht committed.
### Migrationspfad (die 18 bestehenden Posts)
Um Verwechslung mit den „Phasen" im Evolutionspfad (Abschnitt 5) zu vermeiden, werden die Migrationsschritte mit **Schritt A–E** bezeichnet.
**Schritt A — Pre-Launch:**
Hugo-Seite bleibt live, nichts anfassen.
**Schritt B — SPA entwickeln:**
Lokal bauen, gegen Public-Relays mit Test-Events validieren.
**Schritt C — Posts als Events publizieren (Publish-Spec):**
Alle 18 `.md` zu `kind:30023` signieren und zu 4–5 Public-Relays pushen.