Optimize speech search and admin payloads
Some checks failed
/ deploy (push) Has been cancelled

This commit is contained in:
Loyyd 2026-07-05 14:37:47 +02:00
parent b1ebe2ad57
commit 561b28bebf
6 changed files with 223 additions and 38 deletions

52
src/lib/admin-speeches.ts Normal file
View file

@ -0,0 +1,52 @@
import type { ArchiveEntry } from "./archive";
export type SpeechAdminEntry = {
id: string;
path: string;
title: string;
publishedDate: string;
speechYear: string;
speaker: "rev" | "mrs" | "other";
source: string;
categories: string[];
tags: string[];
bodyUrl: string;
};
function encodePath(value: string): string {
return value.split("/").map(encodeURIComponent).join("/");
}
export function speechAdminEntryFromArchiveEntry(entry: ArchiveEntry, fallbackDate: string): SpeechAdminEntry {
const categories = entry.data.categories ?? [];
const tags = entry.data.tags ?? [];
const publishedDate = entry.data.published?.slice(0, 10) ?? fallbackDate;
const speechYear = entry.data.speechYear ?? entry.id.match(/\b(1\d{3}|20\d{2})\b/)?.[1] ?? new Date().getFullYear().toString();
const categoryText = categories.join(" ").toLowerCase();
const collection = entry.data.speechCollection ?? "";
const speaker = categoryText.includes("hak ja han") || collection === "mrs-hak-ja-han-moon"
? "mrs"
: categoryText.includes("rev sun myung moon") || /^\d{4}$/.test(collection) || /^\d{4}$/.test(speechYear)
? "rev"
: "other";
return {
id: entry.id,
path: `content/${entry.id}.md`,
title: entry.data.title ?? entry.id,
publishedDate,
speechYear,
speaker,
source: entry.data.source ?? "",
categories,
tags,
bodyUrl: `/admin/content/${encodePath(entry.id)}.json`,
};
}
export function speechAdminEntryWithBody(entry: ArchiveEntry, fallbackDate: string) {
return {
...speechAdminEntryFromArchiveEntry(entry, fallbackDate),
body: entry.body ?? "",
};
}

View file

@ -32,6 +32,14 @@ export type SpeechArchiveItem = {
searchText: string;
};
export type SpeechSearchIndexItem = Omit<SpeechArchiveItem, "searchText">;
export type CompactSpeechSearchIndex = {
version: 2;
items: SpeechSearchIndexItem[];
index: Record<string, string>;
};
const entityMap: Record<string, string> = {
amp: "&",
apos: "'",
@ -215,6 +223,39 @@ export function getSpeechArchive(): SpeechArchiveItem[] {
});
}
function packDocumentIds(documentIds: number[]): string {
return documentIds.map((documentId) => documentId.toString(36)).join(",");
}
export function getCompactSpeechSearchIndex(): CompactSpeechSearchIndex {
const items = getSpeechArchive();
const index = new Map<string, number[]>();
items.forEach((item, documentId) => {
const terms = new Set(item.searchText.match(/[a-z0-9]+/g) ?? []);
terms.forEach((term) => {
if (term.length < 2) {
return;
}
const documentIds = index.get(term) ?? [];
documentIds.push(documentId);
index.set(term, documentIds);
});
});
return {
version: 2,
items: items.map(({ searchText, ...item }) => item),
index: Object.fromEntries(
Array.from(index)
.sort(([a], [b]) => a.localeCompare(b))
.map(([term, documentIds]) => [term, packDocumentIds(documentIds)]),
),
};
}
export function uniqueSorted(values: string[], direction: "asc" | "desc" = "asc"): string[] {
const unique = Array.from(new Set(values.filter(Boolean)));
return unique.sort((a, b) => {