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}
-
-
-
-
-{#if image}
- 
-{/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}