import type { PageServerLoad } from "./$types"; import { error } from "@sveltejs/kit"; import { getCalendarItemBySlug } from "$lib/cms"; import { marked } from "$lib/markdown-safe"; import { env } from "$env/dynamic/public"; /** * Eigenständige Detail-Seite pro Termin: alle Infos + Aktionen ungekürzt, * eigene OG-Tags (Link-Previews FB/WhatsApp/Twitter) und ein Link zum * Kalender (KEIN Redirect mehr — die Seite ist das Ziel des QR-/Direktlinks). * * terminZeit ist mit Offset gespeichert (z. B. 10:00+02:00). Labels müssen in * Europe/Berlin gerendert werden, sonst zeigt der SSR-Prozess (Server-TZ=UTC) * 2h zu früh. */ const TZ = "Europe/Berlin"; export const load: PageServerLoad = async ({ params, setHeaders }) => { const slug = params.slug; const rawItem = await getCalendarItemBySlug(slug, { locale: "de" }); if (!rawItem) throw error(404, "Termin nicht gefunden"); const item = rawItem as typeof rawItem & { location?: unknown; description?: string; terminEnde?: string; tags?: string[]; image?: unknown; attachment?: { src?: string; label?: string }; link?: unknown; }; const siteBaseUrl = (env.PUBLIC_SITE_URL || "").replace(/\/$/, ""); const title = (item.title ?? "Termin").trim(); const terminZeit = item.terminZeit; const terminEnde = item.terminEnde ?? null; const hasTime = !!terminZeit && !/T00:00:00/.test(terminZeit); const weekdayLabel = terminZeit ? new Date(terminZeit).toLocaleDateString("de-DE", { weekday: "long", timeZone: TZ }) : ""; const dateLabel = terminZeit ? new Date(terminZeit).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric", timeZone: TZ, }) : ""; const timeLabel = hasTime ? new Date(terminZeit).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", timeZone: TZ, }) + " Uhr" : ""; const endDateLabel = terminEnde && !/T00:00:00/.test(terminEnde) ? new Date(terminEnde).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric", timeZone: TZ, }) : ""; const locObj = typeof item.location === "object" && item.location !== null ? (item.location as { text?: string; lat?: number; lng?: number }) : null; const locationText = locObj ? (locObj.text ?? "").trim() || null : typeof item.location === "string" ? item.location.trim() || null : null; const hasCoords = !!locObj && typeof locObj.lat === "number" && typeof locObj.lng === "number"; // Externer Link / Post-Link let externalLink: string | null = null; const link = item.link as { url?: string; _slug?: string } | string | undefined; if (typeof link === "string" && link) externalLink = link; else if (link && typeof link === "object") { if (link.url) externalLink = link.url; else if (link._slug) externalLink = `/post/${encodeURIComponent(link._slug)}`; } const descriptionHtml = item.description ? (marked.parse(item.description) as string) : ""; const descriptionPlain = descriptionHtml .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); const tags = Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === "string") : []; const seoDescription = [ dateLabel + (timeLabel ? `, ${timeLabel}` : ""), locationText, descriptionPlain ? descriptionPlain.slice(0, 200) : null, ] .filter(Boolean) .join(" · "); const socialImage = `${siteBaseUrl}/api/social/calendar/${encodeURIComponent(slug)}?format=og`; const calendarUrl = `/kalender/#event-${slug}`; const canonical = `${siteBaseUrl}/kalender/termin/${encodeURIComponent(slug)}/`; // schema.org Event — Rich-Result/SEO für Termine (Datum, Ort, Veranstalter). const jsonLd = terminZeit ? [ { "@context": "https://schema.org", "@type": "Event", name: title, startDate: terminZeit, ...(terminEnde ? { endDate: terminEnde } : {}), eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", eventStatus: "https://schema.org/EventScheduled", ...(siteBaseUrl ? { url: canonical } : {}), inLanguage: "de-DE", ...(descriptionPlain ? { description: descriptionPlain.slice(0, 500) } : {}), image: socialImage, organizer: { "@type": "Organization", name: "Windwiderstand Thüringen", ...(siteBaseUrl ? { url: siteBaseUrl } : {}), }, ...(locationText ? { location: { "@type": "Place", name: locationText, ...(hasCoords ? { geo: { "@type": "GeoCoordinates", latitude: locObj!.lat, longitude: locObj!.lng, }, } : {}), }, } : {}), }, ] : []; const breadcrumbItems = [ { label: "Start", href: "/" }, { label: "Kalender", href: "/kalender/" }, { label: title }, ]; setHeaders({ "cache-control": "public, s-maxage=300, stale-while-revalidate=3600", }); return { seoTitle: `${title} – Windwiderstand Thüringen`, seoDescription, socialImage, ogType: "article" as const, jsonLd, breadcrumbItems, calendarUrl, item: { slug, title, terminZeit: terminZeit ?? null, terminEnde, hasTime, weekdayLabel, dateLabel, timeLabel, endDateLabel, locationText, descriptionHtml, tags, image: item.image ?? null, attachment: item.attachment && item.attachment.src ? { src: item.attachment.src, label: item.attachment.label ?? "Anhang" } : null, externalLink, }, }; };