diff --git a/docs/superpowers/plans/2026-04-21-multilingual-posts-pipeline.md b/docs/superpowers/plans/2026-04-21-multilingual-posts-pipeline.md new file mode 100644 index 0000000..26c12e1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-multilingual-posts-pipeline.md @@ -0,0 +1,713 @@ +# Multilinguale Posts — Repo-Struktur & Publish-Pipeline (Plan 1/3) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Posts können im Repo unter `content/posts///` liegen; das Deno-Publish-Skript traversiert die neue Struktur korrekt, erzeugt `kind:30023`-Events mit `l`-Tag (bereits vorhanden) und optionalen `a`-Tag-Verweisen auf andere Sprach-Varianten (neu). Die 26 bestehenden Posts werden unter `de/` einsortiert. + +**Architecture:** Der einzige Code-Change liegt in `publish/` (Deno-Pipeline). Die Traversierungs-Funktionen (`allPostDirs`, `filterPostDirs`) lernen eine zusätzliche Verzeichnisebene. `buildKind30023` erhält optionale `a`-Tags aus dem Frontmatter. Die 8 Bestandsposts werden per `git mv` unter `content/posts/de/` verschoben und per `publish --force-all` re-publisht — selber `d`-tag, selber Content, nur Event-Erzeugung läuft durch die neue Pipeline. + +**Tech Stack:** Deno, TypeScript, `@std/yaml`, `@std/path`, `@std/assert` für Tests. Bestehende Test-Infrastruktur unter `publish/tests/`. + +--- + +## Spec-Referenz + +Umsetzt die Abschnitte **Content-Struktur**, **Frontmatter**, **nostr-Event-Mapping**, **Publish-Pipeline** und **Migration bestehender Posts** aus `docs/superpowers/specs/2026-04-21-multilingual-posts-design.md`. Out-of-scope in diesem Plan: SPA-Änderungen (Plan 2) und UI-Lokalisierung via `svelte-i18n` (Plan 3). + +## Datei-Struktur + +**Zu ändern:** +- `publish/src/core/change-detection.ts` — `filterPostDirs` und `allPostDirs` lernen Sprach-Ebene. +- `publish/src/core/frontmatter.ts` — Interface `Frontmatter` um optionales `a`-Feld ergänzen. +- `publish/src/core/event.ts` — `buildKind30023` übernimmt `a`-Tags aus `fm.a` als `["a", coord, "", "translation"]`. +- `publish/src/core/validation.ts` — leichte `a`-tag-Format-Prüfung. +- `publish/tests/change-detection_test.ts` — neue Tests für Sprach-Ebene. +- `publish/tests/event_test.ts` — neue Tests für `a`-tag-Übernahme. +- `publish/tests/frontmatter_test.ts` — Test für `a`-Feld-Parsing. +- `publish/tests/validation_test.ts` — Test für `a`-Format-Fehler. + +**Repo-Content-Umbau (kein Code):** +- Alle 26 Unterordner von `content/posts/` → nach `content/posts/de/` verschieben. +- In jedem `index.md` Frontmatter: `lang: de` sicherstellen; auskommentierten `a`-Platzhalter ergänzen. + +**Nicht angefasst:** +- `publish/src/subcommands/publish.ts` — liest `postDir` ohnehin als opakes Verzeichnis; erhält die bereits um eine Ebene tiefere Pfade unverändert weitergereicht. +- `publish/src/cli.ts` — `resolvePostDirs` nutzt `allPostDirs`/`changedPostDirs`, die wir anpassen; keine eigene Änderung nötig, solange Interface gleich bleibt. +- GitHub-Action `.github/workflows/publish.yml` — ruft CLI mit Env-Variablen, unverändert. + +--- + +## Task 1: Traversierung — Tests für Sprach-Ebene in `filterPostDirs` + +**Files:** +- Test: `publish/tests/change-detection_test.ts` + +- [ ] **Step 1: Failing Tests schreiben** + +Ergänze in `publish/tests/change-detection_test.ts` nach dem letzten bestehenden Test (hinter `Deno.test('changedPostDirs: ...', ...)`): + +```typescript +Deno.test('filterPostDirs: extrahiert post-ordner mit sprach-ebene', () => { + const lines = [ + 'content/posts/de/a/index.md', + 'content/posts/en/b/image.png', + 'content/posts/de/c/index.md', + 'README.md', + ] + assertEquals( + filterPostDirs(lines, 'content/posts').sort(), + ['content/posts/de/a', 'content/posts/de/c', 'content/posts/en/b'], + ) +}) + +Deno.test('filterPostDirs: ignoriert dateien direkt unter lang-ordner', () => { + const lines = [ + 'content/posts/de/index.md', + 'content/posts/de/README.md', + 'content/posts/de/x/index.md', + ] + assertEquals(filterPostDirs(lines, 'content/posts'), ['content/posts/de/x']) +}) + +Deno.test('filterPostDirs: _drafts unter sprach-ebene wird ignoriert', () => { + const lines = [ + 'content/posts/de/_drafts/x/index.md', + 'content/posts/de/real/index.md', + ] + assertEquals(filterPostDirs(lines, 'content/posts'), ['content/posts/de/real']) +}) +``` + +- [ ] **Step 2: Tests laufen, Erwartung FAIL** + +Run: +```bash +cd publish && deno test tests/change-detection_test.ts +``` + +Expected: Die drei neuen Tests schlagen fehl (alte Regex kennt nur eine Ebene). Bestehende Tests bleiben grün. + +- [ ] **Step 3: Commit der Tests** + +```bash +git add publish/tests/change-detection_test.ts +git commit -m "test: filterPostDirs für sprach-ebene (failing)" +``` + +--- + +## Task 2: Traversierung — `filterPostDirs` auf Sprach-Ebene umstellen + +**Files:** +- Modify: `publish/src/core/change-detection.ts` + +- [ ] **Step 1: Regex um Sprach-Ebene erweitern** + +In `publish/src/core/change-detection.ts`, ersetze den Block in `filterPostDirs` ab `const indexRe = ...` bis `return [...dirs].sort()` durch: + +```typescript + const indexRe = new RegExp(`^${escapeRegex(prefix)}([a-z]{2})/([^/]+)/index\\.md$`) + const assetRe = new RegExp(`^${escapeRegex(prefix)}([a-z]{2})/([^/]+)/`) + const dirs = new Set() + for (const line of lines) { + const l = line.trim() + if (!l) continue + const indexMatch = l.match(indexRe) + if (indexMatch) { + const [, lang, slug] = indexMatch + if (slug.startsWith('_')) continue + dirs.add(`${prefix}${lang}/${slug}`) + continue + } + const assetMatch = l.match(assetRe) + if (assetMatch && !l.endsWith('.md')) { + const [, lang, slug] = assetMatch + if (slug.startsWith('_')) continue + dirs.add(`${prefix}${lang}/${slug}`) + } + } + return [...dirs].sort() +``` + +Und entferne die obsolete Zeile `const drafts = prefix + '_'` samt der dazugehörigen `if (l.startsWith(drafts)) continue` — wir filtern jetzt auf Slug-Ebene. + +- [ ] **Step 2: Tests laufen, Erwartung PASS** + +Run: +```bash +cd publish && deno test tests/change-detection_test.ts +``` + +Expected: Alle Tests grün. Falls ein bestehender Test (`filterPostDirs: extrahiert post-ordner aus dateipfaden (content/posts)`) rot wird: Das ist beabsichtigt — er testet die alte flache Struktur. Passe ihn an, indem du die Eingabe-Pfade um `/de/` ergänzt: + +```typescript +// alter Test — Eingabe aktualisieren: +Deno.test('filterPostDirs: extrahiert post-ordner aus dateipfaden (content/posts)', () => { + const lines = [ + 'content/posts/de/a/index.md', + 'content/posts/de/b/image.png', + 'content/posts/de/c/other.md', + 'README.md', + 'app/src/lib/x.ts', + ] + assertEquals( + filterPostDirs(lines, 'content/posts').sort(), + ['content/posts/de/a', 'content/posts/de/b'], + ) +}) + +// alter Test — Eingabe aktualisieren: +Deno.test('filterPostDirs: respektiert alternativen root (blog/)', () => { + const lines = [ + 'blog/de/x/index.md', + 'blog/en/y/pic.png', + 'content/posts/de/z/index.md', + 'README.md', + ] + assertEquals(filterPostDirs(lines, 'blog').sort(), ['blog/de/x', 'blog/en/y']) +}) + +// alter Test — Eingabe aktualisieren: +Deno.test('filterPostDirs: ignoriert _drafts und non-index.md', () => { + const lines = [ + 'content/posts/de/a/index.md', + 'content/posts/de/a/extra.md', + 'content/posts/de/_drafts/x/index.md', + ] + assertEquals(filterPostDirs(lines, 'content/posts'), ['content/posts/de/a']) +}) +``` + +Re-Run `deno test tests/change-detection_test.ts` → alle PASS. + +- [ ] **Step 3: Commit** + +```bash +git add publish/src/core/change-detection.ts publish/tests/change-detection_test.ts +git commit -m "feat(publish): filterPostDirs traversiert sprach-ebene" +``` + +--- + +## Task 3: Traversierung — `allPostDirs` auf Sprach-Ebene umstellen + +**Files:** +- Test: `publish/tests/change-detection_test.ts` +- Modify: `publish/src/core/change-detection.ts` + +- [ ] **Step 1: Failing Test schreiben** + +`allPostDirs` hat bisher keinen Test. Ergänze am Ende von `publish/tests/change-detection_test.ts`: + +```typescript +import { allPostDirs } from '../src/core/change-detection.ts' + +Deno.test('allPostDirs: findet posts in sprach-unterordnern', async () => { + const tmp = await Deno.makeTempDir() + try { + await Deno.mkdir(`${tmp}/de/alpha`, { recursive: true }) + await Deno.writeTextFile(`${tmp}/de/alpha/index.md`, '---\n---') + await Deno.mkdir(`${tmp}/de/beta`, { recursive: true }) + await Deno.writeTextFile(`${tmp}/de/beta/index.md`, '---\n---') + await Deno.mkdir(`${tmp}/en/alpha`, { recursive: true }) + await Deno.writeTextFile(`${tmp}/en/alpha/index.md`, '---\n---') + await Deno.mkdir(`${tmp}/de/_draft/index`, { recursive: true }) + await Deno.writeTextFile(`${tmp}/de/_draft/index.md`, '---\n---') + + const result = await allPostDirs(tmp) + assertEquals( + result.sort(), + [`${tmp}/de/alpha`, `${tmp}/de/beta`, `${tmp}/en/alpha`].sort(), + ) + } finally { + await Deno.remove(tmp, { recursive: true }) + } +}) +``` + +Falls der `allPostDirs`-Import schon oben in der Datei vorhanden ist (weil der Block zu `changedPostDirs` ihn mit-importiert): den doppelten Import weglassen und stattdessen die bestehende `import { ... } from '../src/core/change-detection.ts'`-Zeile erweitern. + +- [ ] **Step 2: Test laufen, Erwartung FAIL** + +Run: +```bash +cd publish && deno test tests/change-detection_test.ts +``` + +Expected: Neuer Test schlägt fehl, weil `allPostDirs` nur eine Ebene tief liest. + +- [ ] **Step 3: `allPostDirs` auf Sprach-Ebene anpassen** + +In `publish/src/core/change-detection.ts`, ersetze die Funktion `allPostDirs` komplett durch: + +```typescript +export async function allPostDirs(contentRoot: string): Promise { + const result: string[] = [] + for await (const langEntry of Deno.readDir(contentRoot)) { + if (!langEntry.isDirectory) continue + if (!/^[a-z]{2}$/.test(langEntry.name)) continue + const langDir = `${contentRoot}/${langEntry.name}` + for await (const postEntry of Deno.readDir(langDir)) { + if (!postEntry.isDirectory) continue + if (postEntry.name.startsWith('_')) continue + const indexPath = `${langDir}/${postEntry.name}/index.md` + try { + const stat = await Deno.stat(indexPath) + if (stat.isFile) result.push(`${langDir}/${postEntry.name}`) + } catch { + // skip folders without index.md + } + } + } + return result.sort() +} +``` + +- [ ] **Step 4: Tests laufen, Erwartung PASS** + +Run: +```bash +cd publish && deno test tests/change-detection_test.ts +``` + +Expected: Alle grün. + +- [ ] **Step 5: Commit** + +```bash +git add publish/src/core/change-detection.ts publish/tests/change-detection_test.ts +git commit -m "feat(publish): allPostDirs traversiert sprach-ebene" +``` + +--- + +## Task 4: Frontmatter — `a`-Feld parsen + +**Files:** +- Test: `publish/tests/frontmatter_test.ts` +- Modify: `publish/src/core/frontmatter.ts` + +- [ ] **Step 1: Failing Test schreiben** + +Ergänze in `publish/tests/frontmatter_test.ts` (am Ende): + +```typescript +Deno.test('parseFrontmatter: liest a-tag-liste aus frontmatter', () => { + const md = [ + '---', + 'title: T', + 'slug: s', + 'date: 2024-01-01', + 'a:', + ' - "30023:abc:other-slug"', + '---', + 'body', + ].join('\n') + const { fm } = parseFrontmatter(md) + assertEquals(fm.a, ['30023:abc:other-slug']) +}) + +Deno.test('parseFrontmatter: a fehlt → undefined', () => { + const md = '---\ntitle: T\nslug: s\ndate: 2024-01-01\n---\nbody' + const { fm } = parseFrontmatter(md) + assertEquals(fm.a, undefined) +}) +``` + +- [ ] **Step 2: Test laufen, Erwartung FAIL** + +Run: +```bash +cd publish && deno test tests/frontmatter_test.ts +``` + +Expected: Neue Tests schlagen fehl (TypeError auf `fm.a`, weil Feld im Interface nicht deklariert ist — oder: beide PASS, weil YAML ein Array ohnehin durchreicht. Falls beide PASS: weiter zu Step 3 für die Interface-Deklaration; der Test dokumentiert dann nur das gewollte Verhalten). + +- [ ] **Step 3: Interface `Frontmatter` erweitern** + +In `publish/src/core/frontmatter.ts`, ergänze im Interface `Frontmatter` vor `[key: string]: unknown` die Zeile: + +```typescript + a?: string[] +``` + +- [ ] **Step 4: Tests laufen, Erwartung PASS** + +Run: +```bash +cd publish && deno test tests/frontmatter_test.ts +``` + +Expected: Alle grün. + +- [ ] **Step 5: Commit** + +```bash +git add publish/src/core/frontmatter.ts publish/tests/frontmatter_test.ts +git commit -m "feat(publish): Frontmatter unterstützt a-tag-liste" +``` + +--- + +## Task 5: Validierung — `a`-Tag-Format prüfen + +**Files:** +- Test: `publish/tests/validation_test.ts` +- Modify: `publish/src/core/validation.ts` + +- [ ] **Step 1: Failing Tests schreiben** + +Ergänze in `publish/tests/validation_test.ts` (am Ende): + +```typescript +Deno.test('validatePost: akzeptiert a-tag im korrekten format', () => { + const fm = { + title: 'T', + slug: 'abc', + date: new Date('2024-01-01'), + a: ['30023:abcdef0123456789:other-slug'], + } as Frontmatter + validatePost(fm) // wirft nicht +}) + +Deno.test('validatePost: lehnt a-tag mit falschem format ab', () => { + const fm = { + title: 'T', + slug: 'abc', + date: new Date('2024-01-01'), + a: ['nur-ein-string'], + } as Frontmatter + assertThrows(() => validatePost(fm), Error, 'invalid a-tag') +}) + +Deno.test('validatePost: lehnt a-tag mit fehlendem d-tag ab', () => { + const fm = { + title: 'T', + slug: 'abc', + date: new Date('2024-01-01'), + a: ['30023:abcdef:'], + } as Frontmatter + assertThrows(() => validatePost(fm), Error, 'invalid a-tag') +}) +``` + +Stelle sicher, dass die Imports oben in der Datei `assertThrows` enthalten (ggf. ergänzen: `import { assertEquals, assertThrows } from '@std/assert'`). Falls `Frontmatter` noch nicht importiert ist: `import type { Frontmatter } from '../src/core/frontmatter.ts'` ergänzen. + +- [ ] **Step 2: Tests laufen, Erwartung FAIL** + +Run: +```bash +cd publish && deno test tests/validation_test.ts +``` + +Expected: Die beiden Negativ-Tests schlagen fehl (keine Validierung vorhanden). + +- [ ] **Step 3: Validierung implementieren** + +In `publish/src/core/validation.ts`, ergänze vor dem Ende der Funktion `validatePost` (nach der Date-Prüfung): + +```typescript + if (fm.a !== undefined) { + if (!Array.isArray(fm.a)) { + throw new Error('a must be a list of strings') + } + const coordRe = /^\d+:[0-9a-f]+:[a-z0-9][a-z0-9-]*$/ + for (const coord of fm.a) { + if (typeof coord !== 'string' || !coordRe.test(coord)) { + throw new Error(`invalid a-tag: "${coord}" (expected "::")`) + } + } + } +``` + +- [ ] **Step 4: Tests laufen, Erwartung PASS** + +Run: +```bash +cd publish && deno test tests/validation_test.ts +``` + +Expected: Alle grün. + +- [ ] **Step 5: Commit** + +```bash +git add publish/src/core/validation.ts publish/tests/validation_test.ts +git commit -m "feat(publish): validatePost prüft a-tag-format" +``` + +--- + +## Task 6: Event-Mapping — `a`-Tags aus Frontmatter übernehmen + +**Files:** +- Test: `publish/tests/event_test.ts` +- Modify: `publish/src/core/event.ts` + +- [ ] **Step 1: Failing Test schreiben** + +Ergänze in `publish/tests/event_test.ts` (am Ende — falls der Import-Block oben `buildKind30023` und `Frontmatter` noch nicht hat, hinzufügen): + +```typescript +Deno.test('buildKind30023: schreibt a-tags aus frontmatter mit marker "translation"', () => { + const fm = { + title: 'T', + slug: 'abc', + date: new Date('2024-01-01T00:00:00Z'), + lang: 'de', + a: [ + '30023:0123456789abcdef:other-slug', + '30023:0123456789abcdef:third-slug', + ], + } as Frontmatter + const ev = buildKind30023({ + fm, + rewrittenBody: 'body', + coverUrl: undefined, + pubkeyHex: '0123456789abcdef', + clientTag: '', + nowSeconds: 1700000000, + }) + const aTags = ev.tags.filter((t) => t[0] === 'a') + assertEquals(aTags, [ + ['a', '30023:0123456789abcdef:other-slug', '', 'translation'], + ['a', '30023:0123456789abcdef:third-slug', '', 'translation'], + ]) +}) + +Deno.test('buildKind30023: ohne a im frontmatter keine a-tags im event', () => { + const fm = { + title: 'T', + slug: 'abc', + date: new Date('2024-01-01T00:00:00Z'), + lang: 'de', + } as Frontmatter + const ev = buildKind30023({ + fm, + rewrittenBody: 'body', + coverUrl: undefined, + pubkeyHex: '0123456789abcdef', + clientTag: '', + nowSeconds: 1700000000, + }) + assertEquals(ev.tags.filter((t) => t[0] === 'a'), []) +}) +``` + +- [ ] **Step 2: Test laufen, Erwartung FAIL** + +Run: +```bash +cd publish && deno test tests/event_test.ts +``` + +Expected: Erster neuer Test schlägt fehl (keine `a`-Tag-Erzeugung), zweiter läuft möglicherweise durch. + +- [ ] **Step 3: `buildKind30023` erweitern** + +In `publish/src/core/event.ts`, ergänze nach dem bestehenden `if (clientTag) tags.push(['client', clientTag])`-Block und **vor** `if (additionalTags) tags.push(...additionalTags)`: + +```typescript + if (Array.isArray(fm.a)) { + for (const coord of fm.a) { + tags.push(['a', coord, '', 'translation']) + } + } +``` + +- [ ] **Step 4: Tests laufen, Erwartung PASS** + +Run: +```bash +cd publish && deno test tests/event_test.ts +``` + +Expected: Alle grün. + +- [ ] **Step 5: Commit** + +```bash +git add publish/src/core/event.ts publish/tests/event_test.ts +git commit -m "feat(publish): buildKind30023 übernimmt a-tags aus frontmatter" +``` + +--- + +## Task 7: Gesamt-Testlauf & Typ-Check + +**Files:** — (nur Testlauf) + +- [ ] **Step 1: Alle Tests im Publish-Subdir** + +Run: +```bash +cd publish && deno test +``` + +Expected: Alle Tests grün. Falls rot: Fehler beheben, bevor die Repo-Migration startet. + +- [ ] **Step 2: Deno-Typecheck gesamtes Publish-Modul** + +Run: +```bash +cd publish && deno check src/cli.ts +``` + +Expected: Keine Typ-Fehler. + +- [ ] **Step 3: Kein Commit nötig** (reiner Verifikations-Schritt). + +--- + +## Task 8: Repo-Migration — bestehende Posts nach `content/posts/de/` + +**Files:** +- Move: alle Unterordner von `content/posts/` → `content/posts/de/` +- Modify: jeder `content/posts/de//index.md` (Frontmatter ergänzen) + +- [ ] **Step 1: `de/`-Zielordner anlegen** + +```bash +mkdir -p content/posts/de +``` + +- [ ] **Step 2: Alle Post-Ordner verschieben (mit `git mv`)** + +```bash +for dir in content/posts/*/; do + name=$(basename "$dir") + [ "$name" = "de" ] && continue + git mv "$dir" "content/posts/de/$name" +done +``` + +Expected: `git status` zeigt 26 Renames unter `content/posts/` → `content/posts/de/`. + +Verifizieren: +```bash +ls content/posts/de/ | wc -l +``` +Expected: `26` + +- [ ] **Step 3: `lang: de` in jedem Frontmatter sicherstellen** + +Prüfe zunächst, wie viele Posts `lang:` bereits haben: + +```bash +for f in content/posts/de/*/index.md; do + head -20 "$f" | grep -q "^lang:" || echo "FEHLT: $f" +done +``` + +Expected: Leere Ausgabe (alle haben `lang:`) oder eine Liste der Dateien, in denen `lang: de` manuell zu ergänzen ist. + +Für jede gelistete Datei: Frontmatter öffnen und `lang: de` in einer neuen Zeile vor dem schließenden `---` ergänzen. (Wenn `lang:` fehlt, manuell mit Editor ergänzen; kein Skript nötig bei wenigen Dateien.) + +- [ ] **Step 4: Auskommentierten `a`-Platzhalter ergänzen** + +Als Konvention fügen wir in jedem Frontmatter direkt vor dem schließenden `---` den Platzhalter ein. Manuell pro Datei (oder via Editor-Makro) — Beispiel am Ende des bestehenden Frontmatter-Blocks: + +```yaml +# a: +# - "30023::" +``` + +Überprüfen: +```bash +grep -L "^# a:" content/posts/de/*/index.md +``` + +Expected: Leere Ausgabe (alle Dateien haben den Platzhalter). + +- [ ] **Step 5: Dry-Run-Publish auf einem einzelnen Post** + +```bash +cd publish && deno run -A src/cli.ts publish --dry-run --post bibel-selfies +``` + +Expected: Ausgabe ähnlich `dry-run: ../content/posts/de/2025-04-17-bibel-selfies`, Exit 0. Falls der Pfad falsch aufgelöst wird: zurück zu Task 2/3, Traversierungs-Logik prüfen. + +- [ ] **Step 6: Commit** + +```bash +git add content/posts/ +git commit -m "chore: posts nach content/posts/de/ migriert, a-tag-platzhalter ergänzt" +``` + +--- + +## Task 9: Re-Publish der Bestandsposts + +**Files:** — (kein Code, nur CLI-Aufruf) + +- [ ] **Step 1: Publish-Konfiguration prüfen** + +Run (im Repo-Root): +```bash +cd publish && deno run -A src/cli.ts check +``` + +Expected: Alle Checks OK (Bunker, Relays, Blossom). Falls FAIL: erst beheben. + +- [ ] **Step 2: Re-Publish aller Posts mit `--force-all`** + +```bash +cd publish && deno run -A src/cli.ts publish --force-all +``` + +Expected: 26 Posts durchlaufen, alle mit Status `success` und `action: update` (selber `d`-tag wie zuvor → NIP-33-Replacement greift). Log landet unter `publish/logs/publish-.json`. + +- [ ] **Step 3: Log-Inspektion** + +```bash +ls -t publish/logs/ | head -1 | xargs -I{} cat publish/logs/{} | head -100 +``` + +Expected: JSON-Log zeigt pro Post `l`-Tag im Event (sichtbar als `["l", "de", "ISO-639-1"]` im Event-Dump, falls im Log enthalten) und keine Fehler. Falls ein Post auf `action: new` statt `update` landet, ist das ein Hinweis auf geänderten `d`-tag — prüfen. + +- [ ] **Step 4: Kein neuer Commit nötig** — Re-Publish ändert nur nostr-Events, kein Repo-Zustand. + +--- + +## Task 10: Ende-zu-Ende-Verifikation + +**Files:** — (Verifikation) + +- [ ] **Step 1: Prüfe auf einem Relay, dass einer der Events den `l`-Tag trägt** + +Wähle einen Post (z. B. `bibel-selfies`) und frage über `nak` oder einen Nostr-Client das neueste Event mit `kind:30023`, `author=`, `d=bibel-selfies` ab. Bestätige die Tags enthalten: + +``` +["L", "ISO-639-1"] +["l", "de", "ISO-639-1"] +``` + +Falls nicht verfügbar: Dump aus dem Publish-Log (`publish/logs/publish-*.json`, Feld `eventId`) nutzen und das Event via Nostr-Tool oder existierender SPA prüfen. + +- [ ] **Step 2: GitHub-Action smoke-test** + +Commit einen harmlosen Änderung in einem einzelnen Post (z. B. ein Leerzeichen im Body) und push: + +```bash +echo "" >> content/posts/de/2025-04-17-bibel-selfies/index.md +git commit -am "test: trigger github-action nach struktur-migration" +git push +``` + +Expected: GitHub-Action (`publish.yml`) läuft durch, findet den einen geänderten Post über `changedPostDirs`, re-publisht erfolgreich. Logs unter Actions-Tab prüfen. + +Falls die Action fehlschlägt, meist: `filterPostDirs` liest `git diff`-Zeilen anders als im Test angenommen — zurück zu Task 2. + +- [ ] **Step 3: Kein Commit nötig** (der Test-Commit aus Step 2 bleibt). + +--- + +## Fertig + +Nach Task 10: +- Repo-Struktur ist `content/posts///` — lauffähig und testbar. +- Publish-Pipeline traversiert die neue Struktur korrekt, erzeugt `l`- und `a`-Tags aus dem Frontmatter. +- 20 Bestandsposts sind unter `de/` einsortiert und frisch re-publisht; Event-Zustand auf Relays ist konsistent mit Repo-Zustand. +- GitHub-Action läuft weiterhin automatisch. + +**Nächster Plan (separat zu schreiben):** SPA liest `a`-Tags, zeigt „Also available in …"-Hinweis, Sprachwahl-Umschalter. 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 3fd9575..bb519c1 100644 --- a/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md +++ b/docs/superpowers/specs/2026-04-21-multilingual-posts-design.md @@ -10,7 +10,7 @@ Posts können in beliebigen Sprachen existieren. Posts, die inhaltlich dasselbe ## Scope -- **In Scope:** Verzeichnisstruktur `content/posts///` inkl. Anpassung der Build-Logik und der SvelteKit-Repräsentation (Routing, Datenquellen); Frontmatter-Erweiterung um `lang` und optional `a`-tag-Verweise; Anpassung des Deno-Publish-Skripts; UI-Lokalisierung (Chrome-Strings) mit `svelte-i18n`; Sprachwahl und Fallback-Verhalten in der SPA; Migration der 8 bereits publizierten Posts (nur `l=de` ergänzen, kein `d`-tag-Change). +- **In Scope:** Verzeichnisstruktur `content/posts///` inkl. Anpassung der Build-Logik und der SvelteKit-Repräsentation (Routing, Datenquellen); Frontmatter-Erweiterung um `lang` und optional `a`-tag-Verweise; Anpassung des Deno-Publish-Skripts; UI-Lokalisierung (Chrome-Strings) mit `svelte-i18n`; Sprachwahl und Fallback-Verhalten in der SPA; Migration der 26 bereits publizierten Posts (nur `l=de` ergänzen, kein `d`-tag-Change). - **Out of Scope:** Übersetzungs-Automatik (LLM-basiert); Community-Contribution-Workflow (PR-Template, Review-Guidelines) — später, wenn Grundlage steht; **sprachspezifische Pfad-Präfixe in URLs** (z. B. `/en/posts/...`) — die Content-Struktur unter `content/posts//` ist explizit In Scope, nur das URL-Schema bleibt slug-basiert ohne Locale-Präfix. ## Architektur @@ -95,7 +95,7 @@ Das bestehende Skript wird angepasst, nicht neu geschrieben. Änderungen: ### Migration bestehender Posts -Die 8 bereits publizierten Posts tragen den `l=de`-Tag vermutlich bereits auf Event-Ebene (Beispiel `["L","ISO-639-1"]` + `["l","de","ISO-639-1"]` im Export). Die Migration ist daher primär eine Repo-Reorganisation: +Die 26 bereits publizierten Posts tragen den `l=de`-Tag vermutlich bereits auf Event-Ebene (Beispiel `["L","ISO-639-1"]` + `["l","de","ISO-639-1"]` im Export). Die Migration ist daher primär eine Repo-Reorganisation: 1. Alle Posts aus `content/posts//` nach `content/posts/de//` verschieben. 2. Frontmatter um `lang: de` ergänzen; auskommentierten `a`-Platzhalter anlegen.