spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
<script lang="ts">
|
2026-04-28 08:41:11 +02:00
|
|
|
|
import type { NostrEvent } from '$lib/nostr/loaders'
|
2026-04-28 08:46:12 +02:00
|
|
|
|
import Reactions from '$lib/components/Reactions.svelte'
|
|
|
|
|
|
import ReplyList from '$lib/components/ReplyList.svelte'
|
|
|
|
|
|
import ReplyComposer from '$lib/components/ReplyComposer.svelte'
|
|
|
|
|
|
import ExternalClientLinks from '$lib/components/ExternalClientLinks.svelte'
|
2026-04-28 08:41:11 +02:00
|
|
|
|
import { renderMarkdown } from '$lib/render/markdown'
|
|
|
|
|
|
import { t } from '$lib/i18n'
|
2026-04-28 08:46:12 +02:00
|
|
|
|
import type { SignedEvent } from '$lib/nostr/signer'
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
let { data } = $props()
|
|
|
|
|
|
const dtag = $derived(data.dtag)
|
|
|
|
|
|
const snapshot = $derived(data.snapshot)
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
const siteUrl = '__SITE_URL__'
|
2026-04-28 09:25:14 +02:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
const canonical = $derived(`${siteUrl}/${snapshot?.slug ?? dtag}/`)
|
2026-04-28 09:25:14 +02:00
|
|
|
|
const ogImage = $derived(snapshot?.cover_image?.url ?? DEFAULT_OG_IMAGE)
|
2026-04-28 08:41:11 +02:00
|
|
|
|
const ogImageAlt = $derived(
|
|
|
|
|
|
snapshot?.cover_image?.alt ?? snapshot?.title ?? 'Jörg Lohrer',
|
|
|
|
|
|
)
|
2026-04-28 09:25:14 +02:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-04-28 08:41:11 +02:00
|
|
|
|
const bodyHtmlPrerendered = $derived(
|
|
|
|
|
|
snapshot ? renderMarkdown(snapshot.content_markdown) : '',
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-28 08:46:12 +02:00
|
|
|
|
let optimisticReplies: NostrEvent[] = $state([])
|
|
|
|
|
|
function handlePublished(signed: SignedEvent) {
|
|
|
|
|
|
optimisticReplies = [...optimisticReplies, signed as unknown as NostrEvent]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
const jsonLd = $derived(
|
|
|
|
|
|
snapshot
|
|
|
|
|
|
? JSON.stringify({
|
|
|
|
|
|
'@context': 'https://schema.org',
|
|
|
|
|
|
'@type': 'Article',
|
|
|
|
|
|
headline: snapshot.title,
|
|
|
|
|
|
description: snapshot.summary,
|
|
|
|
|
|
datePublished: new Date(snapshot.published_at * 1000).toISOString(),
|
|
|
|
|
|
dateModified: new Date(snapshot.created_at * 1000).toISOString(),
|
|
|
|
|
|
author: { '@type': 'Person', name: 'Jörg Lohrer' },
|
|
|
|
|
|
inLanguage: snapshot.lang,
|
|
|
|
|
|
image: ogImage,
|
|
|
|
|
|
mainEntityOfPage: canonical,
|
|
|
|
|
|
})
|
|
|
|
|
|
: '',
|
|
|
|
|
|
)
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
<svelte:head>
|
|
|
|
|
|
{#if snapshot}
|
|
|
|
|
|
<title>{snapshot.title} – Jörg Lohrer</title>
|
|
|
|
|
|
<meta name="description" content={snapshot.summary} />
|
|
|
|
|
|
<link rel="canonical" href={canonical} />
|
|
|
|
|
|
<meta property="og:type" content="article" />
|
|
|
|
|
|
<meta property="og:title" content={snapshot.title} />
|
|
|
|
|
|
<meta property="og:description" content={snapshot.summary} />
|
|
|
|
|
|
<meta property="og:url" content={canonical} />
|
|
|
|
|
|
<meta property="og:locale" content={snapshot.lang === 'de' ? 'de_DE' : 'en_US'} />
|
|
|
|
|
|
<meta property="og:image" content={ogImage} />
|
|
|
|
|
|
<meta property="og:image:alt" content={ogImageAlt} />
|
2026-04-28 09:25:14 +02:00
|
|
|
|
{#if ogImageWidth}
|
|
|
|
|
|
<meta property="og:image:width" content={String(ogImageWidth)} />
|
2026-04-28 08:41:11 +02:00
|
|
|
|
{/if}
|
2026-04-28 09:25:14 +02:00
|
|
|
|
{#if ogImageHeight}
|
|
|
|
|
|
<meta property="og:image:height" content={String(ogImageHeight)} />
|
2026-04-28 08:41:11 +02:00
|
|
|
|
{/if}
|
|
|
|
|
|
<meta property="article:published_time" content={new Date(snapshot.published_at * 1000).toISOString()} />
|
|
|
|
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
|
|
|
|
<meta name="twitter:title" content={snapshot.title} />
|
|
|
|
|
|
<meta name="twitter:description" content={snapshot.summary} />
|
|
|
|
|
|
<meta name="twitter:image" content={ogImage} />
|
|
|
|
|
|
{#each snapshot.translations as alt}
|
|
|
|
|
|
<link rel="alternate" hreflang={alt.lang} href={`${siteUrl}/${alt.slug}/`} />
|
|
|
|
|
|
{/each}
|
2026-04-28 09:25:14 +02:00
|
|
|
|
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
|
2026-04-28 08:50:27 +02:00
|
|
|
|
{@html `<script type="application/ld+json">${jsonLd.replace(/<\/script>/gi, '<\\/script>')}</script>`}
|
2026-04-28 08:41:11 +02:00
|
|
|
|
{/if}
|
|
|
|
|
|
</svelte:head>
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
<nav class="breadcrumb"><a href="/">{$t('post.back_to_overview')}</a></nav>
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
|
2026-04-28 08:41:11 +02:00
|
|
|
|
{#if snapshot}
|
|
|
|
|
|
<article class="post">
|
|
|
|
|
|
<h1 class="post-title">{snapshot.title}</h1>
|
2026-04-28 08:46:12 +02:00
|
|
|
|
{#if snapshot.translations.length > 0}
|
|
|
|
|
|
<p class="lang-switch" role="group" aria-label="Article language">
|
|
|
|
|
|
<span class="icon" aria-hidden="true">📖</span>
|
|
|
|
|
|
<span class="btn active" aria-current="true">{snapshot.lang.toUpperCase()}</span>
|
|
|
|
|
|
{#each [...snapshot.translations].sort((a, b) => a.lang.localeCompare(b.lang)) as alt}
|
|
|
|
|
|
<span class="sep" aria-hidden="true">|</span>
|
|
|
|
|
|
<a class="btn" href={`/${alt.slug}/`}>{alt.lang.toUpperCase()}</a>
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{/if}
|
2026-04-28 08:41:11 +02:00
|
|
|
|
{#if snapshot.cover_image}
|
|
|
|
|
|
<p class="cover">
|
|
|
|
|
|
<img src={snapshot.cover_image.url} alt={snapshot.cover_image.alt ?? ''} />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
{#if snapshot.summary}
|
|
|
|
|
|
<p class="summary">{snapshot.summary}</p>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
<div class="body">{@html bodyHtmlPrerendered}</div>
|
2026-04-28 08:46:12 +02:00
|
|
|
|
{#if snapshot.tags.length > 0}
|
|
|
|
|
|
<div class="tags">
|
|
|
|
|
|
{#each snapshot.tags as tag}
|
|
|
|
|
|
<a class="tag" href={`/tag/${encodeURIComponent(tag)}/`}>{tag}</a>
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
<Reactions dtag={snapshot.slug} />
|
|
|
|
|
|
<ExternalClientLinks dtag={snapshot.slug} />
|
|
|
|
|
|
<ReplyComposer dtag={snapshot.slug} eventId={snapshot.event_id} onPublished={handlePublished} />
|
|
|
|
|
|
<ReplyList dtag={snapshot.slug} optimistic={optimisticReplies} />
|
2026-04-28 08:41:11 +02:00
|
|
|
|
</article>
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-04-28 08:41:11 +02:00
|
|
|
|
.breadcrumb {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.breadcrumb a {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.breadcrumb a:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
.post-title {
|
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
|
line-height: 1.25;
|
|
|
|
|
|
margin: 0 0 0.4rem;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.post-title {
|
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.cover {
|
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
|
margin: 1rem auto 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.cover img {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.summary {
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(img) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(a) {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(pre) {
|
|
|
|
|
|
background: var(--code-bg);
|
|
|
|
|
|
padding: 0.8rem;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
font-size: 0.88em;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(code) {
|
|
|
|
|
|
background: var(--code-bg);
|
|
|
|
|
|
padding: 1px 4px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 0.92em;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(pre code) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
word-break: normal;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(hr) {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
|
margin: 2rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body :global(blockquote) {
|
|
|
|
|
|
border-left: 3px solid var(--border);
|
|
|
|
|
|
padding: 0 0 0 1rem;
|
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
2026-04-28 08:46:12 +02:00
|
|
|
|
.lang-switch {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
margin: 0.25rem 0 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.icon {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
padding: 1px 7px;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn:hover:not(.active) {
|
|
|
|
|
|
color: var(--fg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn.active {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.sep {
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tags {
|
|
|
|
|
|
margin-top: 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tag {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
background: var(--code-bg);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
padding: 1px 7px;
|
|
|
|
|
|
margin: 0 4px 4px 0;
|
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
|
color: var(--fg);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tag:hover {
|
|
|
|
|
|
background: var(--border);
|
|
|
|
|
|
}
|
spa(phase 3, tasks 15-22): routing, komponenten, home, postview
Phase 3 komplett:
- Task 15: LoadingOrError-Komponente (loading/error-states, Habla-Fallback)
- Task 16: app.html mit CSS-Variablen (light/dark), Base-Typography
- Task 17: +layout.svelte mit Container + bootstrapReadRelays onMount
- Task 18: ProfileCard-Komponente (Avatar, Name, About, NIP-05, Website)
- Task 19: PostCard-Komponente (Thumbnail + Titel/Summary/Datum), responsive
- Task 20: +page.svelte als Home (Profil + Liste, Promise.all für beides)
- Task 21: PostView-Komponente (Titel, Meta, Cover, Summary, Markdown-Body)
- Task 22: [...slug]/+page.ts+svelte — Catch-all-Route mit Legacy-301-Redirect
Alle $props()-abhängigen Werte via $derived() (Svelte-5-Runes-Konformität).
npm run check: 0 errors, 0 warnings, 592 files. npm run build grün.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:39:24 +02:00
|
|
|
|
</style>
|