Update build.js

This commit is contained in:
Mark Randall Havens △ The Empathic Technologist ⟁ Doctor Who 42 2025-10-16 19:44:42 -05:00 committed by GitHub
parent 2c6c783a26
commit 9b6154fbda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

115
build.js
View file

@ -1,60 +1,59 @@
// build.js — tiny static-site generator without the generator // build.js — auto-index Markdown posts for The Fold Within
// Scans /posts/*.md, extracts front-matter & first paragraph, // Parses front-matter, removes it from body, and generates clean summaries.
// writes posts/posts.json, rss.xml, and sitemap.xml
const fs = require("fs"); import fs from "fs";
const path = require("path"); import path from "path";
const POSTS_DIR = path.join(__dirname, "posts"); const POSTS_DIR = path.join(".", "posts");
const SITE_URL = "https://thefoldwithin.earth"; // change if needed const SITE_URL = "https://thefoldwithin.earth"; // Update if needed
function slugify(s) { function slugify(s) {
return s return s
.toLowerCase() .toLowerCase()
.normalize("NFKD").replace(/[^\w\s-]/g, "") .normalize("NFKD")
.trim().replace(/\s+/g, "-") .replace(/[^\w\s-]/g, "")
.trim()
.replace(/\s+/g, "-")
.replace(/-+/g, "-"); .replace(/-+/g, "-");
} }
// --- Extract YAML-style front matter ---
function parseFrontMatter(src) { function parseFrontMatter(src) {
// very small YAML-ish parser
const fm = { title: "", date: "", excerpt: "", tags: [] }; const fm = { title: "", date: "", excerpt: "", tags: [] };
const m = src.match(/^---\n([\s\S]*?)\n---\n?/); const match = src.match(/^---\n([\s\S]*?)\n---\n?/);
if (!m) return { fm, body: src }; if (!match) return { fm, body: src };
const block = m[1]; const block = match[1];
for (const line of block.split("\n")) { for (const line of block.split("\n")) {
const idx = line.indexOf(":"); const [key, ...rest] = line.split(":");
if (idx === -1) continue; const value = rest.join(":").trim();
const key = line.slice(0, idx).trim(); if (key.trim() === "title") fm.title = value;
let val = line.slice(idx + 1).trim(); if (key.trim() === "date") fm.date = value;
if (key.trim() === "excerpt") fm.excerpt = value;
if (key === "tags") { if (key.trim() === "tags") {
// tags: [a, b, c] OR tags: a, b, c fm.tags = value
if (/^\[.*\]$/.test(val)) { .replace(/[\[\]]/g, "")
val = val.replace(/^\[|\]$/g, ""); .split(",")
} .map((v) => v.trim())
fm.tags = val.split(",").map(t => t.trim()).filter(Boolean); .filter(Boolean);
} else if (key in fm) {
fm[key] = val;
} }
} }
const body = src.slice(m[0].length); const body = src.slice(match[0].length).trim();
return { fm, body }; return { fm, body };
} }
function firstParagraph(text) { function firstParagraph(text) {
const body = text.replace(/\r/g, "").trim(); const para = text
const noFM = body.replace(/^---[\s\S]*?---/, "").trim(); .replace(/\r/g, "")
const para = noFM.split(/\n{2,}/).find(p => p.replace(/\s/g, "").length > 0) || ""; .split(/\n{2,}/)
return para.replace(/\n/g, " ").trim(); .find((p) => p.replace(/\s/g, "").length > 0);
return para ? para.replace(/\n/g, " ").trim() : "";
} }
function toISODate(s, fallback) { function toISODate(s, fallback) {
// try to parse; if fail, fallback (usually file mtime)
const d = s ? new Date(s) : null; const d = s ? new Date(s) : null;
if (d && !isNaN(d.getTime())) return d.toISOString(); if (d && !isNaN(d.getTime())) return d;
return fallback.toISOString(); return fallback;
} }
function escapeXML(s) { function escapeXML(s) {
@ -66,43 +65,48 @@ function escapeXML(s) {
.replace(/'/g, "'"); .replace(/'/g, "'");
} }
const files = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith(".md")); const files = fs
.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith(".md") && !f.startsWith("_"));
const posts = files.map(file => { const posts = files.map((file) => {
const full = path.join(POSTS_DIR, file); const raw = fs.readFileSync(path.join(POSTS_DIR, file), "utf8");
const raw = fs.readFileSync(full, "utf8"); const stat = fs.statSync(path.join(POSTS_DIR, file));
const stat = fs.statSync(full);
const { fm, body } = parseFrontMatter(raw); const { fm, body } = parseFrontMatter(raw);
const fallbackTitle = file.replace(/\.md$/i, "").replace(/-/g, " ").replace(/\s+/g, " ").trim(); const fallbackTitle = file.replace(/\.md$/, "").replace(/-/g, " ");
const title = fm.title || fallbackTitle; const title = fm.title || fallbackTitle;
const excerpt = fm.excerpt || (firstParagraph(raw).slice(0, 200) + (firstParagraph(raw).length > 200 ? "…" : "")); const slug = slugify(title);
const slug = slugify(fm.title || fallbackTitle); const excerpt =
fm.excerpt ||
(firstParagraph(body).slice(0, 200) +
(firstParagraph(body).length > 200 ? "…" : ""));
const dateISO = toISODate(fm.date, stat.mtime); const dateISO = toISODate(fm.date, stat.mtime);
return { return {
title, title,
date: dateISO, // ISO for reliable sort date: dateISO.toISOString().split("T")[0], // human-readable YYYY-MM-DD
excerpt, excerpt,
tags: fm.tags || [], tags: fm.tags || [],
slug, slug,
file // actual filename file,
}; };
}); });
// newest first // newest first
posts.sort((a, b) => (a.date < b.date ? 1 : -1)); posts.sort((a, b) => (a.date < b.date ? 1 : -1));
// write JSON index // write posts.json
fs.writeFileSync( fs.writeFileSync(
path.join(POSTS_DIR, "posts.json"), path.join(POSTS_DIR, "posts.json"),
JSON.stringify(posts, null, 2), JSON.stringify(posts, null, 2),
"utf8" "utf8"
); );
console.log(` posts.json written (${posts.length} posts)`); console.log(` Generated posts.json (${posts.length} posts)`);
// write RSS // write rss.xml
const rssItems = posts.map(p => { const rssItems = posts
.map((p) => {
const url = `${SITE_URL}/#/post/${p.slug}`; const url = `${SITE_URL}/#/post/${p.slug}`;
return ` return `
<item> <item>
@ -112,28 +116,31 @@ const rssItems = posts.map(p => {
<pubDate>${new Date(p.date).toUTCString()}</pubDate> <pubDate>${new Date(p.date).toUTCString()}</pubDate>
<description>${escapeXML(p.excerpt)}</description> <description>${escapeXML(p.excerpt)}</description>
</item>`; </item>`;
}).join(""); })
.join("");
const rss = `<?xml version="1.0" encoding="UTF-8"?> const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"> <rss version="2.0">
<channel> <channel>
<title>The Fold Within</title> <title>The Fold Within</title>
<link>${SITE_URL}</link> <link>${SITE_URL}</link>
<description>Uncovering the recursive real.</description> <description>Uncovering the Recursive Real.</description>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate> <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${rssItems} ${rssItems}
</channel> </channel>
</rss>`; </rss>`;
fs.writeFileSync(path.join(__dirname, "rss.xml"), rss, "utf8"); fs.writeFileSync("rss.xml", rss, "utf8");
console.log("✅ rss.xml written"); console.log("✅ rss.xml written");
// write sitemap // write sitemap.xml
const sitemapUrls = posts.map(p => ` <url><loc>${SITE_URL}/#/post/${p.slug}</loc></url>`).join("\n"); const sitemapUrls = posts
.map((p) => ` <url><loc>${SITE_URL}/#/post/${p.slug}</loc></url>`)
.join("\n");
const sitemap = `<?xml version="1.0" encoding="UTF-8"?> const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>${SITE_URL}</loc></url> <url><loc>${SITE_URL}</loc></url>
${sitemapUrls} ${sitemapUrls}
</urlset>`; </urlset>`;
fs.writeFileSync(path.join(__dirname, "sitemap.xml"), sitemap, "utf8"); fs.writeFileSync("sitemap.xml", sitemap, "utf8");
console.log("✅ sitemap.xml written"); console.log("✅ sitemap.xml written");