docs: status/handoff/readme/claude.md auf multilingual-stand

- README: neue plan-referenzen (3x multilingual), repo-struktur auf
  content/posts/<lang>/<slug>/, svelte-i18n + translations im app-tree
- STATUS: event-count 27 (26 de + 1 en), kurzfassung um mehrsprachigkeit,
  multilingual-abschnitt in „erledigt" (pipeline, spa, i18n + bugfixes)
- HANDOFF: option D entfernt (erledigt), neuer abschnitt „wie man eine
  übersetzung anlegt", frontmatter-template um a:-platzhalter,
  deploy-target-stolperfalle verschärft, vr-post-pfad aktualisiert
- Multilingual-spec: status von „noch nicht implementiert" auf „umgesetzt"
  + anmerkung zum post-switcher (📖 DE | EN statt text-hinweis)
- CLAUDE.md neu: knapper einstieg für agent-sessions mit commit-konvention,
  deploy-falle, zsh-globbing, forgejo-mirror-timing
- workflow-skill generalüberholt: post-cutover-stand, multilingual,
  publish-pipeline, activeLocale, 73 pipeline-tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-21 16:31:16 +02:00
parent 9040e5ac02
commit d12ed3c40e
6 changed files with 351 additions and 152 deletions

View File

@ -1,80 +1,89 @@
---
name: joerglohrerde-workflow
description: Repo-spezifischer Skill für joerglohrerde. Nutze ihn bei jedem Session-Start, um den aktuellen Zustand zu erfassen, Konventionen zu verstehen und wiederkehrende Workflows (Deploy, Publish, Tests) effizient auszuführen.
description: Repo-spezifischer Skill für joerglohrerde. Nutze ihn bei jedem Session-Start, um den aktuellen Zustand zu erfassen, Konventionen zu verstehen und wiederkehrende Workflows (Deploy, Publish, Tests, Multilingual) effizient auszuführen.
---
# joerglohrerde — Session-Skill
Dieses Repo ist die persönliche Webseite von Jörg Lohrer — in Transition
von Hugo zu einer dezentralen Nostr-basierten SvelteKit-SPA.
Dieses Repo ist die persönliche Webseite von Jörg Lohrer: eine dezentrale
Nostr-basierte SvelteKit-SPA, die NIP-23-Langform-Events live von Public-
Relays rendert. Seit 2026-04-21 mehrsprachig (DE/EN) via `svelte-i18n` +
NIP-33-`a`-Tags.
## Beim Session-Start IMMER zuerst
1. **Lies `docs/STATUS.md`** — aktueller Projektstand, live-URLs, Branches.
2. **Lies `docs/HANDOFF.md`** — was wartet, nächste Schritte, Stolperfallen.
3. Bei konkreten Aufgaben: entsprechende Spec unter `docs/superpowers/specs/`
1. **Lies `CLAUDE.md`** — Agent-spezifische Konventionen (Commit-Stil,
Deploy-Falle, Globbing-Hinweise).
2. **Lies `docs/STATUS.md`** — aktueller Projektstand, Live-URLs.
3. **Lies `docs/HANDOFF.md`** — nächste Schritte, Stolperfallen,
Alltags-Workflow für neue Posts + Übersetzungen.
4. Bei konkreten Aufgaben: zugehörige Spec unter `docs/superpowers/specs/`
oder Plan unter `docs/superpowers/plans/`.
4. Branch-Zustand prüfen: `git log --oneline -10 spa main hugo-archive`.
5. Branch-Check: `git log --oneline -10 main`.
Dann erst Rückfragen oder Vorschläge formulieren.
## Drei Live-Webseiten
## Live-URLs
| URL | Inhalt | Wann anfassen |
|---|---|---|
| `joerg-lohrer.de` | Hugo-Seite (alt) | nur im finalen Cutover |
| `spa.joerg-lohrer.de` | Vanilla-Mini-Spike | als Referenz, aber nicht weiterentwickeln |
| `svelte.joerg-lohrer.de` | SvelteKit-SPA | **Haupt-Arbeitsziel** |
| URL | Rolle |
|---|---|
| `joerg-lohrer.de` | **Produktion**, SvelteKit-SPA (Cutover 2026-04-18, multilingual seit 2026-04-21) |
| `staging.joerg-lohrer.de` | Pre-Prod-Build |
| `svelte.joerg-lohrer.de` | Entwicklungs-Deploy-Target (historischer Default) |
| `spa.joerg-lohrer.de` | Vanilla-HTML-Spike (historisch) |
**Wichtig:** `scripts/deploy-svelte.sh` hat `DEPLOY_TARGET=svelte` als
Default — das zielt auf `svelte.joerg-lohrer.de`, NICHT auf die
Produktion. Für Prod-Deploy IMMER `DEPLOY_TARGET=prod` explizit setzen.
## Git-Branches
- `main` — kanonisch (Content, Specs, Pläne, Deploy-Scripts)
- `spa` — aktueller Arbeitszweig mit allen SvelteKit-Commits
- `hugo-archive` — Orphan, eingefrorener Hugo-Zustand
- `main` — kanonisch, alle Arbeit läuft hier direkt.
- `hugo-archive` — Orphan, eingefrorener Hugo-Zustand (Rollback-Option).
Specs und Pläne gehören auf `main`; SvelteKit-Code auf `spa`. Typischer
Workflow: committe Spec-Updates auf `main`, merge `main``spa` um
sie überall zu haben.
`spa` aus der Pre-Cutover-Phase ist gemerged und historisch.
## Sprache und Ton
- Antworten und Commit-Messages auf **Deutsch** (Kundensprache).
- Code-Kommentare auch auf Deutsch (wenn überhaupt).
- Identifier, Variablen, Funktionen auf **Englisch**.
- Code-Identifier (Variablen, Funktionen, Typen) auf Englisch.
- Kurz und konkret — Jörg ist technisch versiert, erwartet keine
Grundlagen-Erklärungen.
- Commit-Präfixe: `feat`, `fix`, `chore`, `docs`, `test` (conventional).
- Co-Author: `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`.
## Kernkonventionen
### Kanonisches URL-Schema
### Content-Struktur
- Post-URL ist **kurz**: `/<dtag>/` (z. B. `/dezentrale-oep-oer/`).
- Legacy-Hugo-URLs `/YYYY/MM/DD/<dtag>.html/` werden per SvelteKit-Load
auf die kurze Form 301-redirected (Backlink-Kompatibilität).
- Markdown-Posts pro Sprache: `content/posts/<lang>/<slug>/index.md`.
- Slug ist global eindeutig (also NICHT identisch zwischen Sprach-Varianten).
Der Slug wird zum `d`-Tag des Events und zur URL (`/<slug>/`).
- Sprach-Differenzierung über `l`-Tag (NIP-32), nicht über den Slug.
- Bidirektionale Verlinkung zwischen Sprach-Varianten via `a:`-Frontmatter,
wird als `['a', '<coord>', '', 'translation']` ins Event geschrieben.
### URL-Schema
- Post-URL: `/<slug>/` (z. B. `/bibel-selfies/`, `/bible-selfies/`). Keine
Sprach-Präfixe in der URL.
- Legacy-Hugo-URLs `/YYYY/MM/DD/<dtag>.html/` werden 301-redirected.
- Tag-Route: `/tag/<name>/`.
### Slug-Regel
Alle Slugs sind lowercase (Frontmatter `slug:`). Commit `d17410f` hat das
normalisiert. Keine Runtime-Transformation, beim Publishen 1:1 übernehmen.
### Nostr-Konstanten
- Pubkey (hex): `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41`
- npub: `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9`
- Bootstrap-Relay: `wss://relay.damus.io`
- Vollständige Relay-Liste: aus `kind:10002` des Autors (on-the-fly).
- Relay-Liste: aus `kind:10002` des Autors (zur Laufzeit geladen).
- Blossom-Server: aus `kind:10063` des Autors.
Zentralisiert in `app/src/lib/nostr/config.ts`.
- Zentralisiert in `app/src/lib/nostr/config.ts` bzw. `.env.local`.
### Signing
- **Im Browser (Kommentare):** NIP-07 via Extension (Alby, nos2x).
- **Aus der Kommandozeile (Publish):** NIP-46 via Amber-Bunker. Bunker-URL
in `.env.local` als `BUNKER_URL`.
- Privater Schlüssel **nie** im Repo, nie in CI-Secrets, nie in einer
Pipeline-Umgebung direkt.
- **Aus der Kommandozeile (Publish):** NIP-46 via Amber-Bunker.
- Privater Schlüssel **nie** im Repo, nie in CI-Secrets direkt.
## Wiederkehrende Kommandos
@ -83,96 +92,92 @@ Zentralisiert in `app/src/lib/nostr/config.ts`.
```sh
cd app
npm run dev # Dev-Server localhost:5173
npm run check # Type-Check (sollte 0 errors sein)
npm run test:unit # Vitest — aktuell 29 Tests
npm run test:e2e # Playwright — aktuell 3 Tests
npm run check # Type-Check (svelte-check)
npm run test:unit # Vitest
npm run test:e2e # Playwright
npm run build # Prod-Build nach app/build/
```
### Deploy nach `svelte.joerg-lohrer.de`
### Publish-Pipeline
```sh
cd app && npm run build && cd ..
./scripts/deploy-svelte.sh
cd publish
deno task check # pre-flight (Bunker, Relays, Blossom)
deno task publish --dry-run # diff-modus simulation
deno task publish # diff-modus real
deno task publish --force-all # alle 27 Posts
deno task publish --post <slug> # einzelner Post
deno task delete --event-id <hex> --reason "…" # NIP-09-Löschung
deno task validate-post ../content/posts/<lang>/<dir>/index.md
deno task test # Tests (73)
```
Das Script:
- liest `SVELTE_FTP_*` aus `.env.local`
- uploaded `app/build/*` per FTPS (TLS 1.2-Cap wegen All-Inkl-Bug)
- checkt `HTTP/2 200` am Ende
### Deploy
### Manuelles Publishen eines Posts (bis Publish-Pipeline fertig ist)
Siehe `docs/HANDOFF.md` Abschnitt „Manuelles Publishen". Kurz:
- Body aus Markdown-Frontmatter extrahieren (awk-Pattern dort)
- Bilder zu Blossom: `nak blossom upload --server https://blossom.edufeed.org --sec "$BUNKER_URL" <bild>`
- Event bauen mit `nak event -k 30023 -d <slug> -t title=... ...`
- Push zu allen Relays
```sh
DEPLOY_TARGET=staging ./scripts/deploy-svelte.sh # Pre-Prod
DEPLOY_TARGET=prod ./scripts/deploy-svelte.sh # Prod (joerg-lohrer.de)
```
### Nostr-Status checken
```sh
# Alle publizierten kind:30023-Events des Autors
nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io 2>/dev/null | jq -c '{d: (.tags[] | select(.[0]=="d") | .[1]), title: (.tags[] | select(.[0]=="title") | .[1])}'
# kind:10002 (Relay-Liste)
nak req -k 10002 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io
# kind:10063 (Blossom-Liste)
nak req -k 10063 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io
# Alle publizierten kind:30023-Events des Autors (inkl. l-Tag + a-Tags)
nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io 2>/dev/null | jq -c '{d: (.tags[] | select(.[0]=="d") | .[1]), l: (.tags[] | select(.[0]=="l") | .[1]), title: (.tags[] | select(.[0]=="title") | .[1])}'
```
## Tech-Stack-Eigenheiten, die man kennen muss
1. **Svelte 5 Runes:** `$props()`-Werte müssen via `$derived()` in lokale
Variablen abgeleitet werden — sonst `state_referenced_locally`-Warning.
1. **Svelte 5 Runes:** `$props()`-Werte via `$derived()` in lokale Variablen.
`$effect(() => { … event.id })` statt `onMount`, wenn bei Prop-Änderung
neu geladen werden muss (siehe `[...slug]/+page.svelte`).
2. **applesauce-relay v5.x API:** RxJS-basiert. `pool.request(relays, filter)`
liefert `Observable<NostrEvent>`. Die Loader in `app/src/lib/nostr/loaders.ts`
nutzen `toArray() + lastValueFrom + timeout + catchError`-Pattern.
**Nicht** das Tupel-Pattern `msg[0] === 'EVENT'` — das gehört in
alte nostr-tools-Beispiele, nicht hierher.
3. **DOMPurify braucht DOM:** im `renderMarkdown`-Helper gibt es einen
Early-Fail-Guard für Node-Aufrufe (SSR ist ohnehin aus).
3. **DOMPurify braucht DOM:** Early-Fail-Guard für Node-Aufrufe im
`renderMarkdown`-Helper. SSR ist ohnehin aus (`ssr = false` im Layout).
4. **All-Inkl-FTPS-Bug:** Data-Connection bricht bei TLS 1.3 ab.
`--tls-max 1.2` im curl-Call. Sobald SSH auf All-Inkl verfügbar ist
(Premium-Tarif angefragt), wird das Deploy-Script auf rsync umgestellt.
(Premium-Tarif angefragt), Umstellung auf rsync möglich.
5. **Amber-Bunker-Session:** bei neuer Bunker-URL müssen globale
Permissions in Amber zurückgesetzt werden. Sonst hängt `nak event`
auf die Signatur-Response.
Permissions in Amber auf „Allow + Always" für `get_public_key` und
`sign_event` gesetzt werden.
## Was nicht in Scope ist (laut Plan/Specs)
6. **Forgejo→GitHub Push-Mirror:** `git push` geht nach Forgejo, die
Action läuft auf GitHub (nachdem Forgejo gespiegelt hat). Push → Mirror →
Action braucht typisch 12 Minuten.
- Impressum-Inhalt (rechtliche Texte)
- Meta-Stubs pro Post (kommt via Publish-Pipeline Phase 3)
- Menü-Navigation (einfach nachrüstbar, aber nicht priorisiert)
- Eigener Relay (ideologischer Evolutionspfad, nicht Phase 1)
- Eigener Blossom-Server (dito)
7. **svelte-i18n + activeLocale:** `$t('key')` in Templates, `get(t)('key')`
in imperativem Script-Code. `activeLocale` ist der projekteigene Store
(persistiert via `localStorage`), `locale` aus svelte-i18n wird
automatisch synchronisiert.
8. **zsh-Globbing:** Pfade mit eckigen Klammern (z. B. `app/src/routes/[...slug]/`)
müssen in `git add` in einfachen Anführungszeichen stehen, sonst
interpretiert zsh das als Glob-Pattern.
## Wie mit Jörg arbeiten
- **Kurze Antworten**, konkrete Optionen, nicht lang umherreden.
- **Kurze Antworten**, konkrete Optionen, keine Grundlagen-Erklärungen.
- Bei mehreren Wegen: 23 Varianten mit Empfehlung nennen, nicht alles
aufzählen.
- Commit-Nachrichten: imperativ, auf Deutsch, mit Kontext im Body.
Co-Author: `Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>`.
- Vor dem Dispatchen von Subagents: kritische API-Details der Libraries
manuell verifizieren (Plan-Annahmen können alte Versionsstände
widerspiegeln). Beispiel: applesauce-relay API war nicht so wie im Plan
beschrieben — Subagent mit aktueller API briefen statt blind vertrauen.
- Nach jedem Feature-Commit: Build + Deploy, damit Jörg live sehen kann.
Das ist in diesem Workflow wichtig, weil UI-Feedback oft Layout-Fragen
aufwirft, die kein Test entdeckt.
- Spec-Updates auf `main` committen, dort läuft alle Arbeit.
- Nach Feature-Commits: Build + Deploy, damit Jörg live sehen kann.
UI-Feedback fängt Layout-Fragen ab, die Tests nicht entdecken.
- Vor Subagent-Dispatch: kritische API-Details verifizieren
(Plan-Annahmen können veraltet sein).
## Credentials / Secrets
Alle in `.env.local` (gitignored). Variablen:
Alle in `.env.local` (gitignored):
- `BUNKER_URL` — Amber-NIP-46-Pairing für Signaturen
- `SPA_FTP_HOST/USER/PASS/REMOTE_PATH` — FTPS nach spa.joerg-lohrer.de
- `SVELTE_FTP_HOST/USER/PASS/REMOTE_PATH` — FTPS nach svelte.joerg-lohrer.de
- `CLIENT_SECRET_HEX` — identisch mit GitHub-Secret (stabile App-ID in Amber)
- `AUTHOR_PUBKEY_HEX`, `BOOTSTRAP_RELAY`
- `SVELTE_FTP_*`, `STAGING_FTP_*` — FTPS-Credentials pro Deploy-Target
Falls neue Bunker-URL nötig (Amber-Session kaputt):
- In Amber neue Bunker-URL generieren

