From ddf3b5a2f8d3dceda36459b06a7a69504900326e Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Thu, 26 Feb 2026 13:52:15 -0800 Subject: [PATCH] Feat: Generate home and article pages from markdown --- scripts/generate-pages.mjs | 216 +++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 scripts/generate-pages.mjs diff --git a/scripts/generate-pages.mjs b/scripts/generate-pages.mjs new file mode 100644 index 0000000..78a0af0 --- /dev/null +++ b/scripts/generate-pages.mjs @@ -0,0 +1,216 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import fg from "fast-glob"; +import matter from "gray-matter"; +import { marked } from "marked"; + +const ROOT = process.cwd(); +const SRC = path.join(ROOT, "src"); +const ARTICLES_GLOB = "articles/**/index.md"; + +marked.setOptions({ breaks: true, gfm: true }); + +const logo = ` + + + + + + + + + + + + + + +`; + +const shellHead = ({ title, desc, image }) => ` + + + + + ${title} + + + + + ${image ? `` : ""} + + + + +`; + +const nav = ` +
+ +
+`; + +const footer = ` + + +`; + +const fmtDate = (date) => + new Date(date).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); + +const escapeHtml = (s = "") => + s.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch])); + +const fixInternalLinks = (html) => + html + .replace(/href="\.\//g, 'href="/') + .replace(/href="([^"]+)"(?=[^>]*>)/g, (_, href) => + href.startsWith("http") || href.startsWith("#") || href.endsWith("/") || href.endsWith(".md") + ? `href="${href}"` + : `href="${href}/"` + ); + +const renderHome = (articles) => ` +${shellHead({ + title: "apophenia.news — The news outlet for pattern seekers", + desc: "Signals, anomalies, civilization trajectories, and deep pattern analysis.", + image: "https://direct-img.link/constellation+data+points+minimal+white+background" +})} +${nav} +
+
+

Pattern Intelligence Journalism

+

The news outlet for pattern seekers

+

High-agency analysis at the intersection of AGI, consciousness, geopolitics, and first-contact logic.

+
+ +
+
+

Latest Articles

+ ${articles.length} published +
+ +
+ ${articles + .map( + (a) => ` + ` + ) + .join("")} +
+
+
+${footer} +`; + +const renderArticle = (article) => ` +${shellHead({ title: `${article.title} — apophenia.news`, desc: article.description, image: article.header_image })} +${nav} +
+
+ ${escapeHtml(article.title)} +
+

${fmtDate(article.date)} • ${escapeHtml(article.author || "Apophenia")}

+

${escapeHtml(article.title)}

+

${escapeHtml(article.description || "")}

+
+ ${(article.tags || []).map((t) => `#${escapeHtml(t)}`).join("")} +
+
+
${article.html}
+
+
+ + +
+${footer} +`; + +const ensureCleanGenerated = async () => { + await fs.mkdir(SRC, { recursive: true }); + const children = await fs.readdir(SRC, { withFileTypes: true }); + const keep = new Set(["assets"]); + await Promise.all( + children + .filter((d) => d.isDirectory() && !keep.has(d.name)) + .map((d) => fs.rm(path.join(SRC, d.name), { recursive: true, force: true })) + ); +}; + +const run = async () => { + const files = await fg(ARTICLES_GLOB, { cwd: ROOT, absolute: true }); + const articles = []; + + for (const file of files) { + const md = await fs.readFile(file, "utf8"); + const { data, content } = matter(md); + const html = fixInternalLinks(marked.parse(content)); + if (!data.slug) continue; + + articles.push({ + ...data, + html + }); + } + + articles.sort((a, b) => +new Date(b.date) - +new Date(a.date)); + + await ensureCleanGenerated(); + await fs.writeFile(path.join(SRC, "index.html"), renderHome(articles), "utf8"); + + for (const article of articles) { + const dir = path.join(SRC, article.slug); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(path.join(dir, "index.html"), renderArticle(article), "utf8"); + } + + console.log(`Generated ${articles.length} article pages + home.`); +}; + +run().catch((err) => { + console.error(err); + process.exit(1); +});