docs: plan 1/3 für multilinguale posts + spec-korrektur (26 statt 8 posts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f977516552
commit
66ff33e34a
|
|
@ -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/<lang>/<slug>/` 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<string>()
|
||||
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<string[]> {
|
||||
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 "<kind>:<pubkey-hex>:<d-tag>")`)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **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/<slug>/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/<slug>` → `content/posts/de/<slug>`.
|
||||
|
||||
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:<pubkey-hex>:<slug-der-anderssprachigen-variante>"
|
||||
```
|
||||
|
||||
Ü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-<timestamp>.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=<pubkey>`, `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/<lang>/<slug>/` — 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.
|
||||
|
|
@ -10,7 +10,7 @@ Posts können in beliebigen Sprachen existieren. Posts, die inhaltlich dasselbe
|
|||
|
||||
## Scope
|
||||
|
||||
- **In Scope:** Verzeichnisstruktur `content/posts/<lang>/<slug>/` 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/<lang>/<slug>/` 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/<lang>/` 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/<slug>/` nach `content/posts/de/<slug>/` verschieben.
|
||||
2. Frontmatter um `lang: de` ergänzen; auskommentierten `a`-Platzhalter anlegen.
|
||||
|
|
|
|||
Loading…
Reference in New Issue