diff --git a/src/components/ArchiveListPage.astro b/src/components/ArchiveListPage.astro new file mode 100644 index 00000000..6fbd71c4 --- /dev/null +++ b/src/components/ArchiveListPage.astro @@ -0,0 +1,121 @@ +--- +import SiteLayout from "./SiteLayout.astro"; +import SiteSidebar from "./SiteSidebar.astro"; +import { categoryHref, excerptFromBody, formatDate, slugify, tagHref } from "../lib/archive"; + +const { + title, + heading, + description, + pathname, + entries = [], + currentPage = 1, + totalPages = 1, + pageHref, +} = Astro.props; +--- + + +
+
+
+ +
+
+ + +
+ { + entries.map((entry) => { + const categories = entry.data.categories ?? []; + const tags = entry.data.tags ?? []; + const title = entry.data.title ?? "Untitled"; + const publishedDate = formatDate(entry.data.published); + + return ( +
`category-${slugify(category)}`)]}> +

+ {title} +

+ +
+

{excerptFromBody(entry.body)}

+
+ { + tags.length > 0 && ( +
+ +
+ ) + } +
+ ); + }) + } +
+ + { + totalPages > 1 && pageHref && ( +
+ +
+ ) + } +
+ + +
+ +
+
+
+
+ diff --git a/src/components/LegacyRedirectPage.astro b/src/components/LegacyRedirectPage.astro new file mode 100644 index 00000000..11348a20 --- /dev/null +++ b/src/components/LegacyRedirectPage.astro @@ -0,0 +1,19 @@ +--- +const { title, target } = Astro.props; +--- + + + + + + + + + {title} + + + +

This page has moved to {target}.

