471 lines
20 KiB
Markdown
471 lines
20 KiB
Markdown
|
|
# Nostr-Page auf Basis von Events — Design-Spec
|
|||
|
|
|
|||
|
|
**Datum:** 2026-04-15
|
|||
|
|
**Status:** Entwurf, ausstehende User-Freigabe
|
|||
|
|
**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.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Gesamtarchitektur
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Browser JS-loser Client / Bot
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────────────────────────┐ ┌───────────────────────────┐
|
|||
|
|
│ All-Inkl (statisches Hosting)│ │ All-Inkl liefert │
|
|||
|
|
│ • index.html (SPA-Shell) │ │ index.html (leere Shell, │
|
|||
|
|
│ • _app/*.js, *.css │ │ kein Post-Inhalt, keine │
|
|||
|
|
│ • .htaccess (SPA-Fallback) │ │ OG-Tags für Posts) │
|
|||
|
|
│ • images/ bzw. Permalink- │ └───────────────────────────┘
|
|||
|
|
│ Pfade für Altbilder │
|
|||
|
|
└──────────────┬──────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ Public Nostr-Relays (wss://) │
|
|||
|
|
│ damus.io, nos.lol, nostr.wine, │
|
|||
|
|
│ relay.nostr.band … │
|
|||
|
|
│ (später erweiterbar um eigene) │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
Publish-Flow (separate Spec, nur Artefakte hier relevant):
|
|||
|
|
Markdown → signiertes kind:30023 Event → Public-Relays
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
| URL | Inhalt |
|
|||
|
|
|---|---|
|
|||
|
|
| `/` | SPA-Shell. SPA rendert Startseite (Profilkachel + Post-Liste). |
|
|||
|
|
| `/YYYY/MM/DD/<dtag>.html/` | SPA-Shell (via `.htaccess`-Fallback). SPA-Router extrahiert `<dtag>` und lädt Event. |
|
|||
|
|
| `/archives/` | SPA-Shell, SPA rendert chronologische Liste. |
|
|||
|
|
| `/tag/<name>/` | SPA-Shell, SPA rendert Tag-Filter. |
|
|||
|
|
| `/impressum/` | Statisches HTML (rechtlicher Content, liegt wirklich auf Server). |
|
|||
|
|
| `/YYYY/MM/DD/<dtag>.html/<bildname>` | echte Bilddateien der 18 Altposts, liegen unter dem jeweiligen Post-Permalink. |
|
|||
|
|
| `/favicon.ico`, `/logo.png`, `/robots.txt` | globale Site-Assets. |
|
|||
|
|
|
|||
|
|
**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.
|
|||
|
|
|
|||
|
|
### `.htaccess`
|
|||
|
|
|
|||
|
|
```apache
|
|||
|
|
RewriteEngine On
|
|||
|
|
|
|||
|
|
# HTTPS forcieren
|
|||
|
|
RewriteCond %{HTTPS} !=on
|
|||
|
|
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
|||
|
|
|
|||
|
|
# Existierende Datei oder Verzeichnis? Direkt ausliefern (Bilder, _app/*, favicon etc.).
|
|||
|
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
|||
|
|
RewriteCond %{REQUEST_FILENAME} -d
|
|||
|
|
RewriteRule ^ - [L]
|
|||
|
|
|
|||
|
|
# Alles andere → SPA-Fallback.
|
|||
|
|
RewriteRule ^ /index.html [L]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Event-Kontrakt (NIP-23, `kind:30023`)
|
|||
|
|
|
|||
|
|
Normatives Schema; Publish-Spec muss es einhalten, SPA verlässt sich darauf.
|
|||
|
|
|
|||
|
|
**Pflicht-Tags pro Event:**
|
|||
|
|
|
|||
|
|
- `["d", "<slug>"]` — URL-Slug. Stabiler Identifier über Edits hinweg (replaceable-Semantik).
|
|||
|
|
- `["title", "<post title>"]`
|
|||
|
|
- `["published_at", "<unix-timestamp>"]` — ursprüngliches Veröffentlichungsdatum.
|
|||
|
|
|
|||
|
|
**Empfohlene Tags:**
|
|||
|
|
|
|||
|
|
- `["summary", "<kurzbeschreibung>"]` — genutzt in Listen-Previews.
|
|||
|
|
- `["image", "<url>"]` — Vorschaubild.
|
|||
|
|
- `["t", "<tag>"]` — mehrfach erlaubt, Kategorien/Tags.
|
|||
|
|
|
|||
|
|
**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:
|
|||
|
|
|
|||
|
|
- `/` → Home
|
|||
|
|
- `/[year]/[month]/[day]/[dtag].html/` → PostView (nur `dtag` genutzt)
|
|||
|
|
- `/archives/` → Archives
|
|||
|
|
- `/tag/[name]/` → TagView
|
|||
|
|
- `/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-Konfiguration
|
|||
|
|
|
|||
|
|
Fest im Bundle hinterlegte Default-Liste (Konfig-Datei, nicht hartcodiert):
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/lib/nostr/config.ts
|
|||
|
|
export const READ_RELAYS = [
|
|||
|
|
'wss://relay.damus.io',
|
|||
|
|
'wss://nos.lol',
|
|||
|
|
'wss://relay.nostr.band',
|
|||
|
|
'wss://nostr.wine',
|
|||
|
|
]
|
|||
|
|
// TODO bei Implementierung: npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9
|
|||
|
|
// in hex decodieren (nip19.decode) und hier eintragen.
|
|||
|
|
export const AUTHOR_PUBKEY_HEX = '<hex wird bei Implementierung aus npub abgeleitet>'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Später erweiterbar um eigenen Relay per Konfig-Änderung, kein Code-Umbau.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. SPA-Komponenten & Datenfluss
|
|||
|
|
|
|||
|
|
### Stack
|
|||
|
|
|
|||
|
|
- **Framework:** SvelteKit, Adapter `adapter-static`, `ssr: false`, Prerender-Disabled.
|
|||
|
|
- **Nostr-SDK:** `applesauce-relay`, `applesauce-loaders`, `applesauce-signers` (höherstufig, RxJS-basiert, tree-shakable).
|
|||
|
|
- **Markdown-Rendering:** `marked` + `DOMPurify` + `highlight.js` (3–5 Sprachen registriert).
|
|||
|
|
- **Signing:** NIP-07 (Browser-Extension wie Alby oder nos2x).
|
|||
|
|
|
|||
|
|
### Dateistruktur
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
src/
|
|||
|
|
├── app.html # SPA-Shell <head>-Defaults
|
|||
|
|
├── lib/
|
|||
|
|
│ ├── nostr/
|
|||
|
|
│ │ ├── config.ts # Relays, Pubkey
|
|||
|
|
│ │ ├── pool.ts # RelayPool Singleton (applesauce-relay)
|
|||
|
|
│ │ ├── loaders.ts # loadPostList, loadPost, loadReplies, loadReactions
|
|||
|
|
│ │ ├── signer.ts # NIP-07 Wrapper (applesauce-signers)
|
|||
|
|
│ │ └── outbox.ts # NIP-65 Read-Relay-Resolution für fremde Autoren
|
|||
|
|
│ ├── render/
|
|||
|
|
│ │ ├── markdown.ts # renderMarkdown(md: string): string — SINGLE EXPORT
|
|||
|
|
│ │ └── naddr.ts # nip19.naddrEncode Helper
|
|||
|
|
│ └── components/
|
|||
|
|
│ ├── PostCard.svelte
|
|||
|
|
│ ├── PostView.svelte
|
|||
|
|
│ ├── ReplyList.svelte
|
|||
|
|
│ ├── Reactions.svelte
|
|||
|
|
│ └── ReplyComposer.svelte
|
|||
|
|
└── routes/
|
|||
|
|
├── +layout.svelte
|
|||
|
|
├── +page.svelte
|
|||
|
|
├── archives/+page.svelte
|
|||
|
|
├── tag/[name]/+page.svelte
|
|||
|
|
├── impressum/+page.svelte
|
|||
|
|
└── [year]/[month]/[day]/[dtag=html_extension]/+page.svelte # s. Hinweis unten
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Datenfluss: Post-Seite
|
|||
|
|
|
|||
|
|
1. Browser ruft `/2024/01/26/offenheit-das-wesentliche.html/` auf.
|
|||
|
|
2. All-Inkl `.htaccess` → `/index.html` (Fallback, keine Datei unter dem Pfad).
|
|||
|
|
3. SPA bootet, SvelteKit-Router matcht Route, extrahiert `dtag`.
|
|||
|
|
4. `loadPost(dtag)` fragt via `applesauce-loaders` den `RelayPool` mit Filter `{ kinds:[30023], authors:[PUBKEY], '#d':[dtag] }`.
|
|||
|
|
5. Observable emittiert Event (bei Versionen: neueste gewinnt, replaceable-Semantik via `applesauce`).
|
|||
|
|
6. `renderMarkdown(event.content)` → sanitized HTML → `{@html}` in `PostView`.
|
|||
|
|
7. Parallel: `loadReplies(event)` (kind:1 mit `#a` oder `#e`) und `loadReactions(event)` (kind:7), nonblocking.
|
|||
|
|
|
|||
|
|
### Datenfluss: Home/Archiv
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
loadPostList() → req({ kinds:[30023], authors:[PUBKEY], limit:100 })
|
|||
|
|
→ Observable streamt Events
|
|||
|
|
→ Dedup per d-Tag, sortiert nach published_at desc
|
|||
|
|
→ PostCard-Liste rendert reaktiv
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Datenfluss: Kommentar schreiben (NIP-07)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ReplyComposer öffnet
|
|||
|
|
→ signer.getPublicKey() # Alby/nos2x prompted ggf.
|
|||
|
|
→ User tippt, klickt Senden
|
|||
|
|
→ Event bauen (kind:1, Tags: ['a', addr], ['e', eventId], ['p', authorPk])
|
|||
|
|
→ signer.signEvent(event) # Extension prompted zur Freigabe
|
|||
|
|
→ pool.publish(writeRelays, signedEvent)
|
|||
|
|
→ bestehende ReplyList-Subscription zeigt Event optimistisch sofort an
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`writeRelays` kommt bevorzugt aus dem NIP-65-Outbox-Event des Signers; Fallback: `READ_RELAYS`.
|
|||
|
|
|
|||
|
|
### State-Management
|
|||
|
|
|
|||
|
|
RxJS-Observables direkt in Svelte konsumiert:
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script>
|
|||
|
|
import { loadPost } from '$lib/nostr/loaders'
|
|||
|
|
export let data
|
|||
|
|
const post$ = loadPost(data.dtag)
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
{#if $post$}
|
|||
|
|
<article class="prose">{@html renderMarkdown($post$.content)}</article>
|
|||
|
|
{:else}
|
|||
|
|
<p>Lade …</p>
|
|||
|
|
{/if}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
? hljs.highlight(code, { language: lang }).value
|
|||
|
|
: hljs.highlightAuto(code).value
|
|||
|
|
return `<pre><code class="hljs language-${lang ?? ''}">${highlighted}</code></pre>`
|
|||
|
|
},
|
|||
|
|
link(href, title, text) {
|
|||
|
|
const internal = href?.startsWith('/') || href?.includes('joerg-lohrer.de')
|
|||
|
|
const attrs = internal ? '' : ' target="_blank" rel="noopener"'
|
|||
|
|
return `<a href="${href}"${attrs}>${text}</a>`
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export function renderMarkdown(md: string): string {
|
|||
|
|
const raw = marked.parse(md) as string
|
|||
|
|
return DOMPurify.sanitize(raw, { ADD_ATTR: ['target', 'rel'] })
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Sicherheit
|
|||
|
|
|
|||
|
|
- **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.
|
|||
|
|
`d`-Tag = bisheriger Hugo-Slug; `published_at` = Frontmatter-Datum.
|
|||
|
|
Verifikation: Events in Habla-Client gegenchecken.
|
|||
|
|
|
|||
|
|
**Schritt D — Cutover:**
|
|||
|
|
1. Altbilder in Permalink-Pfade hochladen.
|
|||
|
|
2. SPA-Bundle in Webspace-Root deployen.
|
|||
|
|
3. `index.html` und `.htaccess` ersetzen (alte Hugo-Version wird überschrieben).
|
|||
|
|
4. Alte, nicht mehr benötigte Hugo-Artefakte können bleiben oder gelöscht werden; `.htaccess`-Fallback macht sie harmlos.
|
|||
|
|
|
|||
|
|
**Schritt E — Validierung:**
|
|||
|
|
- Alle 18 URLs im Browser prüfen (Inhalt stimmt, Bilder laden).
|
|||
|
|
- Stichprobenhaft `curl -A "Mozilla/5.0" <url>` → Shell wird ausgeliefert.
|
|||
|
|
- Link auf Mastodon posten → Preview fehlt (erwartet im Minimal-Launch).
|
|||
|
|
|
|||
|
|
**Rollback-Fähigkeit:**
|
|||
|
|
Alter Hugo-`public/`-Stand als ZIP lokal archivieren. Rollback = zurückkopieren.
|
|||
|
|
|
|||
|
|
### URL-Kompatibilität (verifiziert)
|
|||
|
|
|
|||
|
|
Hugo-URL: `https://joerg-lohrer.de/2024/01/26/offenheit-das-wesentliche.html/`
|
|||
|
|
Neu: Pfad existiert nicht als Datei → `.htaccess` → `/index.html` → SPA routed via `dtag`.
|
|||
|
|
Für externe Links: identische URL, identische Inhaltsanzeige. Backlinks aus Mastodon, Google, Bookmarks funktionieren weiter.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Testing, Risiken, Evolution
|
|||
|
|
|
|||
|
|
### Testing-Strategie
|
|||
|
|
|
|||
|
|
**Unit-Tests (Vitest):**
|
|||
|
|
- `renderMarkdown()`: Input/Output, XSS-Vektoren (`javascript:`-URLs, `<script>` in Content).
|
|||
|
|
- `naddr`-Encoder: Pubkey + Kind + d-Tag → erwarteter Bech32-String.
|
|||
|
|
- URL-Parser: `/2024/01/26/foo.html/` → `{ dtag: 'foo' }`.
|
|||
|
|
|
|||
|
|
**Integration-Tests:**
|
|||
|
|
- Mock-Relay (in-memory Fake oder `nostr-relay-tray` in Testmodus).
|
|||
|
|
- PostsLoader: mehrere Versionen desselben `d`-Tags → neueste wins.
|
|||
|
|
- Signer: Test-Key signiert, PostLoader ruft ab, Inhalt matcht.
|
|||
|
|
|
|||
|
|
**End-to-End (Playwright):**
|
|||
|
|
- Happy Path: Start → Liste → Klick → Post rendert.
|
|||
|
|
- Deep-Link: `/2024/01/26/offenheit-das-wesentliche.html/` direkt.
|
|||
|
|
- Kommentar mit Mock-NIP-07-Signer.
|
|||
|
|
|
|||
|
|
**Manuelle Tests vor Go-Live:**
|
|||
|
|
- Alle 18 Post-URLs durchklicken, Visual-Parity-Check gegen alte Hugo-Seite.
|
|||
|
|
- `curl`-Tests mit verschiedenen User-Agents.
|
|||
|
|
- Offline-Fall: WLAN aus → Fehlermeldung lesbar, Habla-Fallback verfügbar.
|
|||
|
|
- Reales NIP-07-Kommentar via Alby auf Test-Post.
|
|||
|
|
|
|||
|
|
### Risiken & Mitigationen
|
|||
|
|
|
|||
|
|
| Risiko | Wahrsch. | Auswirkung | Mitigation |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Public-Relays alle down | niedrig | hoch | 4–5 Relays parallel, Timeout-UI, Habla-Fallback |
|
|||
|
|
| Relay löscht Events | niedrig | mittel | Mehrere Relays, bezahltes nostr.wine als Anker |
|
|||
|
|
| Dependency-Break | mittel | hoch | `package-lock.json` committen, `npm ci`, Staging vor Prod |
|
|||
|
|
| `.htaccess` rewritet Bilder irrtümlich | niedrig | mittel | Regel prüft `-f` vor Fallback |
|
|||
|
|
| Google-Rankings brechen | **hoch** | **mittel** | Akzeptiert; Stubs in Phase 3 nachrüstbar |
|
|||
|
|
| Mastodon-Preview leer | **hoch** | **niedrig** | Akzeptiert; Teaser-Text im Toot kompensiert |
|
|||
|
|
| XSS über Reply | niedrig | hoch | DOMPurify ohne Ausnahmen |
|
|||
|
|
| All-Inkl ändert Apache-Config | sehr niedrig | mittel | Support-Ticket, Standard-Feature |
|
|||
|
|
| Privat-Key-Leak | niedrig | **katastrophal** | Key niemals in Repo, Bundle, Server. Evtl. NIP-46 Bunker |
|
|||
|
|
| Reaktions-Spam | mittel | niedrig | Aggregiert anzeigen, Author-Blocklist |
|
|||
|
|
|
|||
|
|
### Nicht Teil dieser Spec
|
|||
|
|
|
|||
|
|
- **Publish-Pipeline** (Signieren, Upload, GitHub Action): separate Spec. Diese Spec definiert nur Event- und URL-Kontrakt.
|
|||
|
|
- **Eigener Relay/Blossom:** Evolutionspfad, nicht jetzt.
|
|||
|
|
- **Impressum-Text:** rechtliche Inhalte, Umsetzung.
|
|||
|
|
- **Visuelles Design:** Orientierung an PaperMod, Details in Implementation.
|
|||
|
|
|
|||
|
|
### Evolutionspfad
|
|||
|
|
|
|||
|
|
**Phase 1 (jetzt):** Minimal-Launch. SPA auf All-Inkl, Public-Relays, Altbilder auf All-Inkl, Kommentare via NIP-07. Kein SEO, keine Social-Previews.
|
|||
|
|
|
|||
|
|
**Phase 2 (nah):** Blossom für neue Bilder (public Blossom-Server, BUD-03-Multi-Upload, eine URL im Markdown). SPA unverändert.
|
|||
|
|
|
|||
|
|
**Phase 3 (bei Bedarf):** Meta-Stubs nachrüsten für SEO/Social-Previews. Publish-Pipeline erweitern, SPA unverändert.
|
|||
|
|
|
|||
|
|
**Phase 4 (ideologisch):** Eigener strfry-Relay (Homeserver oder VPS), zu Publish-Liste und SPA-Read-Liste hinzu.
|
|||
|
|
|
|||
|
|
**Phase 5 (vollständig dezentral):** Eigener Blossom-Server. Neue Posts uploaden dorthin, alte optional migrieren.
|
|||
|
|
|
|||
|
|
Jeder Phasenwechsel: additiv oder lokal begrenzter Refactor, kein Rewrite.
|
|||
|
|
|
|||
|
|
### Success-Kriterien Phase 1
|
|||
|
|
|
|||
|
|
- Alle 18 alten Post-URLs liefern korrekten Inhalt (Visual-Parity, nicht pixelgenau).
|
|||
|
|
- Neuer Post publizieren (Publish-Spec) < 30 s lokal.
|
|||
|
|
- First Contentful Paint < 1,5 s auf Desktop/LAN.
|
|||
|
|
- Time-to-Post-Rendered < 3 s (Shell + Relay + Event + Rendering).
|
|||
|
|
- Lighthouse Accessibility > 90.
|
|||
|
|
- NIP-07-Kommentar funktioniert in Chrome + Firefox mit Alby.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Anhang: Begriffe
|
|||
|
|
|
|||
|
|
- **NIP-23:** Nostr-Langform-Events, `kind:30023`, replaceable per `d`-Tag.
|
|||
|
|
- **NIP-07:** Browser-Extension-Signer-Protokoll (Alby, nos2x, Flamingo).
|
|||
|
|
- **NIP-65:** Outbox-Model, `kind:10002`, definiert Read/Write-Relays pro Autor.
|
|||
|
|
- **naddr:** Bech32-kodierter Pointer auf ein parameterized-replaceable Event (Pubkey + Kind + d-Tag + Relays).
|
|||
|
|
- **Blossom:** Content-addressed Blob-Hosting für Nostr, Dateien über SHA256-Hash adressiert.
|
|||
|
|
- **BUD-03:** Blossom-User-Description-03, Multi-Server-Mirror-Spezifikation.
|
|||
|
|
- **Replaceable Event:** Event, das alte Versionen mit gleichem (Pubkey, Kind, d-Tag) ersetzt. Relays halten nur die neueste Version.
|