From bb9d35076d610d724c21bbc004dd82ebcba49473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Tue, 28 Apr 2026 09:25:14 +0200 Subject: [PATCH] chore(spa): toter code aus pre-prerender-aera entfernt Nach etappe 5 (runtime-fallback entfernt) sind diese files/exports ohne aufrufer: Files (ganz weg): - app/src/lib/components/PostView.svelte - app/src/lib/components/LanguageAvailability.svelte - app/src/lib/nostr/translations.ts - app/src/lib/nostr/translations.test.ts - app/src/lib/nostr/loaders.loadTranslations.test.ts Aus app/src/lib/nostr/loaders.ts entfernt: - loadPost(), loadTranslations(), TranslationInfo - resolveTranslationsFromRefs() (nur von loadTranslations.test.ts genutzt) - TranslationRef-import von ./translations Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src/app.html | 2 +- .../components/LanguageAvailability.svelte | 107 ----------- app/src/lib/components/PostView.svelte | 174 ------------------ .../nostr/loaders.loadTranslations.test.ts | 74 -------- app/src/lib/nostr/loaders.ts | 68 ------- app/src/lib/nostr/translations.test.ts | 51 ----- app/src/lib/nostr/translations.ts | 27 --- app/src/routes/[...slug]/+page.svelte | 35 +++- 8 files changed, 28 insertions(+), 510 deletions(-) delete mode 100644 app/src/lib/components/LanguageAvailability.svelte delete mode 100644 app/src/lib/components/PostView.svelte delete mode 100644 app/src/lib/nostr/loaders.loadTranslations.test.ts delete mode 100644 app/src/lib/nostr/translations.test.ts delete mode 100644 app/src/lib/nostr/translations.ts diff --git a/app/src/app.html b/app/src/app.html index d56bc50..dc464a7 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/app/src/lib/components/LanguageAvailability.svelte b/app/src/lib/components/LanguageAvailability.svelte deleted file mode 100644 index 12a4c08..0000000 --- a/app/src/lib/components/LanguageAvailability.svelte +++ /dev/null @@ -1,107 +0,0 @@ - - -{#if !loading && translations.length > 0} -

- - {#each options as opt, i} - {#if opt.href === null} - {opt.code.toUpperCase()} - {:else} - - {/if} - {#if i < options.length - 1}{/if} - {/each} -

-{/if} - - diff --git a/app/src/lib/components/PostView.svelte b/app/src/lib/components/PostView.svelte deleted file mode 100644 index 755ff30..0000000 --- a/app/src/lib/components/PostView.svelte +++ /dev/null @@ -1,174 +0,0 @@ - - -

{title}

-
- {$t('post.published_on', { values: { date } })} - {#if tags.length > 0} -
- {#each tags as t} - {t} - {/each} -
- {/if} -
- - - -{#if image} -

Cover-Bild

-{/if} - -{#if summary} -

{summary}

-{/if} - -
{@html bodyHtml}
- -{#if dtag} - - - - -{/if} - - diff --git a/app/src/lib/nostr/loaders.loadTranslations.test.ts b/app/src/lib/nostr/loaders.loadTranslations.test.ts deleted file mode 100644 index eb4578c..0000000 --- a/app/src/lib/nostr/loaders.loadTranslations.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { resolveTranslationsFromRefs } from './loaders'; -import type { NostrEvent } from './loaders'; -import type { TranslationRef } from './translations'; - -function ev(tags: string[][]): NostrEvent { - return { - id: 'x', - pubkey: 'p', - created_at: 0, - kind: 30023, - tags, - content: '', - sig: 's' - } as unknown as NostrEvent; -} - -describe('resolveTranslationsFromRefs', () => { - it('liefert lang/slug/title für jeden aufgelösten ref', async () => { - const refs: TranslationRef[] = [ - { kind: 30023, pubkey: 'p1', dtag: 'hello' } - ]; - const fetcher = async () => [ - ev([ - ['d', 'hello'], - ['title', 'Hello World'], - ['L', 'ISO-639-1'], - ['l', 'en', 'ISO-639-1'] - ]) - ]; - const result = await resolveTranslationsFromRefs(refs, fetcher); - expect(result).toEqual([ - { lang: 'en', slug: 'hello', title: 'Hello World' } - ]); - }); - - it('ignoriert refs, zu denen kein event gefunden wird', async () => { - const refs: TranslationRef[] = [ - { kind: 30023, pubkey: 'p1', dtag: 'hello' }, - { kind: 30023, pubkey: 'p1', dtag: 'missing' } - ]; - const fetcher = async (r: TranslationRef) => - r.dtag === 'hello' - ? [ev([ - ['d', 'hello'], - ['title', 'Hi'], - ['l', 'en', 'ISO-639-1'] - ])] - : []; - const result = await resolveTranslationsFromRefs(refs, fetcher); - expect(result).toEqual([{ lang: 'en', slug: 'hello', title: 'Hi' }]); - }); - - it('ignoriert events ohne l-tag (sprache unklar)', async () => { - const refs: TranslationRef[] = [ - { kind: 30023, pubkey: 'p', dtag: 'x' } - ]; - const fetcher = async () => [ - ev([ - ['d', 'x'], - ['title', 'kein lang-tag'] - ]) - ]; - const result = await resolveTranslationsFromRefs(refs, fetcher); - expect(result).toEqual([]); - }); - - it('leere ref-liste → leere ergebnis-liste', async () => { - const fetcher = async () => { - throw new Error('should not be called'); - }; - expect(await resolveTranslationsFromRefs([], fetcher)).toEqual([]); - }); -}); diff --git a/app/src/lib/nostr/loaders.ts b/app/src/lib/nostr/loaders.ts index 15961eb..b681b48 100644 --- a/app/src/lib/nostr/loaders.ts +++ b/app/src/lib/nostr/loaders.ts @@ -6,7 +6,6 @@ import type { Filter as ApplesauceFilter } from 'applesauce-core/helpers/filter' import { pool } from './pool'; import { readRelays } from '$lib/stores/readRelays'; import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config'; -import type { TranslationRef } from './translations'; /** Re-export als sprechenden Alias */ export type { NostrEvent }; @@ -89,21 +88,6 @@ export async function loadPostList( }); } -/** Einzelpost per d-Tag */ -export async function loadPost(dtag: string): Promise { - const relays = get(readRelays); - const events = await collectEvents(relays, { - kinds: [30023], - authors: [AUTHOR_PUBKEY_HEX], - '#d': [dtag], - limit: 1 - }); - if (events.length === 0) return null; - return events.reduce((best, cur) => - cur.created_at > best.created_at ? cur : best - ); -} - /** * Profil-Event kind:0 (neueste Version). * Default: Autoren-Pubkey der SPA. Optional: beliebiger Pubkey für @@ -190,55 +174,3 @@ export async function loadReactions(dtag: string): Promise { .map(([content, count]) => ({ content, count })) .sort((a, b) => b.count - a.count); } - -export interface TranslationInfo { - lang: string; - slug: string; - title: string; -} - -/** - * Pure Variante für Tests — erhält die Events via Fetcher statt Relays. - */ -export async function resolveTranslationsFromRefs( - refs: TranslationRef[], - fetcher: (ref: TranslationRef) => Promise -): Promise { - if (refs.length === 0) return []; - const results = await Promise.all(refs.map(fetcher)); - const infos: TranslationInfo[] = []; - for (let i = 0; i < refs.length; i++) { - const evs = results[i]; - if (evs.length === 0) continue; - const latest = evs.reduce((best, cur) => - cur.created_at > best.created_at ? cur : best - ); - const lang = latest.tags.find((t) => t[0] === 'l')?.[1]; - if (!lang) continue; - const slug = latest.tags.find((t) => t[0] === 'd')?.[1] ?? refs[i].dtag; - const title = latest.tags.find((t) => t[0] === 'title')?.[1] ?? ''; - infos.push({ lang, slug, title }); - } - return infos; -} - -/** - * Loader: findet die anderssprachigen Varianten eines Posts. - * Liefert leere Liste, wenn keine a-Tags mit marker "translation" vorhanden. - */ -export async function loadTranslations( - event: NostrEvent -): Promise { - const { parseTranslationRefs } = await import('./translations'); - const refs = parseTranslationRefs(event); - if (refs.length === 0) return []; - const relays = get(readRelays); - return resolveTranslationsFromRefs(refs, (ref) => - collectEvents(relays, { - kinds: [ref.kind], - authors: [ref.pubkey], - '#d': [ref.dtag], - limit: 1 - }) - ); -} diff --git a/app/src/lib/nostr/translations.test.ts b/app/src/lib/nostr/translations.test.ts deleted file mode 100644 index df82317..0000000 --- a/app/src/lib/nostr/translations.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseTranslationRefs } from './translations'; -import type { NostrEvent } from './loaders'; - -function ev(tags: string[][]): NostrEvent { - return { - id: 'x', - pubkey: 'p', - created_at: 0, - kind: 30023, - tags, - content: '', - sig: 's' - } as unknown as NostrEvent; -} - -describe('parseTranslationRefs', () => { - it('extrahiert a-tags mit marker "translation"', () => { - const e = ev([ - ['d', 'x'], - ['a', '30023:abc:other-slug', '', 'translation'], - ['a', '30023:abc:third-slug', '', 'translation'] - ]); - expect(parseTranslationRefs(e)).toEqual([ - { kind: 30023, pubkey: 'abc', dtag: 'other-slug' }, - { kind: 30023, pubkey: 'abc', dtag: 'third-slug' } - ]); - }); - - it('ignoriert a-tags ohne marker "translation"', () => { - const e = ev([ - ['a', '30023:abc:root-thread', '', 'root'], - ['a', '30023:abc:x', '', 'reply'] - ]); - expect(parseTranslationRefs(e)).toEqual([]); - }); - - it('ignoriert a-tags mit malformed coordinate', () => { - const e = ev([ - ['a', 'not-a-coord', '', 'translation'], - ['a', '30023:abc:ok', '', 'translation'] - ]); - expect(parseTranslationRefs(e)).toEqual([ - { kind: 30023, pubkey: 'abc', dtag: 'ok' } - ]); - }); - - it('leeres tag-array → leere liste', () => { - expect(parseTranslationRefs(ev([]))).toEqual([]); - }); -}); diff --git a/app/src/lib/nostr/translations.ts b/app/src/lib/nostr/translations.ts deleted file mode 100644 index 8b1cd6c..0000000 --- a/app/src/lib/nostr/translations.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { NostrEvent } from './loaders'; - -export interface TranslationRef { - kind: number; - pubkey: string; - dtag: string; -} - -const COORD_RE = /^(\d+):([0-9a-f]+):([a-z0-9][a-z0-9-]*)$/; - -export function parseTranslationRefs(event: NostrEvent): TranslationRef[] { - const refs: TranslationRef[] = []; - for (const tag of event.tags) { - if (tag[0] !== 'a') continue; - if (tag[3] !== 'translation') continue; - const coord = tag[1]; - if (typeof coord !== 'string') continue; - const m = coord.match(COORD_RE); - if (!m) continue; - refs.push({ - kind: parseInt(m[1], 10), - pubkey: m[2], - dtag: m[3] - }); - } - return refs; -} diff --git a/app/src/routes/[...slug]/+page.svelte b/app/src/routes/[...slug]/+page.svelte index 8b9f894..baf3b82 100644 --- a/app/src/routes/[...slug]/+page.svelte +++ b/app/src/routes/[...slug]/+page.svelte @@ -13,13 +13,32 @@ const snapshot = $derived(data.snapshot) const siteUrl = '__SITE_URL__' + + // Site-default-OG-bild aus app/static. Dimensionen sind hartcodiert, + // weil das asset stabil ist (siehe spec §Algorithmus-Schritt 8). + const DEFAULT_OG_IMAGE = `${siteUrl}/joerg-profil-2024.webp` + const DEFAULT_OG_IMAGE_WIDTH = 512 + const DEFAULT_OG_IMAGE_HEIGHT = 512 + const canonical = $derived(`${siteUrl}/${snapshot?.slug ?? dtag}/`) - const ogImage = $derived( - snapshot?.cover_image?.url ?? `${siteUrl}/joerg-profil-2024.webp`, - ) + const ogImage = $derived(snapshot?.cover_image?.url ?? DEFAULT_OG_IMAGE) const ogImageAlt = $derived( snapshot?.cover_image?.alt ?? snapshot?.title ?? 'Jörg Lohrer', ) + const ogImageWidth = $derived( + snapshot?.cover_image?.width ?? (snapshot?.cover_image ? undefined : DEFAULT_OG_IMAGE_WIDTH), + ) + const ogImageHeight = $derived( + snapshot?.cover_image?.height ?? (snapshot?.cover_image ? undefined : DEFAULT_OG_IMAGE_HEIGHT), + ) + // x-default zeigt auf die DE-variante, weil der autor DE-first arbeitet. + // Bei EN-posts: DE-slug aus translations[] suchen; sonst (DE-post) + // bleibt x-default = canonical. + const xDefaultHref = $derived( + snapshot?.lang === 'en' + ? `${siteUrl}/${snapshot.translations.find((tr) => tr.lang === 'de')?.slug ?? snapshot.slug}/` + : canonical, + ) const bodyHtmlPrerendered = $derived( snapshot ? renderMarkdown(snapshot.content_markdown) : '', ) @@ -59,11 +78,11 @@ - {#if snapshot.cover_image?.width} - + {#if ogImageWidth} + {/if} - {#if snapshot.cover_image?.height} - + {#if ogImageHeight} + {/if} @@ -73,7 +92,7 @@ {#each snapshot.translations as alt} {/each} - + {@html ``} {/if}