From e8bdb9e987f6c9cf9868f4fe95519b28832ba3bf Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Tue, 14 Apr 2026 15:54:00 +0200 Subject: [PATCH] feat(posts): search + year filter + upcoming events on tag routes Extracts buildPostSearchIndex + selectUpcomingEvents into blog-utils so all four routes (posts index, posts pagination, tag, tag pagination) share the same feature set. --- src/lib/blog-utils.ts | 42 +++++++++++++++++++++ src/pages/posts/index.astro | 27 ++----------- src/pages/posts/page/[page].astro | 8 ++++ src/pages/posts/tag/[tag].astro | 8 ++++ src/pages/posts/tag/[tag]/page/[page].astro | 8 ++++ 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/lib/blog-utils.ts b/src/lib/blog-utils.ts index 784398d..f6f3756 100644 --- a/src/lib/blog-utils.ts +++ b/src/lib/blog-utils.ts @@ -171,6 +171,48 @@ export function postHref(post: PostEntry): string { return norm ? `${POST_BASE}/${encodeURIComponent(norm)}` : POST_BASE; } +export type PostSearchIndexEntry = { + slug: string; + headline: string; + excerpt: string; + subheadline: string; + tags: string[]; + image: string | null; + created: string | null; + isEvent: boolean; + eventDate: string | null; +}; + +type PostWithEvent = PostEntry & { + isEvent?: boolean; + eventDate?: string; + _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 PostWithEvent)._resolvedImageUrl ?? null, + created: p.created ?? null, + isEvent: Boolean((p as PostWithEvent).isEvent), + eventDate: (p as PostWithEvent).eventDate ?? null, + })); +} + +export function selectUpcomingEvents(posts: PostEntry[], max = 4): PostEntry[] { + const now = Date.now(); + return (posts as PostWithEvent[]) + .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[]; +} + export function formatPostDate(value: string | undefined): string { if (!value) return ""; try { diff --git a/src/pages/posts/index.astro b/src/pages/posts/index.astro index b4837c5..1227696 100644 --- a/src/pages/posts/index.astro +++ b/src/pages/posts/index.astro @@ -15,6 +15,8 @@ import { getTagsMap, resolvePostTagsInPost, getPostImageUrl, + buildPostSearchIndex, + selectUpcomingEvents, } from '../../lib/blog-utils'; import { t, T } from '../../lib/translations'; @@ -49,29 +51,8 @@ const filtered = filterPostsByTag(posts, null); const totalPages = getTotalPages(filtered.length, perPage); const pagePosts = paginate(filtered, 1, perPage); -type PostWithEvent = (typeof posts)[number] & { - isEvent?: boolean; - eventDate?: string; - _resolvedImageUrl?: string; -}; -const nowMs = Date.now(); -const upcomingEvents = (posts as PostWithEvent[]) - .filter((p) => p.isEvent && p.eventDate && new Date(p.eventDate).getTime() >= nowMs) - .sort((a, b) => new Date(a.eventDate ?? 0).getTime() - new Date(b.eventDate ?? 0).getTime()) - .slice(0, 4); - -// Lightweight index for client-side search over the full archive. -const searchIndex = 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 PostWithEvent)._resolvedImageUrl ?? null, - created: p.created ?? null, - isEvent: Boolean((p as PostWithEvent).isEvent), - eventDate: (p as PostWithEvent).eventDate ?? null, -})); +const upcomingEvents = selectUpcomingEvents(posts); +const searchIndex = buildPostSearchIndex(posts); const translations = Astro.locals.translations ?? {}; --- diff --git a/src/pages/posts/page/[page].astro b/src/pages/posts/page/[page].astro index 79952c1..4d19085 100644 --- a/src/pages/posts/page/[page].astro +++ b/src/pages/posts/page/[page].astro @@ -10,6 +10,8 @@ import { paginate, getPostsPerPage, getPostImageUrl, + buildPostSearchIndex, + selectUpcomingEvents, } from '../../../lib/blog-utils'; import { t, T } from '../../../lib/translations'; @@ -60,6 +62,9 @@ const totalPages = getTotalPages(filtered.length, perPage); const pageNum = Math.min(currentPage, totalPages); const pagePosts = paginate(filtered, pageNum, perPage); +const upcomingEvents = selectUpcomingEvents(filtered); +const searchIndex = buildPostSearchIndex(filtered); + const translations = Astro.locals.translations ?? {}; --- @@ -90,6 +95,9 @@ const translations = Astro.locals.translations ?? {}; totalPosts={filtered.length} basePath="/posts" translations={translations} + upcomingEvents={upcomingEvents} + searchIndex={searchIndex} + client:load /> )} diff --git a/src/pages/posts/tag/[tag].astro b/src/pages/posts/tag/[tag].astro index 786a95c..dd089cc 100644 --- a/src/pages/posts/tag/[tag].astro +++ b/src/pages/posts/tag/[tag].astro @@ -12,6 +12,8 @@ import { getTagsMap, resolvePostTagsInPost, getPostImageUrl, + buildPostSearchIndex, + selectUpcomingEvents, } from '../../../lib/blog-utils'; import { t, T } from '../../../lib/translations'; @@ -72,6 +74,9 @@ const pagePosts = paginate(filtered, 1, perPage); const tagName = tags.find((t) => (t._slug ?? '') === tagSlug)?.name ?? tagSlug; +const upcomingEvents = selectUpcomingEvents(filtered); +const searchIndex = buildPostSearchIndex(filtered); + const translations = Astro.locals.translations ?? {}; --- @@ -103,6 +108,9 @@ const translations = Astro.locals.translations ?? {}; totalPosts={filtered.length} basePath="/posts" translations={translations} + upcomingEvents={upcomingEvents} + searchIndex={searchIndex} + client:load /> )} diff --git a/src/pages/posts/tag/[tag]/page/[page].astro b/src/pages/posts/tag/[tag]/page/[page].astro index 9ddc6ea..2bf3866 100644 --- a/src/pages/posts/tag/[tag]/page/[page].astro +++ b/src/pages/posts/tag/[tag]/page/[page].astro @@ -12,6 +12,8 @@ import { getTagsMap, resolvePostTagsInPost, getPostImageUrl, + buildPostSearchIndex, + selectUpcomingEvents, } from '../../../../../lib/blog-utils'; import { t, T } from '../../../../../lib/translations'; @@ -85,6 +87,9 @@ const pagePosts = paginate(filtered, pageNum, perPage); const tagName = tags.find((t) => (t._slug ?? '') === tagSlug)?.name ?? tagSlug; +const upcomingEvents = selectUpcomingEvents(filtered); +const searchIndex = buildPostSearchIndex(filtered); + const translations = Astro.locals.translations ?? {}; --- @@ -116,6 +121,9 @@ const translations = Astro.locals.translations ?? {}; totalPosts={filtered.length} basePath="/posts" translations={translations} + upcomingEvents={upcomingEvents} + searchIndex={searchIndex} + client:load /> )}