diff --git a/tools/generate-index.mjs b/tools/generate-index.mjs
index 46af349..8196f5f 100755
--- a/tools/generate-index.mjs
+++ b/tools/generate-index.mjs
@@ -1,105 +1,67 @@
-import { promises as fs } from "fs";
+#!/usr/bin/env node
+import fs from "fs/promises";
import path from "path";
-import { fileURLToPath } from "url";
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const PUBLIC = path.resolve(__dirname, "../public");
-const ROOTS = ["pinned","posts"];
-const ALLOWED = new Set([".md",".html"]);
-const MAX_BYTES = 64 * 1024; // read head for title parse
-
-// --- helpers ------------------------------------------------------
-async function statSafe(p){ try{ return await fs.stat(p); }catch{ return null; } }
-
-function posixJoin(...xs){ return path.posix.join(...xs); }
+const ROOT = "public";
+const OUT = path.join(ROOT, "index.json");
+const STATIC_TOPLEVEL = new Set(["about","contact","legal"]);
+const MAX_BYTES = 64 * 1024;
function dateFromName(name){
- const m = name.match(/^(\d{4}-\d{2}-\d{2})/);
- if (!m) return null;
- const d = new Date(m[1]); const t = d.getTime();
- return Number.isFinite(t) ? t : null;
+ const m=name.match(/^(\d{4}-\d{2}-\d{2})/);
+ return m?new Date(m[1]).getTime():null;
}
-
async function readHead(abs){
- const fh = await fs.open(abs,"r");
- const buf = Buffer.alloc(MAX_BYTES);
- const { bytesRead } = await fh.read(buf, 0, MAX_BYTES, 0);
+ const fh=await fs.open(abs,"r");
+ const buf=Buffer.alloc(MAX_BYTES);
+ const {bytesRead}=await fh.read(buf,0,MAX_BYTES,0);
await fh.close();
- return buf.slice(0, bytesRead).toString("utf8");
+ return buf.slice(0,bytesRead).toString("utf8");
}
-
-function parseTitle(raw, ext){
- if (ext===".md"){
- const m = raw.match(/^\s*#\s+(.+?)\s*$/m);
- if (m) return m[1].trim();
- } else if (ext===".html"){
- const m = raw.match(/
]*>([^<]+)<\/title>/i);
- if (m) return m[1].trim();
- }
+function parseTitle(raw,ext){
+ if(ext===".md") return raw.match(/^\s*#\s+(.+?)\s*$/m)?.[1].trim();
+ if(ext===".html") return raw.match(/]*>([^<]+)<\/title>/i)?.[1].trim();
return null;
}
-// --- walker -------------------------------------------------------
-async function walk(absDir, relBase){
- const out = [];
- const ents = await fs.readdir(absDir, { withFileTypes: true });
- for (const e of ents){
- if (e.name.startsWith(".")) continue;
- const abs = path.join(absDir, e.name);
- const rel = posixJoin(relBase, e.name);
- const st = await fs.stat(abs);
-
- if (e.isDirectory()){
- const children = await walk(abs, rel);
- out.push({ type:"dir", name:e.name, path:rel, children });
- } else {
- const ext = path.extname(e.name);
- if (!ALLOWED.has(ext)) continue;
-
- const raw = await readHead(abs);
- const title = parseTitle(raw, ext) || e.name;
- const dated = dateFromName(e.name);
- out.push({
- type:"file",
- name:e.name,
- title,
- path:rel,
- ext,
- pinned: relBase.startsWith("pinned"),
- mtime: dated ?? st.mtimeMs
- });
- }
- }
- return out;
-}
-
-function flatten(node, list){
- if (node.type==="file") list.push(node);
- else node.children.forEach(c=>flatten(c,list));
-}
-
-// --- build --------------------------------------------------------
-async function build(){
- const tree = [];
- const flat = [];
-
- for (const root of ROOTS){
- const abs = path.join(PUBLIC, root);
- const st = await statSafe(abs);
- if (!st?.isDirectory()){
- console.warn(`Warning: skipping missing ${root}/`);
+async function walk(relBase=""){
+ const abs=path.join(ROOT,relBase);
+ const entries=await fs.readdir(abs,{withFileTypes:true});
+ const dir={type:"dir",name:path.basename(relBase)||"",path:relBase,children:[]};
+ for(const e of entries){
+ if(e.name.startsWith(".")) continue;
+ const rel=path.posix.join(relBase,e.name);
+ const absPath=path.join(ROOT,rel);
+ if(e.isDirectory()){
+ const top=rel.split("/")[0];
+ if(STATIC_TOPLEVEL.has(top)){continue;}
+ const child=await walk(rel);
+ dir.children.push(child);
continue;
}
- const children = await walk(abs, root);
- const dirNode = { type:"dir", name:root, path:root, children };
- tree.push(dirNode);
- flatten(dirNode, flat);
+ const ext=path.extname(e.name);
+ if(![".md",".html"].includes(ext)) continue;
+ const st=await fs.stat(absPath);
+ const raw=await readHead(absPath);
+ const title=parseTitle(raw,ext)||e.name;
+ const date=dateFromName(e.name);
+ dir.children.push({
+ type:"file",
+ name:e.name,
+ title,
+ path:rel,
+ ext,
+ pinned:rel.startsWith("pinned/"),
+ mtime:date||st.mtimeMs
+ });
}
-
- const data = { generatedAt: Date.now(), tree, flat };
- const outPath = path.join(PUBLIC, "index.json");
- await fs.writeFile(outPath, JSON.stringify(data, null, 2));
- console.log(`✅ index.json generated (${flat.length} files)`);
+ return dir;
}
-await build();
\ No newline at end of file
+(async()=>{
+ const tree=await walk();
+ const flat=[];
+ (function flatten(n){for(const c of n.children){if(c.type==="file")flat.push(c);else flatten(c);}})(tree);
+ const sections=[...new Set(flat.map(f=>f.path.split("/")[0]))];
+ await fs.writeFile(OUT,JSON.stringify({tree:tree.children,flat,sections},null,2));
+ console.log("index.json built:",OUT);
+})();
\ No newline at end of file