diff --git a/.claude/skills/joerglohrerde-workflow.md b/.claude/skills/joerglohrerde-workflow.md index 56daa4c..d671a38 100644 --- a/.claude/skills/joerglohrerde-workflow.md +++ b/.claude/skills/joerglohrerde-workflow.md @@ -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) `. ## Kernkonventionen -### Kanonisches URL-Schema +### Content-Struktur -- Post-URL ist **kurz**: `//` (z. B. `/dezentrale-oep-oer/`). -- Legacy-Hugo-URLs `/YYYY/MM/DD/.html/` werden per SvelteKit-Load - auf die kurze Form 301-redirected (Backlink-Kompatibilität). +- Markdown-Posts pro Sprache: `content/posts///index.md`. +- Slug ist global eindeutig (also NICHT identisch zwischen Sprach-Varianten). + Der Slug wird zum `d`-Tag des Events und zur URL (`//`). +- Sprach-Differenzierung über `l`-Tag (NIP-32), nicht über den Slug. +- Bidirektionale Verlinkung zwischen Sprach-Varianten via `a:`-Frontmatter, + wird als `['a', '', '', 'translation']` ins Event geschrieben. + +### URL-Schema + +- Post-URL: `//` (z. B. `/bibel-selfies/`, `/bible-selfies/`). Keine + Sprach-Präfixe in der URL. +- Legacy-Hugo-URLs `/YYYY/MM/DD/.html/` werden 301-redirected. - Tag-Route: `/tag//`. -### 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 # einzelner Post +deno task delete --event-id --reason "…" # NIP-09-Löschung +deno task validate-post ../content/posts///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" ` -- Event bauen mit `nak event -k 30023 -d -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`. 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 1–2 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: 2–3 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) `. -- 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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5fba03a --- /dev/null +++ b/CLAUDE.md @@ -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: 2–3 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) + ``` + +## 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 30–90 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///` + +Die Zwei-Ebenen-Struktur ist Teil der Traversierung. Wer einen Post +versehentlich in `content/posts//` (ohne Sprach-Ordner) anlegt, +wird von der Pipeline ignoriert. + +## Hauptarbeitsbereiche im Repo + +| Pfad | Inhalt | +|---|---| +| `content/posts///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) diff --git a/README.md b/README.md index e88fb82..b3b6c6b 100644 --- a/README.md +++ b/README.md @@ -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//index.md` mit +1. **Inhalte** liegen als Markdown in `content/posts///index.md` + (z. B. `content/posts/de//` oder `content/posts/en//`) 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/// 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 diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 15378fe..085feac 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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-/` +1. Neuen Ordner anlegen: `content/posts/de/YYYY-MM-DD-/` (oder + `content/posts/en//` 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//index.md` +4. Lokal validieren: `cd publish && deno task validate-post ../content/posts///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:" --- 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//`, +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/--en/index.md`, - mit `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:']`-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//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:" + ``` + +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', '', '', '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` diff --git a/docs/STATUS.md b/docs/STATUS.md index 2164da8..9cd8dcd 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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', , 'ISO-639-1']`. Deutsche Events haben `lang=de`, englische + `lang=en`. Ergänzt durch `['a', '::', '', '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/// # 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///`, 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 diff --git a/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md b/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md index bb519c1..e104395 100644 --- a/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md +++ b/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md @@ -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.