From 892afc2d9fd5789822fde3c0a3bfca7d9e3c9cf6 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Thu, 7 May 2026 16:08:16 +0200 Subject: [PATCH] refactor(post-events): read event data from resolved calendarItem Replace direct post.isEvent/eventDate/eventLocation reads with getPostEventInfo() helper that derives info from the resolved calendarItem reference. Loaders now resolve "calendarItem" and request that field instead of legacy flat properties. Also regenerate iconify mdi subset (map-marker-outline, download, google, calendar-search, refresh). Co-Authored-By: Claude Opus 4.7 --- src/lib/blog-utils.ts | 110 +++++++++++++++------- src/lib/components/BlogOverview.svelte | 9 +- src/lib/components/PostCard.svelte | 15 +-- src/lib/iconify-mdi-subset.generated.json | 21 ++++- src/routes/api/search-index/+server.ts | 8 +- src/routes/post/[slug]/+page.server.ts | 10 +- 6 files changed, 112 insertions(+), 61 deletions(-) diff --git a/src/lib/blog-utils.ts b/src/lib/blog-utils.ts index de5142e..7c3c29f 100644 --- a/src/lib/blog-utils.ts +++ b/src/lib/blog-utils.ts @@ -121,6 +121,44 @@ export function filterPostsByTag( }); } +export type PostEventLocation = { + text?: string; + lat?: number; + lng?: number; +}; + +export type PostEventInfo = { + isEvent: boolean; + eventDate: string | null; + eventLocation: PostEventLocation | null; +}; + +/** + * Liest event-Daten aus dem aufgelösten calendarItem-Reference des Posts. + * Voraussetzung: post mit resolve=["all"] oder explizit "calendarItem" geladen, + * sodass `post.calendarItem` als Objekt mit terminZeit/location vorliegt. + */ +export function getPostEventInfo( + post: PostEntry | null | undefined, +): PostEventInfo { + const ci = (post as { calendarItem?: unknown } | null | undefined)?.calendarItem; + if (!ci || typeof ci !== "object") { + return { isEvent: false, eventDate: null, eventLocation: null }; + } + const obj = ci as { + terminZeit?: string; + location?: PostEventLocation; + }; + if (!obj.terminZeit) { + return { isEvent: false, eventDate: null, eventLocation: null }; + } + return { + isEvent: true, + eventDate: obj.terminZeit, + eventLocation: obj.location ?? null, + }; +} + /** URL aus post.postImage (RustyCMS img mit src, oder legacy file.url). */ export function getPostImageUrl( postImage: PostEntry["postImage"], @@ -200,32 +238,33 @@ export type PostSearchIndexEntry = { eventDate: string | null; }; -type PostWithEventIndex = PostEntry & { - isEvent?: boolean; - eventDate?: string; +type PostWithImageIndex = PostEntry & { _resolvedImageUrl?: string; }; export function buildPostSearchIndex(posts: PostEntry[]): PostSearchIndexEntry[] { - return posts.map((p) => ({ - slug: p.slug ?? p._slug ?? "", - headline: p.headline ?? "", - excerpt: p.excerpt ?? "", - subheadline: p.subheadline ?? "", - tags: (p.postTag ?? []) - .map((t) => - typeof t === "string" - ? t - : (t as { name?: string; _slug?: string }).name ?? - (t as { _slug?: string })._slug ?? - "", - ) - .filter(Boolean), - image: (p as PostWithEventIndex)._resolvedImageUrl ?? null, - created: p.created ?? null, - isEvent: Boolean((p as PostWithEventIndex).isEvent), - eventDate: (p as PostWithEventIndex).eventDate ?? null, - })); + return posts.map((p) => { + const ev = getPostEventInfo(p); + return { + slug: p.slug ?? p._slug ?? "", + headline: p.headline ?? "", + excerpt: p.excerpt ?? "", + subheadline: p.subheadline ?? "", + tags: (p.postTag ?? []) + .map((t) => + typeof t === "string" + ? t + : (t as { name?: string; _slug?: string }).name ?? + (t as { _slug?: string })._slug ?? + "", + ) + .filter(Boolean), + image: (p as PostWithImageIndex)._resolvedImageUrl ?? null, + created: p.created ?? null, + isEvent: ev.isEvent, + eventDate: ev.eventDate, + }; + }); } export type LoadPostsResult = { @@ -260,7 +299,7 @@ export async function loadPostsList(opts: { // (post → postImage → img-Sidecar; post → postTag → name/icon/color). [posts, tags] = await Promise.all([ getPosts({ - resolve: ["postImage", "postTag"], + resolve: ["postImage", "postTag", "calendarItem"], depth: 2, fields: [ "_slug", @@ -270,8 +309,7 @@ export async function loadPostsList(opts: { "subheadline", "excerpt", "created", - "isEvent", - "eventDate", + "calendarItem", "hideFromListing", "postImage.*", "postTag.name", @@ -342,16 +380,18 @@ export async function loadPostsList(opts: { export function selectUpcomingEvents(posts: PostEntry[], max = 4): PostEntry[] { const now = Date.now(); - return (posts as PostWithEventIndex[]) - .filter( - (p) => p.isEvent && p.eventDate && new Date(p.eventDate).getTime() >= now, - ) - .sort( - (a, b) => - new Date(a.eventDate ?? 0).getTime() - - new Date(b.eventDate ?? 0).getTime(), - ) - .slice(0, max) as PostEntry[]; + type Scored = { post: PostEntry; ts: number }; + const scored: Scored[] = []; + for (const p of posts) { + const ev = getPostEventInfo(p); + if (!ev.isEvent || !ev.eventDate) continue; + const ts = new Date(ev.eventDate).getTime(); + if (ts >= now) scored.push({ post: p, ts }); + } + return scored + .sort((a, b) => a.ts - b.ts) + .slice(0, max) + .map((s) => s.post); } const POST_BASE = "/post"; diff --git a/src/lib/components/BlogOverview.svelte b/src/lib/components/BlogOverview.svelte index 55fe79a..fdcc8c6 100644 --- a/src/lib/components/BlogOverview.svelte +++ b/src/lib/components/BlogOverview.svelte @@ -6,7 +6,7 @@ import Tag from "./Tag.svelte"; import Pagination from "./Pagination.svelte"; import type { PostEntry } from "$lib/cms"; - import { postHref } from "$lib/blog-utils"; + import { postHref, getPostEventInfo } from "$lib/blog-utils"; import { t as tStatic, T } from "$lib/translations"; import type { Translations } from "$lib/translations"; import { @@ -34,8 +34,6 @@ eventDate: string | null; }; - type EventPost = PostEntry & { eventDate?: string; isEvent?: boolean }; - let { posts = [], tags = [], @@ -56,7 +54,7 @@ totalPosts?: number; basePath?: string; translations?: Translations | null; - upcomingEvents?: EventPost[]; + upcomingEvents?: PostEntry[]; searchIndex?: SearchIndexEntry[] | null; } = $props(); @@ -193,12 +191,13 @@