feat(posts): search + year filter + upcoming events on tag routes
Deploy to Firebase Hosting / deploy (push) Successful in 3m34s

Extracts buildPostSearchIndex + selectUpcomingEvents into blog-utils so
all four routes (posts index, posts pagination, tag, tag pagination)
share the same feature set.
This commit is contained in:
Peter Meier
2026-04-14 15:54:00 +02:00
parent e66ff01e18
commit e8bdb9e987
5 changed files with 70 additions and 23 deletions
+42
View File
@@ -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 {
+4 -23
View File
@@ -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 ?? {};
---
+8
View File
@@ -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
/>
)}
</div>
+8
View File
@@ -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
/>
)}
</div>
@@ -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
/>
)}
</div>