// Reads the staged post files in content/blog. Node-only: the seed and the // content check use it, the pages never do. import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { postInputSchema, type PostInput } from "./schema"; export const CONTENT_DIR = join(process.cwd(), "content", "blog"); // Front matter is strict on purpose: `key: value` per line, where a value is a // JSON string or array, true/false, or a bare word function parseFrontMatter( raw: string, file: string, ): { data: Record; body: string } { const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); if (!match) throw new Error(`${file}: missing front matter`); const data: Record = {}; for (const line of match[1].split("\n")) { if (!line.trim()) continue; const m = line.match(/^([A-Za-z]+):\s*(.*)$/); if (!m) throw new Error(`${file}: bad front matter line "${line}"`); const value = m[2].trim(); if (value === "true" || value === "false") data[m[1]] = value === "true"; else if (value.startsWith("[") || value.startsWith('"')) { try { data[m[1]] = JSON.parse(value); } catch { throw new Error(`${file}: ${m[1]} is not valid JSON: ${value}`); } } else if (value) data[m[1]] = value; } return { data, body: match[2].trim() }; } export function readPostFile(slug: string): PostInput { const file = join(CONTENT_DIR, `${slug}.md`); const { data, body } = parseFrontMatter(readFileSync(file, "utf8"), file); const result = postInputSchema.safeParse({ ...data, slug, body }); if (!result.success) { const issues = result.error.issues .map((i) => `${i.path.join(".") || "post"}: ${i.message}`) .join("; "); throw new Error(`${slug}.md: ${issues}`); } return result.data; } export function listPostFiles(): string[] { try { return readdirSync(CONTENT_DIR) .filter((f) => f.endsWith(".md") && f !== "README.md") .map((f) => f.slice(0, -3)) .sort(); } catch { return []; } } /** Rows of docs/blog-keywords.md: keyword and slug, for the check's subset mode. */ export function readKeywordMap(): { keyword: string; slug: string; category: string; status: string; }[] { let raw: string; try { raw = readFileSync(join(process.cwd(), "docs", "blog-keywords.md"), "utf8"); } catch { return []; } const lines = raw.split("\n").filter((l) => l.startsWith("|")); const header = lines[0]?.split("|").map((c) => c.trim().toLowerCase()); if (!header) return []; const col = (name: string) => header.indexOf(name); const [k, s, c, st] = [ col("keyword"), col("slug"), col("category"), col("status"), ]; return lines .slice(2) .map((l) => l.split("|").map((cell) => cell.trim())) .filter((cells) => cells[k] && cells[s] && !/^-+$/.test(cells[k])) .map((cells) => ({ keyword: cells[k], slug: cells[s].replace(/`/g, ""), category: cells[c], status: cells[st] ?? "", })); }