feat(posts): search + year filter + upcoming events on tag routes
Deploy to Firebase Hosting / deploy (push) Successful in 3m34s
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:
@@ -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 {
|
||||
|
||||
@@ -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 ?? {};
|
||||
---
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user