feat(spa): detail-route auf prerender + ssr=true

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>
This commit is contained in:
Jörg Lohrer 2026-04-28 08:40:17 +02:00
parent 3fa85fcb07
commit b5772b8aa2
2 changed files with 74 additions and 16 deletions

View File

@ -20,6 +20,7 @@
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@types/node": "^25.6.0",
"jsdom": "^29.0.2", "jsdom": "^29.0.2",
"svelte": "^5.55.2", "svelte": "^5.55.2",
"svelte-check": "^4.4.6", "svelte-check": "^4.4.6",

View File

@ -1,21 +1,78 @@
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'
import { parseLegacyUrl, canonicalPostPath } from '$lib/url/legacy'; import { parseLegacyUrl, canonicalPostPath } from '$lib/url/legacy'
import type { PageLoad } from './$types'; import type { EntryGenerator, PageLoad } from './$types'
import { browser } from '$app/environment'
export const ssr = true
export const prerender = true
export const trailingSlash = 'always'
interface SnapshotIndex {
posts: Array<{ slug: string; lang: string; title: string }>
}
interface PostJson {
slug: string
event_id: string
created_at: number
published_at: number
title: string
summary: string
lang: string
cover_image: { url: string; alt?: string; width?: number; height?: number; mime?: string } | null
content_markdown: string
tags: string[]
naddr: string
habla_url: string
translations: Array<{ lang: string; slug: string; title: string }>
}
let cachedIndex: SnapshotIndex | undefined
async function readIndex(): Promise<SnapshotIndex> {
if (cachedIndex) return cachedIndex
const fs = await import('node:fs/promises')
const path = await import('node:path')
const dir = path.resolve('../snapshot/output')
const text = await fs.readFile(path.join(dir, 'index.json'), 'utf-8')
cachedIndex = JSON.parse(text) as SnapshotIndex
return cachedIndex
}
async function readPost(slug: string): Promise<PostJson | undefined> {
try {
const fs = await import('node:fs/promises')
const path = await import('node:path')
const dir = path.resolve('../snapshot/output')
const text = await fs.readFile(path.join(dir, 'posts', `${slug}.json`), 'utf-8')
return JSON.parse(text) as PostJson
} catch {
return undefined
}
}
export const entries: EntryGenerator = async () => {
const idx = await readIndex()
return idx.posts.map((p) => ({ slug: p.slug }))
}
export const load: PageLoad = async ({ url }) => { export const load: PageLoad = async ({ url }) => {
const pathname = url.pathname; const pathname = url.pathname
// Legacy-Form /YYYY/MM/DD/<dtag>.html/ → Redirect auf /<dtag>/ const legacyDtag = parseLegacyUrl(pathname)
const legacyDtag = parseLegacyUrl(pathname); if (legacyDtag) {
if (legacyDtag) { throw redirect(301, canonicalPostPath(legacyDtag))
throw redirect(301, canonicalPostPath(legacyDtag)); }
}
// Kanonisch: /<dtag>/ — erster Segment des Pfades. const segments = pathname.replace(/^\/+|\/+$/g, '').split('/')
const segments = pathname.replace(/^\/+|\/+$/g, '').split('/'); if (segments.length !== 1 || !segments[0]) {
if (segments.length !== 1 || !segments[0]) { throw error(404, 'Seite nicht gefunden')
throw error(404, 'Seite nicht gefunden'); }
} const dtag = decodeURIComponent(segments[0])
return { dtag: decodeURIComponent(segments[0]) }; if (!browser) {
}; const snapshot = await readPost(dtag)
if (snapshot) return { dtag, snapshot }
}
return { dtag, snapshot: null }
}