+ + + diff --git a/src/lib/archive.ts b/src/lib/archive.ts new file mode 100644 index 00000000..d9bc6eaa --- /dev/null +++ b/src/lib/archive.ts @@ -0,0 +1,134 @@ +import type { CollectionEntry } from "astro:content"; +import { getCollection } from "astro:content"; + +export type ArchiveEntry = CollectionEntry<"archive">; + +const entityMap: Record = { + 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(//gi, " ") + .replace(//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(/&/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 { + 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`; +} + diff --git a/src/lib/legacy-page.ts b/src/lib/legacy-page.ts new file mode 100644 index 00000000..bacd7efb --- /dev/null +++ b/src/lib/legacy-page.ts @@ -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(/
{ 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 { diff --git a/src/pages/2013-sunday-service-archive.astro b/src/pages/2013-sunday-service-archive.astro new file mode 100644 index 00000000..91a1d7a6 --- /dev/null +++ b/src/pages/2013-sunday-service-archive.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/2014-sunday-services.astro b/src/pages/2014-sunday-services.astro new file mode 100644 index 00000000..a6c8898b --- /dev/null +++ b/src/pages/2014-sunday-services.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/2015-sunday-services-archive.astro b/src/pages/2015-sunday-services-archive.astro new file mode 100644 index 00000000..295ff6cc --- /dev/null +++ b/src/pages/2015-sunday-services-archive.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/2016-sunday-service-archive.astro b/src/pages/2016-sunday-service-archive.astro new file mode 100644 index 00000000..440d5c9f --- /dev/null +++ b/src/pages/2016-sunday-service-archive.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/2017-sunday-service-archive.astro b/src/pages/2017-sunday-service-archive.astro new file mode 100644 index 00000000..9c53622d --- /dev/null +++ b/src/pages/2017-sunday-service-archive.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/2018-sunday-service-archive.astro b/src/pages/2018-sunday-service-archive.astro new file mode 100644 index 00000000..9c2a387e --- /dev/null +++ b/src/pages/2018-sunday-service-archive.astro @@ -0,0 +1,6 @@ +--- +import LegacyRedirectPage from "../components/LegacyRedirectPage.astro"; +--- + + + diff --git a/src/pages/about-us-organizations.astro b/src/pages/about-us-organizations.astro new file mode 100644 index 00000000..3106e711 --- /dev/null +++ b/src/pages/about-us-organizations.astro @@ -0,0 +1,18 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("about-us-organizations.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2013.astro b/src/pages/archive-sunday-services-2013.astro new file mode 100644 index 00000000..b0e0e6dc --- /dev/null +++ b/src/pages/archive-sunday-services-2013.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2013.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2014.astro b/src/pages/archive-sunday-services-2014.astro new file mode 100644 index 00000000..78de1e37 --- /dev/null +++ b/src/pages/archive-sunday-services-2014.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2014.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2015.astro b/src/pages/archive-sunday-services-2015.astro new file mode 100644 index 00000000..d2ae98a3 --- /dev/null +++ b/src/pages/archive-sunday-services-2015.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2015.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2016.astro b/src/pages/archive-sunday-services-2016.astro new file mode 100644 index 00000000..70cb25f7 --- /dev/null +++ b/src/pages/archive-sunday-services-2016.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2016.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2017.astro b/src/pages/archive-sunday-services-2017.astro new file mode 100644 index 00000000..11cb438d --- /dev/null +++ b/src/pages/archive-sunday-services-2017.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2017.html"); +--- + + + + + diff --git a/src/pages/archive-sunday-services-2018.astro b/src/pages/archive-sunday-services-2018.astro new file mode 100644 index 00000000..20f827f3 --- /dev/null +++ b/src/pages/archive-sunday-services-2018.astro @@ -0,0 +1,12 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("archive-sunday-services-2018.html"); +--- + + + + + diff --git a/src/pages/archive/sunday-services/[year]/index.astro b/src/pages/archive/sunday-services/[year]/index.astro new file mode 100644 index 00000000..401c6e9a --- /dev/null +++ b/src/pages/archive/sunday-services/[year]/index.astro @@ -0,0 +1,16 @@ +--- +import LegacyRedirectPage from "../../../../components/LegacyRedirectPage.astro"; + +export function getStaticPaths() { + const years = ["2013", "2014", "2015", "2016", "2017", "2018"]; + + return years.map((year) => ({ + params: { year }, + props: { year }, + })); +} + +const { year } = Astro.props; +--- + + diff --git a/src/pages/blog/categories/[category].astro b/src/pages/blog/categories/[category].astro new file mode 100644 index 00000000..21f64e4c --- /dev/null +++ b/src/pages/blog/categories/[category].astro @@ -0,0 +1,28 @@ +--- +import ArchiveListPage from "../../../components/ArchiveListPage.astro"; +import { blogCategorySlug, getArchiveEntries, uniqueSorted } from "../../../lib/archive"; + +export async function getStaticPaths() { + const entries = await getArchiveEntries("blog"); + const categories = uniqueSorted(entries.flatMap((entry) => entry.data.categories ?? [])); + + return categories.map((category) => ({ + params: { category: blogCategorySlug(category) }, + props: { + category, + entries: entries.filter((entry) => (entry.data.categories ?? []).includes(category)), + }, + })); +} + +const { category, entries } = Astro.props; +--- + +${category}`} + description={`Browse FFWPU Ireland blog posts in the ${category} category.`} + pathname={`/blog/categories/${blogCategorySlug(category)}.html`} + entries={entries} +/> + diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro new file mode 100644 index 00000000..927336c0 --- /dev/null +++ b/src/pages/blog/index.astro @@ -0,0 +1,14 @@ +--- +import ArchiveListPage from "../../components/ArchiveListPage.astro"; +import { getArchiveEntries } from "../../lib/archive"; + +const entries = await getArchiveEntries("blog"); +--- + + diff --git a/src/pages/blog/tags/[tag].astro b/src/pages/blog/tags/[tag].astro new file mode 100644 index 00000000..e845b24c --- /dev/null +++ b/src/pages/blog/tags/[tag].astro @@ -0,0 +1,28 @@ +--- +import ArchiveListPage from "../../../components/ArchiveListPage.astro"; +import { blogTagSlug, getArchiveEntries, uniqueSorted } from "../../../lib/archive"; + +export async function getStaticPaths() { + const entries = await getArchiveEntries("blog"); + const tags = uniqueSorted(entries.flatMap((entry) => entry.data.tags ?? [])); + + return tags.map((tag) => ({ + params: { tag: blogTagSlug(tag) }, + props: { + tag, + entries: entries.filter((entry) => (entry.data.tags ?? []).includes(tag)), + }, + })); +} + +const { tag, entries } = Astro.props; +--- + +${tag}`} + description={`Browse FFWPU Ireland blog posts tagged ${tag}.`} + pathname={`/blog/tags/${blogTagSlug(tag)}.html`} + entries={entries} +/> + diff --git a/src/pages/register.astro b/src/pages/register.astro new file mode 100644 index 00000000..b9d92923 --- /dev/null +++ b/src/pages/register.astro @@ -0,0 +1,20 @@ +--- +import SiteLayout from "../components/SiteLayout.astro"; +import TwoColumnPage from "../components/TwoColumnPage.astro"; +import { legacyPageArticle } from "../lib/legacy-page"; + +const articleHtml = legacyPageArticle("register.html"); +--- + + + + + + + diff --git a/src/pages/speeches/categories/[category].astro b/src/pages/speeches/categories/[category].astro new file mode 100644 index 00000000..da86ebff --- /dev/null +++ b/src/pages/speeches/categories/[category].astro @@ -0,0 +1,53 @@ +--- +import ArchiveListPage from "../../../components/ArchiveListPage.astro"; +import { getArchiveEntries, speechCategorySlug, uniqueSorted } from "../../../lib/archive"; + +function pageSlug(baseSlug, page) { + return page === 1 ? baseSlug : `${baseSlug}-page-${page}`; +} + +function pageHref(baseSlug) { + return (page) => `/speeches/categories/${pageSlug(baseSlug, page)}.html`; +} + +export async function getStaticPaths() { + const pageSize = 10; + const entries = await getArchiveEntries("speech"); + const categories = uniqueSorted(entries.flatMap((entry) => entry.data.categories ?? [])); + + return categories.flatMap((category) => { + const baseSlug = speechCategorySlug(category); + const categoryEntries = entries.filter((entry) => (entry.data.categories ?? []).includes(category)); + const totalPages = Math.max(1, Math.ceil(categoryEntries.length / pageSize)); + + return Array.from({ length: totalPages }, (_, index) => { + const currentPage = index + 1; + const currentSlug = currentPage === 1 ? baseSlug : `${baseSlug}-page-${currentPage}`; + return { + params: { category: currentSlug }, + props: { + category, + baseSlug, + currentPage, + totalPages, + entries: categoryEntries.slice(index * pageSize, index * pageSize + pageSize), + }, + }; + }); + }); +} + +const { category, baseSlug, currentPage, entries, totalPages } = Astro.props; +const title = `Category: ${category}`; +--- + +${category}`} + description={`Browse FFWPU Ireland speeches in ${category}.`} + pathname={`/speeches/categories/${pageSlug(baseSlug, currentPage)}.html`} + entries={entries} + currentPage={currentPage} + totalPages={totalPages} + pageHref={pageHref(baseSlug)} +/> diff --git a/src/pages/speeches/rev-dr-sun-myung-moon/[year].astro b/src/pages/speeches/rev-dr-sun-myung-moon/[year].astro new file mode 100644 index 00000000..17ee3eb0 --- /dev/null +++ b/src/pages/speeches/rev-dr-sun-myung-moon/[year].astro @@ -0,0 +1,56 @@ +--- +import SiteLayout from "../../../components/SiteLayout.astro"; +import { excerptFromBody, getArchiveEntries, uniqueSorted } from "../../../lib/archive"; + +export async function getStaticPaths() { + const entries = await getArchiveEntries("speech"); + const years = uniqueSorted(entries.map((entry) => entry.data.speechYear ?? "").filter(Boolean)); + + return years.map((year) => ({ + params: { year }, + props: { + year, + entries: entries.filter((entry) => entry.data.speechYear === year), + }, + })); +} + +const { year, entries } = Astro.props; +--- + + +
+
+
+ +
+
+
+

Speeches by Rev. Dr. Sun Myung Moon {year}

+
+ +
+
+
+
+ +
+
+
+
+