// build.js — tiny static-site generator without the generator // Scans /posts/*.md, extracts front-matter & first paragraph, // writes posts/posts.json, rss.xml, and sitemap.xml const fs = require("fs"); const path = require("path"); const POSTS_DIR = path.join(__dirname, "posts"); const SITE_URL = "https://thefoldwithin.earth"; // change if needed function slugify(s) { return s .toLowerCase() .normalize("NFKD").replace(/[^\w\s-]/g, "") .trim().replace(/\s+/g, "-") .replace(/-+/g, "-"); } function parseFrontMatter(src) { // very small YAML-ish parser const fm = { title: "", date: "", excerpt: "", tags: [] }; const m = src.match(/^---\n([\s\S]*?)\n---\n?/); if (!m) return { fm, body: src }; const block = m[1]; for (const line of block.split("\n")) { const idx = line.indexOf(":"); if (idx === -1) continue; const key = line.slice(0, idx).trim(); let val = line.slice(idx + 1).trim(); if (key === "tags") { // tags: [a, b, c] OR tags: a, b, c if (/^\[.*\]$/.test(val)) { val = val.replace(/^\[|\]$/g, ""); } fm.tags = val.split(",").map(t => t.trim()).filter(Boolean); } else if (key in fm) { fm[key] = val; } } const body = src.slice(m[0].length); return { fm, body }; } function firstParagraph(text) { const body = text.replace(/\r/g, "").trim(); const noFM = body.replace(/^---[\s\S]*?---/, "").trim(); const para = noFM.split(/\n{2,}/).find(p => p.replace(/\s/g, "").length > 0) || ""; return para.replace(/\n/g, " ").trim(); } function toISODate(s, fallback) { // try to parse; if fail, fallback (usually file mtime) const d = s ? new Date(s) : null; if (d && !isNaN(d.getTime())) return d.toISOString(); return fallback.toISOString(); } function escapeXML(s) { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const files = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith(".md")); const posts = files.map(file => { const full = path.join(POSTS_DIR, file); const raw = fs.readFileSync(full, "utf8"); const stat = fs.statSync(full); const { fm, body } = parseFrontMatter(raw); const fallbackTitle = file.replace(/\.md$/i, "").replace(/-/g, " ").replace(/\s+/g, " ").trim(); const title = fm.title || fallbackTitle; const excerpt = fm.excerpt || (firstParagraph(raw).slice(0, 200) + (firstParagraph(raw).length > 200 ? "…" : "")); const slug = slugify(fm.title || fallbackTitle); const dateISO = toISODate(fm.date, stat.mtime); return { title, date: dateISO, // ISO for reliable sort excerpt, tags: fm.tags || [], slug, file // actual filename }; }); // newest first posts.sort((a, b) => (a.date < b.date ? 1 : -1)); // write JSON index fs.writeFileSync( path.join(POSTS_DIR, "posts.json"), JSON.stringify(posts, null, 2), "utf8" ); console.log(`✅ posts.json written (${posts.length} posts)`); // write RSS const rssItems = posts.map(p => { const url = `${SITE_URL}/#/post/${p.slug}`; return ` ${escapeXML(p.title)} ${url} ${url} ${new Date(p.date).toUTCString()} ${escapeXML(p.excerpt)} `; }).join(""); const rss = ` The Fold Within ${SITE_URL} Uncovering the recursive real. ${new Date().toUTCString()} ${rssItems} `; fs.writeFileSync(path.join(__dirname, "rss.xml"), rss, "utf8"); console.log("✅ rss.xml written"); // write sitemap const sitemapUrls = posts.map(p => ` ${SITE_URL}/#/post/${p.slug}`).join("\n"); const sitemap = ` ${SITE_URL} ${sitemapUrls} `; fs.writeFileSync(path.join(__dirname, "sitemap.xml"), sitemap, "utf8"); console.log("✅ sitemap.xml written");