105
CLAUDE.md Normal file
View File

@ -0,0 +1,105 @@
# CLAUDE.md — Einstieg für Claude-Sessions
Dieser Einstieg ist für Claude-Code-Sessions gedacht. Für den inhaltlichen
Projektstand siehe [`docs/STATUS.md`](docs/STATUS.md) und
[`docs/HANDOFF.md`](docs/HANDOFF.md).
## Was dieses Repo ist
Die persönliche Webseite [`joerg-lohrer.de`](https://joerg-lohrer.de/) als
SvelteKit-SPA, die Blog-Posts live aus Nostr-Events (NIP-23, `kind:30023`)
auf 5 Public-Relays rendert. Seit 2026-04-21 mehrsprachig (DE/EN).
## Einstiegsreihenfolge
1. Diese Datei (Agent-Konventionen, Fallstricke).
2. [`docs/STATUS.md`](docs/STATUS.md) — wo steht alles gerade.
3. [`docs/HANDOFF.md`](docs/HANDOFF.md) — Alltags-Workflow, Stolperfallen.
4. Für konkrete Aufgaben: Spec unter `docs/superpowers/specs/`, Plan unter
`docs/superpowers/plans/`.
## Sprache und Ton
- **Antworten und Commit-Messages auf Deutsch.**
- Code-Identifier auf Englisch.
- Kurz, konkret, kein Grundlagen-Tutorial. Jörg ist technisch versiert.
- Bei mehreren Wegen: 23 Varianten mit Empfehlung, nicht alles aufzählen.
## Commit-Konvention
- Conventional-Commit-Präfixe: `feat`, `fix`, `chore`, `docs`, `test`.
- Imperativ, Deutsch, Body erklärt das *Warum*.
- Co-Author immer ergänzen:
```
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```
## Kritische Fallstricke
### 1. Deploy-Target
`scripts/deploy-svelte.sh` hat `DEPLOY_TARGET=svelte` als Default —
das zielt auf `svelte.joerg-lohrer.de`, NICHT auf die Produktion.
Für Live-Deploy auf `joerg-lohrer.de`:
```sh
DEPLOY_TARGET=prod ./scripts/deploy-svelte.sh
```
**Immer explizit setzen.** Der stumme Default-Fehler ist nur sichtbar,
wenn man die Live-Seite kontrolliert. Reproduzierbar als Memory-Entry
im Claude-Memory-System.
### 2. zsh-Globbing mit eckigen Klammern
SvelteKit-Routen wie `app/src/routes/[...slug]/+page.svelte` enthalten
eckige Klammern, die zsh als Glob-Pattern interpretiert. Pfade IMMER in
einfachen Anführungszeichen:
```sh
git add 'app/src/routes/[...slug]/+page.svelte'
```
### 3. Forgejo → GitHub Push-Mirror
`git push` landet zuerst auf Forgejo (`forgejo.joerglohrer.synology.me`).
Der Forgejo-Mirror synct dann zu GitHub (typisch 3090 s). Die GitHub-
Action (Publish-Pipeline) läuft erst nach dem Mirror. Wer direkt nach
`git push` `gh run list` aufruft, sieht evtl. noch keinen neuen Run.
### 4. Deno-Path-Konventionen
Publish-Pipeline läuft aus `publish/` (CWD), daher sind Pfade relativ
mit `../content/posts/...`. Git-Diff liefert aber repo-root-relative
Pfade (`content/posts/...`). `changedPostDirs` normalisiert beides —
wenn `posts=0` obwohl Änderungen vorliegen, ist das der Hotspot.
### 5. Publish-Pipeline erwartet `content/posts/<lang>/<slug>/`
Die Zwei-Ebenen-Struktur ist Teil der Traversierung. Wer einen Post
versehentlich in `content/posts/<slug>/` (ohne Sprach-Ordner) anlegt,
wird von der Pipeline ignoriert.
## Hauptarbeitsbereiche im Repo
| Pfad | Inhalt |
|---|---|
| `content/posts/<lang>/<slug>/index.md` | Markdown-Posts pro Sprache |
| `app/src/lib/i18n/` | UI-Lokalisierung (svelte-i18n, activeLocale-Store) |
| `app/src/lib/nostr/` | Relay-Loader, Translations-Resolving |
| `app/src/lib/components/` | Svelte-5-Runes-Komponenten |
| `app/src/routes/` | SvelteKit-Routen (Layout, Home, Archiv, Post, Impressum) |
| `publish/src/` | Deno-Publish-Pipeline (Deno-Tasks in `publish/deno.jsonc`) |
| `publish/tests/` | Deno-Tests für die Pipeline |
| `docs/superpowers/specs/` | Produktdesigns, Konventionen |
| `docs/superpowers/plans/` | Implementierungspläne (alle erledigt) |
| `scripts/deploy-svelte.sh` | FTPS-Deploy |
## Quick-Links
- [Produktspezifikation SPA](docs/superpowers/specs/2026-04-15-nostr-page-design.md)
- [Produktspezifikation Publish-Pipeline](docs/superpowers/specs/2026-04-15-publish-pipeline-design.md)
- [Bild-Metadaten-Konvention](docs/superpowers/specs/2026-04-16-image-metadata-convention.md)
- [Multilingual-Design](docs/superpowers/specs/2026-04-21-multilingual-posts-design.md)
- [Repo-Workflow-Skill](.claude/skills/joerglohrerde-workflow.md) (ausführlicher, mit Kommandos)

View File

@ -6,7 +6,7 @@ Blog-Posts live aus signierten Nostr-Events (NIP-23, `kind:30023`) rendert.
## Aktueller Stand
- **`https://joerg-lohrer.de/`** — SvelteKit-SPA, Cutover am 2026-04-18 erfolgt.
- **`https://joerg-lohrer.de/`** — SvelteKit-SPA, seit 2026-04-18 live. Seit 2026-04-21 **multilingual** (Deutsch + Englisch via NIP-32 `l`-Tag und NIP-33-`a`-Tag-Verlinkung).
- **`https://staging.joerg-lohrer.de/`** — Staging (gleicher Build, ein Schritt vor Prod).
- **`https://svelte.joerg-lohrer.de/`** — Entwicklungs-Deploy-Target der Pipeline.
- **`https://spa.joerg-lohrer.de/`** — Vanilla-HTML-Mini-Spike (Proof of Concept, historisch).
@ -15,13 +15,20 @@ Detailliert in [`docs/STATUS.md`](docs/STATUS.md).
## Wie die Seite funktioniert
1. **Inhalte** liegen als Markdown in `content/posts/<slug>/index.md` mit
1. **Inhalte** liegen als Markdown in `content/posts/<lang>/<slug>/index.md`
(z. B. `content/posts/de/<slug>/` oder `content/posts/en/<slug>/`) mit
strukturierten Bild-Metadaten im Frontmatter (Alt-Text, Lizenz, Autor:innen).
Übersetzungen eines Posts werden über bidirektionale `a:`-Tags im
Frontmatter verlinkt — Details in
[`docs/superpowers/specs/2026-04-21-multilingual-posts-design.md`](docs/superpowers/specs/2026-04-21-multilingual-posts-design.md).
2. **Publish-Pipeline** (`publish/`, Deno) lädt Bilder auf Blossom-Server
(content-addressed) und publiziert signierte `kind:30023`-Events via
NIP-46-Bunker (Amber) auf 5 Relays.
NIP-46-Bunker (Amber) auf 5 Relays — inkl. NIP-32 `l`-Tag (Sprache) und
NIP-33 `a`-Tag (Verlinkung zu anderssprachigen Varianten).
3. **SvelteKit-SPA** (`app/`) lädt diese Events zur Laufzeit und rendert
Post-Liste + Detailseiten. Keine Server-Komponente, Static-Hosting reicht.
Post-Liste + Detailseiten. UI-Chrome via `svelte-i18n` (DE/EN), Browser-
Locale als Default, Listen nach aktivem Locale gefiltert. Keine
Server-Komponente, Static-Hosting reicht.
4. **CI**: GitHub Actions triggert die Publish-Pipeline bei Push auf `main`
(via Forgejo→GitHub Push-Mirror).
@ -35,11 +42,16 @@ Identität und Assets:
- 📍 **Stand und Live-URLs:** [`docs/STATUS.md`](docs/STATUS.md)
- 🔜 **Wie es weitergeht:** [`docs/HANDOFF.md`](docs/HANDOFF.md)
- 🤖 **Claude-Einstieg:** [`CLAUDE.md`](CLAUDE.md) (Agent-Konventionen, Deploy-Falle, Commit-Stil)
- 📐 **SPA-Spec:** [`docs/superpowers/specs/2026-04-15-nostr-page-design.md`](docs/superpowers/specs/2026-04-15-nostr-page-design.md)
- 📐 **Publish-Pipeline-Spec:** [`docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`](docs/superpowers/specs/2026-04-15-publish-pipeline-design.md)
- 📐 **Bild-Metadaten-Konvention:** [`docs/superpowers/specs/2026-04-16-image-metadata-convention.md`](docs/superpowers/specs/2026-04-16-image-metadata-convention.md)
- 🛠 **SvelteKit-SPA-Plan:** [`docs/superpowers/plans/2026-04-15-spa-sveltekit.md`](docs/superpowers/plans/2026-04-15-spa-sveltekit.md) (35 Tasks, abgeschlossen)
- 🛠 **Publish-Pipeline-Plan:** [`docs/superpowers/plans/2026-04-16-publish-pipeline.md`](docs/superpowers/plans/2026-04-16-publish-pipeline.md) (24 Tasks, abgeschlossen)
- 📐 **Multilinguale Posts:** [`docs/superpowers/specs/2026-04-21-multilingual-posts-design.md`](docs/superpowers/specs/2026-04-21-multilingual-posts-design.md)
- 🛠 **SvelteKit-SPA-Plan:** [`docs/superpowers/plans/2026-04-15-spa-sveltekit.md`](docs/superpowers/plans/2026-04-15-spa-sveltekit.md) (35 Tasks, erledigt)
- 🛠 **Publish-Pipeline-Plan:** [`docs/superpowers/plans/2026-04-16-publish-pipeline.md`](docs/superpowers/plans/2026-04-16-publish-pipeline.md) (24 Tasks, erledigt)
- 🛠 **Multilingual 1/3 — Pipeline:** [`docs/superpowers/plans/2026-04-21-multilingual-posts-pipeline.md`](docs/superpowers/plans/2026-04-21-multilingual-posts-pipeline.md) (10 Tasks, erledigt)
- 🛠 **Multilingual 2/3 — SPA-Auflösung:** [`docs/superpowers/plans/2026-04-21-multilingual-posts-spa.md`](docs/superpowers/plans/2026-04-21-multilingual-posts-spa.md) (8 Tasks, erledigt)
- 🛠 **Multilingual 3/3 — UI-i18n:** [`docs/superpowers/plans/2026-04-21-multilingual-posts-i18n.md`](docs/superpowers/plans/2026-04-21-multilingual-posts-i18n.md) (11 Tasks, erledigt)
- 🤖 **Claude-Workflow-Skill:** [`.claude/skills/joerglohrerde-workflow.md`](.claude/skills/joerglohrerde-workflow.md)
## Branches
@ -52,9 +64,11 @@ Identität und Assets:
## Repo-Struktur
```
content/posts/ Markdown-Posts (Quelle für Nostr-Events, 18 Stück)
content/posts/<lang>/<slug>/ Markdown-Posts pro Sprache (26× de, 1× en)
content/impressum.md Statisches Impressum (wird von SPA geladen)
app/ SvelteKit-SPA (Laufzeit-Renderer)
src/lib/i18n/ UI-Lokalisierung (svelte-i18n + Messages)
src/lib/nostr/ Relay-Loader, Translations-Resolving
publish/ Deno-Publish-Pipeline (Blossom + Nostr)
preview/spa-mini/ Vanilla-HTML-Mini-Spike (historische Referenz)
scripts/deploy-svelte.sh FTPS-Deploy, Targets: svelte/staging/prod
@ -62,6 +76,7 @@ static/ Site-Assets (Favicons, Profilbild, .well-known/)
docs/ Specs, Pläne, Status, Handoff, Wiki-Entwürfe
.github/workflows/ GitHub-Actions CI (Publish-Pipeline-Trigger)
.claude/ Claude-Code-Sessions (Transparenz) + Skills
CLAUDE.md Einstiegspunkt für Claude-Sessions
```
## Entwicklung

View File

@ -5,11 +5,13 @@ Dieses Dokument sagt: was ist der Zustand, was wartet, wo liegen die Fäden.
## Zustand (Details in `STATUS.md`)
**Cutover + Reimport am 2026-04-18 abgeschlossen.** `joerg-lohrer.de`
läuft als SvelteKit-SPA, rendert 26 Nostr-Langform-Posts live aus 5
Relays, Bilder auf Blossom. Repo ist alleinige Quelle der Wahrheit.
Pipeline-Subcommands `publish` + `delete` decken den kompletten
Content-Lifecycle ab.
**Cutover + Reimport 2026-04-18, Mehrsprachigkeit live seit 2026-04-21.**
`joerg-lohrer.de` läuft als SvelteKit-SPA, rendert 27 Nostr-Langform-Posts
(26 DE + 1 EN) live aus 5 Relays, Bilder auf Blossom. UI-Chrome via
`svelte-i18n` in DE/EN, Header-Switcher, Listen-Filter nach aktivem Locale,
bidirektionale Sprach-Verlinkung der Posts via NIP-33 `a`-Tag mit Marker
`translation`. Repo ist alleinige Quelle der Wahrheit. Pipeline-
Subcommands `publish` + `delete` decken den kompletten Content-Lifecycle ab.
**Das inhaltliche Kernziel des Gesamtprojekts ist erreicht.** Der Rest
sind optionale Verbesserungen.
@ -18,11 +20,12 @@ sind optionale Verbesserungen.
**Kompletter Happy-Path, kein manueller Publish nötig:**
1. Neuen Ordner anlegen: `content/posts/YYYY-MM-DD-<slug>/`
1. Neuen Ordner anlegen: `content/posts/de/YYYY-MM-DD-<slug>/` (oder
`content/posts/en/<slug>/` für Englisch).
2. `index.md` schreiben mit Frontmatter (siehe Template unten).
3. Bilder in den Ordner legen und im Markdown als `![alt](bildname.jpg)`
referenzieren.
4. Lokal validieren: `cd publish && deno task validate-post ../content/posts/<dir>/index.md`
4. Lokal validieren: `cd publish && deno task validate-post ../content/posts/<lang>/<dir>/index.md`
5. Commit + `git push origin main` — fertig.
**Was automatisch passiert:**
@ -45,7 +48,7 @@ gesetzt haben. Das gilt so lange, bis der Client-Key rotiert wird.
---
title: "Titel des Posts"
slug: "url-freundlicher-slug"
date: 2026-04-18
date: 2026-04-21
description: "Kurzbeschreibung für SEO und den summary-Tag im Event."
image: hauptbild.jpg
tags:
@ -53,11 +56,17 @@ tags:
- Tag2
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
# a:
# - "30023:4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41:<slug-der-anderssprachigen-variante>"
---
Body in Markdown…
```
Der auskommentierte `a:`-Block ist **Konvention für alle neuen Posts** — so
lässt sich später eine Übersetzung dazu verlinken, ohne das Template zu
suchen. Siehe Abschnitt „Wie man eine Übersetzung anlegt" weiter unten.
Bilder mit voller Attribution (NIP-standardisiert nach unserer Konvention,
siehe `docs/superpowers/specs/2026-04-16-image-metadata-convention.md`):
@ -100,24 +109,47 @@ gefiltert. Defensive Maßnahme für zukünftige Duplikate / Soft-Deletes.
User-Task: im All-Inkl KAS als Weiterleitung anlegen. Der Link im
Footer und in den Social-Icons zeigt bereits darauf.
### Option D — Mehrsprachigkeit (Translation-of)
### Wie man eine Übersetzung anlegt (Konvention seit 2026-04-21)
**Grundlage steht:** Pipeline taggt seit 2026-04-18 jedes Event mit
NIP-32 `['L', 'ISO-639-1']` + `['l', 'de', 'ISO-639-1']` (default),
überschreibbar per `lang:`-Frontmatter.
**Kurz:** Pro Sprache ein eigener Unterordner unter `content/posts/<lang>/`,
pro Sprache ein eigenes `kind:30023`-Event mit eigenem Slug (= `d`-Tag).
Die Beziehung zwischen Sprach-Varianten kommt ausschließlich über
bidirektionale `a`-Tags im Frontmatter.
**Zu tun für einen bilingualen Post:**
1. Zweiter Markdown-Ordner, z. B. `content/posts/<date>-<slug>-en/index.md`,
mit `slug: <slug>-en`, `lang: en`, englischem Body.
2. Publish → eigenes `kind:30023`-Event mit `lang=en`.
3. (Noch zu bauen) Pipeline erweitern: `translation_of:`-Frontmatter-Feld,
das ein `['a', '30023:pubkey:<slug-de>']`-Tag ins Event setzt. Damit
erkennen Clients wie Habla die Verwandtschaft.
4. (Optional) SPA bekommt Language-Switcher auf der Post-Detailseite.
**Schritt für Schritt:**
Nicht dringend, erst wenn echter englischer Content entsteht.
1. Neuen Ordner für die Übersetzung anlegen, z. B.
`content/posts/en/<eigener-slug>/index.md`. **Der Slug muss global
eindeutig sein** — also *nicht* identisch mit dem deutschen Slug. Beispiel:
`bibel-selfies` (DE) ↔ `bible-selfies` (EN).
### Option E — Pipeline weg von GitHub (self-hosted CI)
2. Frontmatter mit `lang: en` (oder jeweiliger Sprach-Code) und aktivem
`a:`-Verweis auf den Slug der anderen Sprach-Variante:
```yaml
a:
- "30023:4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41:<slug-der-anderen-sprache>"
```
3. **Bidirektional**: im Original-Post den bereits auskommentierten
`a:`-Platzhalter aktivieren (Kommentarzeichen entfernen, Slug einsetzen).
Beide Posts verweisen dann aufeinander.
4. Commit + Push — die Action re-publisht beide Events, `a`-Tags landen im
Nostr-Event als `['a', '<coord>', '', 'translation']`. Die SPA erkennt
die Beziehung automatisch und zeigt den Sprach-Switcher (`📖 DE | EN`)
unter dem Post-Titel.
**Was die SPA automatisch tut:**
- Listen-Seiten (Startseite + Archiv) filtern nach aktivem Locale — englische
Besucher:innen sehen nur englische Posts.
- Klick auf den anderen Sprachcode im Switcher setzt `activeLocale` global
und navigiert zum verknüpften Slug.
- UI-Chrome (Menü, Footer, Meta-Zeile, Datumsformat) wechselt mit.
**Details:** [`docs/superpowers/specs/2026-04-21-multilingual-posts-design.md`](superpowers/specs/2026-04-21-multilingual-posts-design.md).
### Option D — Pipeline weg von GitHub (self-hosted CI)
**Wann:** Wenn der Optiplex-Server steht und ein zentraler Ort für Dienste
existiert.
@ -131,7 +163,7 @@ existiert.
Der Pipeline-Code selbst (`publish/src/**`) ist CI-agnostisch — nur die
Trigger-Konfiguration ändert sich.
### Option F — Design-Refinements
### Option E — Design-Refinements
**Wann:** irgendwann, wenn Lust drauf ist.
@ -190,20 +222,25 @@ cd publish && deno task test # tests
- **Hugo-quotierte Dates:** `date: "2023-02-26"` ist ein YAML-String, nicht
ein Date-Objekt. `validatePost` coerced das automatisch; in neuen Posts
am besten ohne Quotes schreiben.
- **Deploy-Targets:** `svelte` → Entwicklung, `staging` → Pre-Prod,
- **Deploy-Targets:** `svelte` (Default!) → Entwicklung, `staging` → Pre-Prod,
`prod``joerglohrer26/` (Produktion seit Cutover). Script parst
`.env.local` per awk (wegen Sonderzeichen in FTP-Passwörtern).
**Für Live-Deploy auf `joerg-lohrer.de` IMMER explizit `DEPLOY_TARGET=prod`
setzen** — der Default zielt auf `svelte.joerg-lohrer.de` (historischer
Cutover-Stand), ein stummer Fehler wenn man es vergisst.
- **Slug-Hygiene:** nur `[a-z0-9-]`, keine Umlaute/Emojis/Doppelpunkte.
Der Slug landet als `d`-Tag im Event und wird zur URL. Einmal
publiziert, ist Umbenennen nur über Delete + Re-Publish mit neuem Slug
möglich.
möglich. **Sprach-Varianten brauchen eigene Slugs** (z. B. `bibel-selfies`
/ `bible-selfies`) — die Sprache kommt über den `l`-Tag, nicht über den
`d`-Tag.
- **Clients, die Markdown ignorieren:** Yakihonne/Habla kennen NIP-32
Sprach-Tags; kurzen Text in `description:` halten, damit die Vorschau
überall sinnvoll aussieht.
## Offene UNKNOWN-Einträge zur späteren Recherche
Im VR-Post (`content/posts/2021-08-15-virtual-reality/index.md`) sind
Im VR-Post (`content/posts/de/2021-08-15-virtual-reality/index.md`) sind
4 Bilder als `license: UNKNOWN / authors: UNKNOWN` markiert:
- `01-immersion-wikipedia.jpg` (Wikipedia-Screenshot)
- `02-mittelalterliche-kirche.jpg` (Sketchfab — Lizenz ist CC BY-NC, Fotograf fehlt)
@ -220,7 +257,7 @@ Hilfreich beim Wiedereinstieg mit Claude:
- Live-Check: `curl -sI https://joerg-lohrer.de/`
- Event-Count Repo vs. Relays:
```sh
ls content/posts/ | wc -l
find content/posts -mindepth 3 -name index.md | wc -l
nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.edufeed.org 2>/dev/null | jq -r '.tags[]|select(.[0]=="d")|.[1]' | sort -u | wc -l
```
- Pipeline-Tests: `cd publish && deno task test`

View File

@ -1,6 +1,6 @@
# Projekt-Status: joerg-lohrer.de → Nostr-basierte SPA
**Stand:** 2026-04-18 (Cutover abgeschlossen)
**Stand:** 2026-04-21 (Mehrsprachigkeit live)
## Kurzfassung
@ -9,6 +9,14 @@ signierten Nostr-Events (NIP-23, `kind:30023`) auf 5 Public-Relays rendert.
Bilder liegen content-addressed auf 2 Blossom-Servern. Die Hugo-basierte
Altseite ist als `hugo-archive`-Branch eingefroren.
**Seit 2026-04-21 multilingual:** UI-Chrome (Menü, Footer, Post-Meta)
in Deutsch und Englisch via `svelte-i18n`, mit Browser-Locale-Default,
`localStorage`-Persistenz und Header-Sprachswitcher. Inhalte pro Sprache
als eigene `kind:30023`-Events, verlinkt über bidirektionale
NIP-33-`a`-Tags mit Marker `translation`; Listen-Seiten filtern nach
aktivem Locale. Eine englische Übersetzung existiert bereits
(`bible-selfies`) und dient als lebendes Referenzbeispiel.
**Das inhaltliche Kernziel des Gesamtprojekts ist erreicht.**
## Live-URLs
@ -25,13 +33,15 @@ Altseite ist als `hugo-archive`-Branch eingefroren.
- **Autoren-Pubkey:** `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9`
(hex: `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41`)
- **NIP-05:** `joerglohrer@joerg-lohrer.de` (via `/.well-known/nostr.json`)
- **Publizierte Events:** **26 Langform-Posts** (`kind:30023`), alle mit
sauberen ASCII-slugs, alle aus dem Repo publiziert. 18 Alt-Posts aus der
Hugo-Migration plus 8 re-importierte Client-Posts (Habla/Yakihonne), die
mit bereinigten d-tags neu publiziert und alte Duplikate per NIP-09
gelöscht wurden (Commit `7186c32`).
- **Publizierte Events:** **27 Langform-Posts** (`kind:30023`) —
26 Deutsch + 1 Englisch. 26 Alt-Posts (18 Hugo-Migration + 8 Client-
Reimport) tragen seit 2026-04-21 konsistent `lang: de` im Frontmatter,
`bible-selfies` (EN, 2026-04-21) verweist bidirektional auf `bibel-selfies`
via NIP-33-`a`-Tag mit Marker `translation`.
- **NIP-32-Sprach-Tags:** Alle Events tragen `['L', 'ISO-639-1']` +
`['l', 'de', 'ISO-639-1']`. Grundlage für spätere Mehrsprachigkeit.
`['l', <lang>, 'ISO-639-1']`. Deutsche Events haben `lang=de`, englische
`lang=en`. Ergänzt durch `['a', '<kind>:<pubkey>:<d-tag>', '', 'translation']`
bei verknüpften Sprach-Varianten.
- **Relay-Liste** (`kind:10002`): `relay.damus.io`, `nos.lol`,
`relay.primal.net`, `relay.tchncs.de`, `relay.edufeed.org`
- **Blossom-Server** (`kind:10063`): `blossom.edufeed.org`, `blossom.primal.net`
@ -42,30 +52,32 @@ Altseite ist als `hugo-archive`-Branch eingefroren.
```
joerglohrerde/
├── content/posts/ # 18 Markdown-Posts, alle mit strukturierten images:
├── content/impressum.md # Statisches Impressum (wird von SPA geladen)
├── app/ # SvelteKit-SPA (Laufzeit-Renderer)
├── publish/ # Deno-Publish-Pipeline (Blossom + Nostr)
├── preview/spa-mini/ # Vanilla-HTML-Mini-Spike (historisch)
├── content/posts/<lang>/<slug>/ # Markdown-Posts pro Sprache (26 de, 1 en)
├── content/impressum.md # Statisches Impressum (wird von SPA geladen)
├── app/
│ ├── src/lib/i18n/ # svelte-i18n + activeLocale-Store + Messages
│ ├── src/lib/nostr/ # Relay-Loader, Translations-Resolving
│ └── src/lib/components/ # u. a. LanguageSwitcher, LanguageAvailability
├── publish/ # Deno-Publish-Pipeline (Blossom + Nostr)
├── preview/spa-mini/ # Vanilla-HTML-Mini-Spike (historisch)
├── scripts/
│ └── deploy-svelte.sh # FTPS-Deploy, Targets: svelte/staging/prod
│ └── deploy-svelte.sh # FTPS-Deploy, Targets: svelte/staging/prod
├── docs/
│ ├── STATUS.md # Dieses Dokument
│ ├── HANDOFF.md # Wie man hier weitermacht
│ ├── STATUS.md # Dieses Dokument
│ ├── HANDOFF.md # Wie man hier weitermacht
│ ├── redaktion-bild-metadaten.md
│ ├── wiki-entwurf-nostr-bild-metadaten.md
│ ├── wiki-draft-nostr-image-metadata.md
│ ├── github-ci-setup.md
│ └── superpowers/
│ ├── specs/ # SPA + Publish-Pipeline + Bild-Metadaten-Konvention
│ └── plans/
│ ├── 2026-04-15-spa-sveltekit.md # erledigt
│ └── 2026-04-16-publish-pipeline.md # erledigt
├── .github/workflows/ # publish.yml (Forgejo→GitHub Push-Mirror-Trigger)
│ ├── specs/ # SPA, Publish-Pipeline, Bild-Metadaten, Multilingual
│ └── plans/ # Alle Pläne erledigt (SPA, Pipeline, 3× Multilingual)
├── .github/workflows/ # publish.yml (Forgejo→GitHub Push-Mirror-Trigger)
├── .claude/
│ ├── skills/ # Repo-spezifischer Claude-Skill
│ └── settings.local.json # Claude-Session-State (gitignored)
└── .env.local # Gitignored: FTP-Creds, Bunker-URL, Publish-Pipeline-Keys
│ ├── skills/ # Repo-spezifischer Claude-Skill
│ └── settings.local.json # Claude-Session-State (gitignored)
├── CLAUDE.md # Einstiegspunkt für Claude-Sessions
└── .env.local # Gitignored: FTP-Creds, Bunker-URL, Publish-Pipeline-Keys
```
## Branch-Layout (Git)
@ -93,10 +105,11 @@ Einmalig manuell erledigt (gitignored in `.env.local`):
Nach Priorität:
1. **Postfach `webmaster@joerg-lohrer.de`** als Weiterleitung in KAS anlegen.
2. **SPA respektiert NIP-09-Deletion-Events** (defensiver kind:5-Filter).
3. **Mehrsprachigkeit** — parallele `lang:en`-Versionen bei Bedarf anlegen,
per `a`-Tag als `translation_of` verlinken (NIP-32-Grundlage steht).
4. **Self-hosted CI** (Woodpecker / Cron auf Optiplex), weg von GitHub.
5. **5 UNKNOWN-Einträge** im VR-Post zur späteren Recherche.
3. **Self-hosted CI** (Woodpecker / Cron auf Optiplex), weg von GitHub.
4. **5 UNKNOWN-Einträge** im VR-Post zur späteren Recherche.
5. **Weitere Übersetzungen** nach Bedarf — Framework ist sprach-agnostisch,
neuer Sprach-Unterordner (z. B. `content/posts/fr/`) genügt, UI-i18n-
Messages ergänzen.
## Erledigt (chronologisch seit 2026-04-15)
@ -126,6 +139,20 @@ Nach Priorität:
nutzt stabile Bunker-Identität via `CLIENT_SECRET_HEX`.
- ✅ **NIP-32 Sprach-Tags** in `buildKind30023` (Default `de`, über
`lang:`-Frontmatter überschreibbar).
- ✅ **Multilinguale Posts (2026-04-21)** — drei sequentielle Pläne
(Pipeline, SPA-Resolving, UI-i18n) abgeschlossen:
- Publish-Pipeline traversiert `content/posts/<lang>/<slug>/`, akzeptiert
`a:`-Tags im Frontmatter und schreibt sie als NIP-33-Koordinaten mit
Marker `translation` ins Event.
- SPA löst `a`-Tags auf, zeigt kompakten Switcher im Post (`📖 DE | EN`),
Klick setzt globalen Locale-State und navigiert zur Sprach-Variante.
- UI-Chrome via `svelte-i18n`, `activeLocale`-Store mit `localStorage`-
Persistenz, Listen-Seiten nach aktivem Locale gefiltert.
- Erste englische Übersetzung `bible-selfies` existiert als lebendes
Referenzbeispiel.
- Zwei Publisher-Pipeline-Bugfixes (`contentRoot`-Pfad-Handling) und
ein Route-Refresh-Bug (`onMount` → `$effect`) dabei nebenbei
bereinigt — GitHub-Action re-publisht nun wirklich auf Content-Änderung.
## Live-Verifikation

View File

@ -1,9 +1,19 @@
# Multilinguale Posts — Design
**Datum:** 2026-04-21
**Status:** Design, noch nicht implementiert
**Status:** Umgesetzt. Live seit 2026-04-21 via drei Pläne (Pipeline, SPA-Resolving, UI-i18n).
**Scope:** Posts der SPA in mehreren Sprachen anbieten; UI-Chrome lokalisieren; Publish-Pipeline entsprechend anpassen.
## Umsetzungshinweis
Das Design unten beschreibt den angenommenen Produktstand. Während der
Implementierung gab es eine kleine Abweichung beim Sprach-Hinweis im Post:
Statt „Auch verfügbar in: English" wird ein kompakter Switcher gerendert
(`📖 DE | EN`), der Sprachcode + globale Locale-Umschaltung in einem
Klick kombiniert. Grund: UI-Sprache und Anzeige-Sprache bleiben
konsistent, Switcher-Stil identisch zum Header. Siehe
[`docs/HANDOFF.md`](../../HANDOFF.md) für das Nutzer:innen-Verhalten.
## Ziel
Posts können in beliebigen Sprachen existieren. Posts, die inhaltlich dasselbe Thema in unterschiedlichen Sprachen behandeln, werden über nostr-native Referenzen verknüpft, sodass die SPA eine Sprachwahl anbieten kann. Das Repo bleibt Quelle der Wahrheit; die GitHub-Action publisht weiterhin automatisch.