CI-build hat mit "Some specified paths were not resolved, unable to
cache dependencies" abgebrochen, weil actions/setup-node@v4 mit
cache: 'npm' eine committed package-lock.json erwartet.
Lockfile war bisher in app/.gitignore ausgeschlossen — ungewoehnliche
konvention fuer SvelteKit-projekte; reproduzierbare builds (sowohl
CI als auch fresh-clone) brauchen den lockfile. Jetzt committed,
mit dem stand der nach den isomorphic-dompurify+@types/node-installs
existiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime-fallback (loadPost + LoadingOrError + PostView) entfernt.
Detail-seite rendert jetzt ausschliesslich aus dem snapshot. Imports
und state, die nur fuer den fallback gebraucht wurden, sind weg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slugs ausserhalb des snapshots werfen jetzt 404 (server-side beim
build, browser-side via static-fallback). Vorher kam in dem fall
{ dtag, snapshot: null } zurueck — die svelte-seite versuchte dann
clientseitig via loadPost() zu laden.
Frische nostr-first-posts erscheinen ab jetzt erst nach dem
naechsten snapshot+build-lauf, nicht mehr live aus den relays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-feedback: kommentar-inhalt aus app.html erschien sichtbar im
body der detail-seiten. Ursache: SvelteKit ersetzt jeden vorkommen
von %sveltekit.head%, auch innerhalb von HTML-kommentaren. Mein
beschreibender kommentar erwaehnte das wort als beispiel — wurde
dann zur build-zeit durch tatsaechliche head-tags ersetzt, was den
kommentar-block aufbrach (eingefuegte tags enthalten >, das schliesst
den kommentar) und den rest des kommentar-textes in den body
wandern liess.
Fix: kommentar umformuliert, sodass der platzhalter-name nicht mehr
woertlich vorkommt ("via SvelteKit-head-injection" statt "via
%sveltekit.head%").
Live-verifiziert auf banksy-high-court-prophet — body startet jetzt
sauber mit <header>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drei build-blocker beim ersten prerender-versuch identifiziert und gefixt:
1. svelte.config.js: handleHttpError + handleMissingId fuer den
prerender-crawler. Der crawler folgt zur build-zeit allen hrefs/srcs
im HTML — sieht dabei
- __SITE_URL__-platzhalter in canonical/hreflang (werden im deploy
per sed ersetzt, sind keine echten routes)
- relative bild-paths in alten posts (z.B. h01-json-import.png)
- anchor-links auf headings ohne id-attribute (#ACF-JSON-Export)
Alle drei sind keine echten 404s — handlers ignorieren sie.
2. +page.svelte: <script type="application/ld+json">{jsonLd}</script>
in <svelte:head> rendert {jsonLd} als literalen string, weil svelte
den script-tag-inhalt nicht als expression evaluiert. Zurueck zu
{@html ...} mit </script>-escape-hardening, damit titel oder
beschreibungen mit </script> den output nicht aufbrechen koennen.
3. app.html behaelt seine homepage-defaults fuer og:title/og:url/
og:description/canonical — der prerender-crawler rendert nur
detail-routen (/<slug>/), die homepage bleibt SPA-only und braucht
die defaults im app.html-template, weil dort kein svelte:head greift.
Detail-routen ueberschreiben per <svelte:head>; last-wins greift bei
LinkedIn/Mastodon/Browser. Facebook/Twitter (first-wins) haetten
einen homepage-prerender-schritt noetig — folge-aufgabe.
Plus snapshot/deno.lock committed — deno empfiehlt lockfile-commit fuer
reproduzierbare CI-builds, analog package-lock.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot-pfad bekommt feature-paritaet mit dem runtime-fallback:
- Sprach-switcher (inline, gleiche optik wie LanguageAvailability,
ohne neue i18n-keys — verwendet snapshot.translations direkt)
- Tag-liste mit links auf /tag/<name>/
- Reactions, ExternalClientLinks, ReplyComposer, ReplyList
(alle dtag-basiert, brauchen keine NostrEvent-konstruktion)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot-pfad: page+head komplett aus json, mit og/twitter/jsonld/hreflang.
Runtime-fallback: falls data.snapshot null, loadPost+PostView wie bisher.
Reactions/replies kommen im naechsten task.
svelte:head auf top-level verschoben (svelte-constraint: keine meta-tags
innerhalb von {#if}-bloecken erlaubt).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lokaler override des global ssr=false. entries() liest aus
snapshot/output/index.json, load() pro-slug aus posts/<slug>.json.
runtime-fallback bleibt fuer slugs ausserhalb des snapshots.
@types/node als devDependency ergaenzt, da node:fs/promises-Typen
fuer den SSR-Pfad benoetigt werden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dead-code aus etappe 1 nachgezogen:
- dompurify + @types/dompurify aus package.json (jetzt isomorphic-dompurify
als einziger sanitizer, bringt eigene typen mit)
- design-rationale-kommentar fuer markedInstance zurueckgebracht
(Spec §3: lokale ersetzbarkeit der engine — nicht aus dem code ablesbar)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Funktioniert jetzt sowohl in Browser/jsdom als auch in Node (SvelteKit-Build).
Schritt 1 der prerender-snapshot-migration. Verhalten in der SPA unveraendert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
statt text-hinweis "auch verfügbar in: ..." zeigt der post jetzt einen
kompakten switcher (📖 aktiver-code | anderer-code). klick auf den
anderen code setzt die ui-sprache global und navigiert zur sprach-
variante — alles konsistent.
language names raus (unused): displayLanguage + tests entfernt, da die
darstellung nun nur noch sprachcodes (DE/EN) zeigt. auch i18n-keys
lang.de/lang.en und post.also_available_in aufgeräumt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bei navigation zwischen slugs innerhalb der gleichen [...slug]-route
bleibt die komponente montiert — onMount feuert dann nicht mehr, und
der neue post lud erst nach manuellem reload. $effect auf dtag löst
das und rendert die neue view sofort.
race-condition-guard: currentDtag wird pro effect-lauf festgefroren;
stale responses werden verworfen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
app.html nutzt __SITE_URL__ als platzhalter in og:url und canonical.
deploy-svelte.sh ersetzt ihn nach dem build pro ziel via sed:
- svelte → https://svelte.joerg-lohrer.de (default, bisheriger SVELTE_FTP_-pfad)
- staging → https://staging.joerg-lohrer.de (STAGING_FTP_-pfad, webroot joerglohrer26)
- prod → https://joerg-lohrer.de (STAGING_FTP_-pfad, cutover-ziel)
env-auslese aus .env.local nicht mehr via `source` (bricht bei
sonderzeichen im passwort), sondern via awk pro schlüssel. build wird
jetzt vom deploy-skript angestoßen, damit immer gegen den frischen
html-stand gebaut wird.
app/.env.example dokumentiert PUBLIC_SITE_URL (derzeit ungenutzt, da
der platzhalter-ansatz zuverlässiger ist als runtime-env für prerender).
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>