Files
blog/test/frontmatter-schema.test.js

77 lines
2.0 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const postsDir = path.resolve("src/posts");
test("Content Contract: All posts in src/posts/ satisfy frontmatter schema", () => {
if (!fs.existsSync(postsDir)) {
fs.mkdirSync(postsDir, { recursive: true });
}
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
if (postFiles.length === 0) {
// When posts are decoupled and mounted externally or ingested via API
return;
}
for (const file of postFiles) {
const filePath = path.join(postsDir, file);
const content = fs.readFileSync(filePath, "utf-8");
// 1. Filename Convention: YYYY-MM-DD-slug.md
assert.match(
file,
/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.md$/,
`Post filename '${file}' must follow 'YYYY-MM-DD-slug.md' naming standard`
);
// 2. YAML Frontmatter Boundary check
assert.match(
content,
/^---\n([\s\S]*?)\n---/,
`Post '${file}' must start with valid YAML frontmatter delimiters`
);
// Extract frontmatter block
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const fmText = fmMatch[1];
// 3. Title Invariant
assert.match(
fmText,
/title:\s*"[^"]{5,}"/,
`Post '${file}' must contain a valid title of at least 5 characters`
);
// 4. Date Invariant (ISO Format)
assert.match(
fmText,
/date:\s*\d{4}-\d{2}-\d{2}/,
`Post '${file}' must define an ISO date (YYYY-MM-DD)`
);
// 5. Layout Invariant
assert.match(
fmText,
/layout:\s*layouts\/post\.njk/,
`Post '${file}' must specify 'layout: layouts/post.njk'`
);
// 6. Tags Invariant
assert.match(
fmText,
/tags:\s*\n(\s+-\s+[a-z0-9-]+\n)+/,
`Post '${file}' must specify a non-empty YAML tags list`
);
// 7. Excerpt Invariant
assert.match(
fmText,
/excerpt:\s*"[^"]{20,}"/,
`Post '${file}' must provide a substantive excerpt of at least 20 characters`
);
}
});