uploadBlob(args) berechnet sha256, baut kind:24242-auth-event via
signer, schickt es base64-kodiert im authorization-header an PUT
/upload aller servers parallel. erfolg: report mit ok/failed-listen
und primaryUrl (erster erfolgreicher server). wirft wenn alle ablehnen.
BlossomClient via dependency-injection für tests.
TS-casts für Uint8Array→BufferSource/BodyInit (deno-strict). 3 tests
grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
collectImages(postDir) scannt ordner nach png/jpg/jpeg/gif/webp/svg,
ignoriert hugo-resize-derivate (*_hu_<hex>.*), liest bytes und gibt
sortierte ImageFile[]-liste zurück. mimeFromExt() mapped extension
auf mime-type. deno.jsonc test-task um --allow-write erweitert (Deno.
makeTempDir+writeFile in tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createBunkerSigner(bunkerUrl) nutzt NostrConnectSigner.fromBunkerURI
aus applesauce-signers 2.x (früher als Nip46Signer im plan bezeichnet;
klassenname hat sich geändert). subscription- und publishMethod werden
global am class-constructor an einen shared RelayPool gekoppelt.
getPublicKey und signEvent bekommen je 30s-timeout mit sauberem
clearTimeout via withTimeout-helper. signer.ts ist vollständig, aber
ohne eigene unit-tests — integration folgt über check-subcommand
(task 16).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
publishToRelays(urls, ev, opts) publisht signiertes event parallel zu
allen relays, mit retries + exponential backoff + timeout pro versuch.
retour: { ok: string[], failed: string[] }. default-pool via
applesauce-relay 2.x (new RelayPool()); publishFn via dependency-
injection für tests. checkExisting(slug, pubkey, urls) fragt je relay
nach kind:30023 mit #d-filter ab — true wenn irgendeiner matcht.
timer-leaks vermieden per clearTimeout in publishOne + im mock-test.
3 tests grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
buildKind30023(args) baut unsigniertes kind:30023-event aus frontmatter
+ rewritten-body + cover-url. erzeugt pflicht-tags (d, title,
published_at) und bedingt optionale (summary aus description, image
aus coverUrl, t-tags aus tags[], client aus clientTag). plus
additionalTags-parameter für spätere task 15: license-tag und
imeta-tags (mit blossom-sha256) werden dort nach dem upload angehängt.
4 tests grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rewriteImageUrls(md, mapping) ersetzt alle - und
[](link)-konstrukte per dateinamen-lookup durch
blossom-urls aus dem mapping. absolute urls bleiben unverändert,
unbekannte dateinamen bleiben stehen (kein strip). =WxH-größenhinweise
werden entfernt. URL-dekodierung für leerzeichen-dateinamen (z.b.
'03-config generieren.png' im moodle-post). resolveCoverUrl()-helper
für den cover-lookup. 7 tests grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
parseFrontmatter(md) trennt yaml-frontmatter vom markdown-body via
regex, parst yaml mit @std/yaml. Frontmatter-interface enthält
nostr-publish-konvention: optionale images-liste mit file/role/alt/
license/authors plus Author-interface (name/url/orcid). 4 tests grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
deno 2.x projekt mit jsr/npm-imports für @std, nostr-tools, applesauce-signers,
applesauce-relay und rxjs. env-handling: primär ../.env.local (projekt-lokal),
alternativ publish/.env für fremd-repos (template in .env.example). tasks für
publish, check, validate-post, test, fmt, lint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- spec publish-pipeline aktualisiert: blossom-only (rsync-legacy-pfad raus)
- plan publish-pipeline (24 tasks in 12 phasen), blaupausen-tauglich:
alle projekt-konstanten via env (BUNKER_URL, AUTHOR_PUBKEY_HEX,
BOOTSTRAP_RELAY, CONTENT_ROOT, CLIENT_TAG, MIN_RELAY_ACKS)
- spec bild-metadaten-konvention (phase 1, yaml-basiert)
- wiki-entwurf deutsch + englisch: inline-markdown-konvention zur
bildattribution mit mapping zu nip-92 imeta-tags. konvention für
menschen, parser passt sich an; bei ambiguität eskalation zur
redaktion statt stillem raten
- redaktions-checkliste für bild-durchgang (abgearbeitet, bleibt
zum abgleich bei späteren migrationen)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pro post ein images:-block im frontmatter mit file, role, alt, license,
authors — als vorbereitung für publish-pipeline (imeta-tags nip-92).
91 bilder insgesamt. fünf UNKNOWN-einträge im vr-post zur späteren
recherche markiert (wikipedia-screenshot, sketchfab-fotograf,
ready-player-me, eyemeasure-app).
bei erlebnispadagogik-post: tote amazon-hotlinks entfernt, literatur-
referenzen in saubere textliste umgebaut.
redaktionell geprüft; CC0 für eigene fotos und screenshots,
CC BY-SA 3.0 DE für saemann-midjourney-collage, CC BY 4.0 für
dezentrale-oep-oer-gemeinschaftsbeitrag, CC BY-NC-SA 3.0 für
raupen-flickr-bild, CC BY-NC 4.0 für sketchfab-kirche.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docs/STATUS.md: was ist getan, was läuft live, wo liegen die Fäden
- docs/HANDOFF.md: nächste Optionen (Publish-Pipeline, Menü, Cutover),
Dev-Kommandos, bekannte Stolperfallen
- .claude/skills/joerglohrerde-workflow.md: repo-spezifischer Skill
zum Session-Start — Konventionen, API-Eigenheiten, wiederkehrende
Kommandos, wie mit Jörg arbeiten
- README.md als Navigation zu allen Dokumenten
Damit kann die nächste Session ohne Kontextverlust fortgeführt werden.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- externalClientLinks-Reihenfolge: EduFeed, Habla, Yakihonne (njump
raus). EduFeed als OER/OEP-Community-Home an erster Stelle.
- njump bleibt für Kommentar-Autor-Profile (Klick auf Avatar/Name
unter einem Kommentar) — dort ist es der bessere Profil-Viewer.
- EduFeed-URL-Schema: https://edufeed.org/a/<naddr> (falls sich das
als falsch erweist, in zweitem Commit korrigieren).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zwei Erweiterungen, die Community-Interaktion an Nostr-native Clients
auslagern statt in der SPA nachzubauen:
1. ReplyItem-Header ist jetzt ein <a href=https://njump.me/<npub>
target=_blank>. Klick auf Avatar/Name öffnet das vollständige
Profil des Kommentar-Authors mit allen Events.
2. Neue ExternalClientLinks.svelte zwischen Reactions und Composer:
dezente Box mit "In Nostr-Client öffnen" — drei Links (Habla,
Yakihonne, njump) über naddr, damit Leser Thread-Replies,
Reactions, Teilen dort nutzen können, wo die volle Nostr-Social-
Layer läuft.
Nostr-Helper erweitert:
- buildNpub(hex) — npub1…-Bech32-Encoding
- buildNjumpProfileUrl(hex) — njump.me/<npub>
- externalClientLinks({pubkey, kind, identifier}) — Liste der drei
etablierten Langform-Viewer mit naddr1…-URLs.
npm run check: 0 errors, 611 files. Deploy live.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Neue Komponenten unter $lib/components/:
- Reactions.svelte: lädt kind:7-Aggregation via loadReactions, rendert
Chips mit Emoji + Count. +/- werden zu 👍/👎 gemappt.
- ReplyItem.svelte: einzelner Kommentar mit Author-Npub-Prefix + Datum.
- ReplyList.svelte: lädt kind:1-Replies, merged mit optimistic-Props
(dedup per id), sortiert chronologisch.
- ReplyComposer.svelte: Textarea + Senden-Button. Nutzt NIP-07-Wrapper
(getPublicKey, signEvent), baut kind:1-Event mit a/e/p-Tags, pusht
via pool.publish() zu allen Read-Relays. Fehlertolerant: zeigt
Hinweis, wenn NIP-07-Extension fehlt.
Integration in PostView: Reactions, Composer, ReplyList unterhalb des
Artikel-Bodys. Optimistisches Reply-Pattern: Composer.onPublished
pushed signed event in PostView-local $state, ReplyList merged mit
fetched events.
Playwright-E2E:
- playwright.config.ts mit Dev-Server-Auto-Start
- home.test.ts: Profil + Beitragsliste sichtbar
- post.test.ts: Titel + Body + Legacy-URL-Redirect
Alle 3 E2E-Tests grün. npm run check: 600 files, 0 errors.
Deploy live auf svelte.joerg-lohrer.de (Phase 5 inklusive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- loadPostsByTag(tagName): client-seitige Filterung der Post-Liste
(case-insensitive). #t-Filter wird nicht von allen Relays zuverlässig
unterstützt — wir laden alles und filtern lokal.
- /tag/[name]/+page.ts+svelte: neue Tag-Route, Breadcrumb zurück zur
Übersicht, #tagName als H1, dieselbe PostCard-Darstellung wie Home.
- Tag-Chips in PostView sind bereits klickbar (aus Task 21).
npm run check: 0 errors. Deploy live auf svelte.joerg-lohrer.de.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
window.nostr-Proxy für Alby/nos2x/Flamingo-Extensions. Fehlertolerant:
bei fehlender Extension ODER User-Ablehnung returnen die Helper null,
damit UI klar "bitte Extension installieren"-Hinweise zeigen kann
statt zu crashen.
UnsignedEvent/SignedEvent als explizite Types — werden ab Task 29
(ReplyComposer) für NIP-07-signierte kind:1-Kommentare genutzt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
loadReactions(dtag) sammelt kind:7-Events mit #a-Filter auf den
Post, gruppiert nach content (emoji oder +/-), zählt und sortiert
nach Häufigkeit. Leerer content wird als + interpretiert (NIP-25-
Konvention für Like-Default).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fügt `loadReplies(dtag)` an loaders.ts an. Filter `#a` auf das
addressable-Event-Format "30023:<pubkey>:<dtag>" findet alle kind:1
Replies auf den Post. Sortiert aufsteigend (älteste zuerst).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Eigene `new Marked({...})`-Instanz statt globaler `marked.use()`-Mutation
— schützt andere Module vor Konfigurationsleckage, schärft Spec §3
("lokale Ersetzbarkeit").
- SSR-Guard: `renderMarkdown` wirft in Non-DOM-Umgebungen eine
Fehlermeldung statt stumm unsicher durchzulaufen. SPA hat `ssr=false`,
Vitest läuft in jsdom — Guard ist Early-Fail für versehentliche
Node-Aufrufe.
- `ADD_ATTR: ['target', 'rel']` entfernt — war ein No-Op, weil Marked
diese Attribute nicht einfügt. Link-Attribut-Hardening kommt später,
wenn externe Links tatsächlich `target="_blank"` bekommen sollen.
- Code-Block-Test prüft zusätzlich `class="hljs"` (Regression-Anker
für Custom-Renderer).
- Erweiterte XSS-Matrix: onerror, onclick, iframe, data:text/html,
vbscript:, svg+script — relevant für spätere Reply-Darstellung.
14/14 Tests grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- JSDoc zu NaddrArgs, buildNaddr, buildHablaLink (Stil konsistent mit config.ts).
- Neue Tests: ohne relays (Default-`?? []`-Pfad), unterschiedliche Inputs
erzeugen unterschiedliche Links (Guard gegen konstanten Rückgabewert).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- decodeURIComponent in try/catch (malformed URI encoding crasht
den SPA-Boot-Path nicht mehr, returned stattdessen null).
- JSDoc präzisiert: erwartet nur Pfad ohne Query/Fragment.
- Neue Tests: malformed %E0 → null, leerer dtag → null,
round-trip Legacy → canonical.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- FALLBACK_READ_RELAYS als `as const` tuple (kein mutables Array).
- BOOTSTRAP_RELAY als erster Eintrag referenziert statt dupliziert.
- Präzisere JSDoc zu HABLA_BASE (klarmacht, dass /a/ baked-in ist).
- Timeout-Kommentare trennen soft (per-Relay) vs. hard (Page-Budget).
Code-Quality-Nitpicks aus Task 3 Review adressiert. npm run check grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deckt die vollständige SPA-Spec ab: Setup + adapter-static, Daten-
ebene (Relay-Pool, Loader, Profil, Markdown, Signer), Routing mit
Legacy-URL-Normalisierung, Tag-Filter, Reactions, NIP-07-Kommentare.
TDD für pure Transformationen (URL-Parser, naddr, Markdown), E2E mit
Playwright für Happy-Paths.
Deploy-Ziel: svelte.joerg-lohrer.de. spa.joerg-lohrer.de bleibt als
Vanilla-Mini-Spike-MVP erhalten (Referenz-Verhalten).
Publish-Pipeline hat eigene Spec und bekommt separaten Plan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revidiert das URL-Schema: die lange Hugo-Form /YYYY/MM/DD/<dtag>.html/
wird als Legacy behandelt, die kurze Form /<dtag>/ als kanonisch.
Gründe: schöner zu teilen, skaliert über die 18 Altposts hinaus,
Datumskomponente fügt keinen Informationswert (Datum steht im Event
als published_at).
Legacy-URLs werden clientseitig via history.replaceState ohne Reload
auf die kurze Form normalisiert — Backlinks bleiben funktional,
Bookmarks konvergieren zur kurzen Form. Keine .htaccess-Redirects.
Im Mini-Spike bereits umgesetzt und live auf spa.joerg-lohrer.de.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/YYYY/MM/DD/<dtag>.html/ wird erkannt, via history.replaceState auf
die kanonische Form /<dtag>/ umgeschrieben, dann der Post geladen.
Externe Backlinks auf alte Hugo-URLs landen damit ohne Reload-Flash
auf der neuen kurzen Adresse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lädt kind:0-Metadata-Event des Autors parallel zur Beitragsliste und
zeigt Avatar, Anzeigename, About-Text, NIP-05 und Website oben auf
der Übersichtsseite. Einzelpost-Seiten bleiben fokussiert, ohne
Profil-Header.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>