thefoldwithin-earth/.old/build.mjs
Mark Randall Havens 7f86647175 fresh start
2025-10-19 16:48:12 -05:00

184 lines
7.4 KiB
JavaScript
Executable file

// build.mjs — The Fold Within Earth
// Markdown-native static site builder with no external generator.
// ────────────────────────────────────────────────────────────────
// Imports
// ────────────────────────────────────────────────────────────────
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import yaml from "js-yaml";
import { marked } from "marked";
// ────────────────────────────────────────────────────────────────
// Setup
// ────────────────────────────────────────────────────────────────
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, "config.json");
const CONTENT_DIR = path.join(__dirname, "content");
const PUBLIC_DIR = path.join(__dirname, "public");
// ────────────────────────────────────────────────────────────────
// Config loader
// ────────────────────────────────────────────────────────────────
let config = {};
try {
const cfgText = await fs.readFile(CONFIG_PATH, "utf8");
config = JSON.parse(cfgText);
console.log("Loaded config.json");
} catch {
console.warn("config.json not found or invalid; using defaults.");
config = {
siteTitle: "The Fold Within Earth",
baseUrl: "",
};
}
// ────────────────────────────────────────────────────────────────
// Utilities
// ────────────────────────────────────────────────────────────────
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const res = path.resolve(dir, entry.name);
return entry.isDirectory() ? walk(res) : res;
})
);
return Array.prototype.concat(...files);
}
function parseFrontMatter(text) {
if (!text.startsWith("---")) return { meta: {}, body: text };
const end = text.indexOf("---", 3);
if (end === -1) return { meta: {}, body: text };
const yamlText = text.slice(3, end).trim();
const body = text.slice(end + 3).trim();
let meta = {};
try {
meta = yaml.load(yamlText) || {};
} catch (err) {
console.error("YAML parse error:", err);
}
return { meta, body };
}
// ────────────────────────────────────────────────────────────────
// Build Posts
// ────────────────────────────────────────────────────────────────
async function buildPosts() {
const mdFiles = (await walk(CONTENT_DIR)).filter((f) => f.endsWith(".md"));
console.log(`Found ${mdFiles.length} markdown files.`);
const postsPromises = mdFiles.map(async (file) => {
try {
const text = await fs.readFile(file, "utf8");
const { meta, body } = parseFrontMatter(text);
if (meta.status === "draft") return null;
const html = marked.parse(body);
const slug =
meta.slug ||
path.basename(file, ".md").replace(/\s+/g, "-").toLowerCase();
const relPath = path.relative(CONTENT_DIR, file);
const section = relPath.split(path.sep)[0] || "general";
return {
title: meta.title || slug,
date: meta.date || new Date().toISOString().slice(0, 10),
excerpt: meta.excerpt || body.slice(0, 200),
tags: meta.tags || [],
section,
slug,
cover: meta.cover || "",
html,
};
} catch (err) {
console.error(`Error reading ${file}:`, err);
return null;
}
});
const postObjects = await Promise.all(postsPromises);
const posts = postObjects.filter(Boolean);
posts.sort((a, b) => b.date.localeCompare(a.date));
console.log(`Processed ${posts.length} posts.`);
return posts;
}
// ────────────────────────────────────────────────────────────────
// Copy Helpers
// ────────────────────────────────────────────────────────────────
async function copyDir(src, dest) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) await copyDir(srcPath, destPath);
else await fs.copyFile(srcPath, destPath);
}
}
// ────────────────────────────────────────────────────────────────
// Main Build
// ────────────────────────────────────────────────────────────────
async function main() {
const posts = await buildPosts();
// Reset public dir
await fs.rm(PUBLIC_DIR, { recursive: true, force: true });
await fs.mkdir(PUBLIC_DIR, { recursive: true });
// Copy static assets
const staticFiles = [
"index.html",
"styles.css",
"app.js",
"render.js",
"sanitize.js",
"util.js",
"mud.js",
];
for (const f of staticFiles) {
try {
await fs.copyFile(path.join(__dirname, f), path.join(PUBLIC_DIR, f));
} catch {
console.warn(`⚠️ Skipped missing static file: ${f}`);
}
}
// Copy content for direct linking
await copyDir(CONTENT_DIR, path.join(PUBLIC_DIR, "content"));
// Write index.json
await fs.writeFile(
path.join(PUBLIC_DIR, "index.json"),
JSON.stringify(posts, null, 2),
"utf8"
);
// Write search.json
const searchData = posts.map((p) => ({
title: p.title,
excerpt: p.excerpt,
tags: p.tags.join(" "),
section: p.section,
slug: p.slug,
}));
await fs.writeFile(
path.join(PUBLIC_DIR, "search.json"),
JSON.stringify(searchData, null, 2),
"utf8"
);
console.log("✅ Build complete. Files written to /public.");
}
// ────────────────────────────────────────────────────────────────
await main().catch((err) => {
console.error("Build failed:", err);
process.exit(1);
});