Migrate legacy archive pages

This commit is contained in:
Workshop Bot 2026-06-11 18:31:35 +00:00
parent e90920ce86
commit cbedd9b723
25 changed files with 652 additions and 1 deletions

134
src/lib/archive.ts Normal file
View file

@ -0,0 +1,134 @@
import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
export type ArchiveEntry = CollectionEntry<"archive">;
const entityMap: Record<string, string> = {
amp: "&",
apos: "'",
hellip: "...",
laquo: "<<",
nbsp: " ",
quot: '"',
raquo: ">>",
rsquo: "'",
lsquo: "'",
rdquo: '"',
ldquo: '"',
};
export function decodeEntities(value = ""): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
const key = entity.toLowerCase();
if (key.startsWith("#x")) {
return String.fromCodePoint(Number.parseInt(key.slice(2), 16));
}
if (key.startsWith("#")) {
return String.fromCodePoint(Number.parseInt(key.slice(1), 10));
}
return entityMap[key] ?? match;
});
}
export function textFromHtml(html = ""): string {
return decodeEntities(
html
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " "),
)
.replace(/\s+/g, " ")
.trim();
}
export function excerptFromBody(body = "", maxLength = 230): string {
const text = textFromHtml(body);
if (text.length <= maxLength) {
return text;
}
const clipped = text.slice(0, maxLength - 3);
const finalSpace = clipped.lastIndexOf(" ");
return `${finalSpace > 80 ? clipped.slice(0, finalSpace) : clipped}...`;
}
export function slugify(value: string): string {
return value
.toLowerCase()
.replace(/&amp;/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
export function formatDate(value?: string): string | undefined {
if (!value) {
return undefined;
}
const date = new Date(value);
if (Number.isNaN(date.valueOf())) {
return undefined;
}
return new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
}).format(date);
}
export function sortByPublishedDesc(entries: ArchiveEntry[]): ArchiveEntry[] {
return [...entries].sort((a, b) => {
const dateA = Date.parse(a.data.published ?? "");
const dateB = Date.parse(b.data.published ?? "");
if (!Number.isNaN(dateA) && !Number.isNaN(dateB) && dateA !== dateB) {
return dateB - dateA;
}
return (a.data.title ?? a.id).localeCompare(b.data.title ?? b.id);
});
}
export async function getArchiveEntries(type?: "blog" | "speech"): Promise<ArchiveEntry[]> {
const entries = await getCollection("archive");
return sortByPublishedDesc(type ? entries.filter((entry) => entry.data.type === type) : entries);
}
export function uniqueSorted(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b));
}
export function blogCategorySlug(category: string): string {
return slugify(category);
}
export function blogTagSlug(tag: string): string {
return slugify(tag);
}
export function speechCategorySlug(category: string): string {
if (category.toLowerCase().includes("hak ja han moon")) {
return "mrs-hak-ja-han-moon";
}
const year = category.match(/\b(1\d{3}|20\d{2})\b/)?.[1];
if (year === "1956") {
return "rev-sun-myung-moon";
}
return year ? `rev-sun-myung-moon-${year}` : "rev-sun-myung-moon";
}
export function categoryHref(entry: ArchiveEntry, category: string): string {
if (entry.data.type === "speech") {
return `/speeches/categories/${speechCategorySlug(category)}.html`;
}
return `/blog/categories/${blogCategorySlug(category)}.html`;
}
export function tagHref(tag: string): string {
return `/blog/tags/${blogTagSlug(tag)}.html`;
}

15
src/lib/legacy-page.ts Normal file
View file

@ -0,0 +1,15 @@
import fs from "node:fs";
import path from "node:path";
import { legacyHtmlRoot } from "./static-pages";
export function legacyPageArticle(relativePath: string): string {
const html = fs.readFileSync(path.join(legacyHtmlRoot, relativePath), "utf8");
const match = html.match(/<div id="post-[\s\S]*?<\/div><!-- #post-## -->/);
if (!match) {
throw new Error(`Could not find page article in ${relativePath}`);
}
return match[0];
}

View file

@ -4,16 +4,37 @@ import path from "node:path";
export const repoRoot = process.cwd();
export const legacyHtmlRoot = path.join(repoRoot, "archive-source");
export const migratedHtmlPages = new Set([
"2013-sunday-service-archive.html",
"2014-sunday-services.html",
"2015-sunday-services-archive.html",
"2016-sunday-service-archive.html",
"2017-sunday-service-archive.html",
"2018-sunday-service-archive.html",
"about.html",
"about-us-organizations.html",
"archive-sunday-services-2013.html",
"archive-sunday-services-2014.html",
"archive-sunday-services-2015.html",
"archive-sunday-services-2016.html",
"archive-sunday-services-2017.html",
"archive-sunday-services-2018.html",
"contact.html",
"events.html",
"index.html",
"register.html",
"services.html",
"speeches/index.html",
"the-founders.html",
"videos.html",
]);
const migratedHtmlPrefixes = [
"blog/categories/",
"blog/tags/",
"speeches/categories/",
"speeches/rev-dr-sun-myung-moon/",
];
export function walkHtmlFiles(dir = legacyHtmlRoot): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const fullPath = path.join(dir, entry.name);
@ -37,7 +58,7 @@ export function routeFromRelativePath(relativePath: string): string {
}
export function isMigratedHtmlPage(relativePath: string): boolean {
return migratedHtmlPages.has(relativePath);
return migratedHtmlPages.has(relativePath) || migratedHtmlPrefixes.some((prefix) => relativePath.startsWith(prefix));
}
export function sourcePathFromRoute(route = ""): string {