|
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// Blog content validation script |
| 3 | +// Checks every blog post for required frontmatter fields and data integrity. |
| 4 | +// Run: node scripts/validate-content.mjs |
| 5 | +// --------------------------------------------------------------------------- |
| 6 | +import fs from "node:fs"; |
| 7 | +import path from "node:path"; |
| 8 | +import matter from "gray-matter"; |
| 9 | + |
| 10 | +const BLOG_DIR = path.join(process.cwd(), "blog"); |
| 11 | +const REQUIRED_FIELDS = ["title", "date", "authors"]; |
| 12 | + |
| 13 | +let errors = 0; |
| 14 | +let checked = 0; |
| 15 | + |
| 16 | +// --- Blog frontmatter validation --- |
| 17 | +const dirs = fs |
| 18 | + .readdirSync(BLOG_DIR, { withFileTypes: true }) |
| 19 | + .filter((d) => d.isDirectory()); |
| 20 | + |
| 21 | +for (const dir of dirs) { |
| 22 | + const base = path.join(BLOG_DIR, dir.name); |
| 23 | + const file = ["index.mdx", "index.md"] |
| 24 | + .map((n) => path.join(base, n)) |
| 25 | + .find((f) => fs.existsSync(f)); |
| 26 | + |
| 27 | + if (!file) { |
| 28 | + console.error(`ERROR blog/${dir.name}/ — missing index.mdx or index.md`); |
| 29 | + errors++; |
| 30 | + continue; |
| 31 | + } |
| 32 | + |
| 33 | + checked++; |
| 34 | + const raw = fs.readFileSync(file, "utf-8"); |
| 35 | + let fm; |
| 36 | + try { |
| 37 | + fm = matter(raw).data; |
| 38 | + } catch (e) { |
| 39 | + console.error(`ERROR blog/${dir.name}/ — invalid frontmatter: ${e.message}`); |
| 40 | + errors++; |
| 41 | + continue; |
| 42 | + } |
| 43 | + |
| 44 | + for (const field of REQUIRED_FIELDS) { |
| 45 | + if (fm.draft === true) continue; // skip draft posts |
| 46 | + if (!fm[field]) { |
| 47 | + console.error(`ERROR blog/${dir.name}/ — missing required field "${field}"`); |
| 48 | + errors++; |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + // Validate date is parseable |
| 53 | + if (fm.date && isNaN(new Date(fm.date).getTime())) { |
| 54 | + console.error(`ERROR blog/${dir.name}/ — invalid date "${fm.date}"`); |
| 55 | + errors++; |
| 56 | + } |
| 57 | + |
| 58 | + // Title should not be empty |
| 59 | + if (fm.title && fm.title.trim().length === 0) { |
| 60 | + console.error(`ERROR blog/${dir.name}/ — title is empty`); |
| 61 | + errors++; |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +// --- Data file import validation --- |
| 66 | +const DATA_DIR = path.join(process.cwd(), "src", "data"); |
| 67 | +const dataFiles = fs |
| 68 | + .readdirSync(DATA_DIR) |
| 69 | + .filter((f) => f.endsWith(".ts")); |
| 70 | + |
| 71 | +for (const file of dataFiles) { |
| 72 | + const filePath = path.join(DATA_DIR, file); |
| 73 | + const content = fs.readFileSync(filePath, "utf-8"); |
| 74 | + |
| 75 | + // Check for empty export arrays (likely a mistake) |
| 76 | + const emptyArrayMatch = content.match(/export const \w+:\s*\w+\[\]\s*=\s*\[\s*\]/); |
| 77 | + if (emptyArrayMatch) { |
| 78 | + console.error(`WARN src/data/${file} — contains empty exported array`); |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +console.log(`\nValidated ${checked} blog posts, found ${errors} error(s).`); |
| 83 | + |
| 84 | +if (errors > 0) { |
| 85 | + process.exit(1); |
| 86 | +} |
| 87 | + |
| 88 | +console.log("All content checks passed."); |
0 commit comments