diff --git a/scripts/data/blog.mjs b/scripts/data/blog.mjs index 9c90fd4a..824faec1 100644 --- a/scripts/data/blog.mjs +++ b/scripts/data/blog.mjs @@ -11,6 +11,18 @@ const OUTPUT = join(ROOT, 'generated', 'blog.json'); const titleFromBody = body => body.match(/^#\s+(.+)$/m)?.[1].trim() ?? null; +const parseDate = (value, file) => { + const date = value ? new Date(value) : null; + + if (!date || Number.isNaN(date.getTime())) { + throw new Error( + `Invalid or missing blog date in pages/blog/posts/${file}: ${JSON.stringify(value ?? '')}` + ); + } + + return date.toISOString(); +}; + const readPosts = async () => { const entries = await readdir(POSTS_DIR); const files = entries.filter(name => name.endsWith('.md')); @@ -26,7 +38,7 @@ const readPosts = async () => { slug, title: titleFromBody(content) ?? slug, authors: data.authors?.split(',').map(s => s.trim()), - date: new Date(data.date).toISOString(), + date: parseDate(data.date, file), category: data.category ?? null, image: data.image ?? null, ...(data.description && { description: data.description }), diff --git a/tests/data/blog.test.mjs b/tests/data/blog.test.mjs new file mode 100644 index 00000000..bc5a0bf8 --- /dev/null +++ b/tests/data/blog.test.mjs @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import { execFile } from 'node:child_process'; +import { + copyFile, + mkdtemp, + mkdir, + readFile, + rm, + writeFile, +} from 'node:fs/promises'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import { test } from 'node:test'; + +const execFileAsync = promisify(execFile); +const blogScript = fileURLToPath( + new URL('../../scripts/data/blog.mjs', import.meta.url) +); + +const createFixture = async content => { + const root = await mkdtemp(join('tests', 'data', '.blog-')); + await mkdir(join(root, 'scripts', 'data'), { recursive: true }); + await mkdir(join(root, 'pages', 'blog', 'posts'), { recursive: true }); + await mkdir(join(root, 'generated'), { recursive: true }); + await copyFile(blogScript, join(root, 'scripts', 'data', 'blog.mjs')); + await writeFile(join(root, 'pages', 'blog', 'posts', 'example.md'), content); + return root; +}; + +const runFixture = root => + execFileAsync(process.execPath, ['scripts/data/blog.mjs'], { cwd: root }); + +test('generates blog data for valid dates', async t => { + const root = await createFixture("---\ndate: '2026-09-22'\n---\n# Test\n"); + t.after(() => rm(root, { recursive: true, force: true })); + + await runFixture(root); + const posts = JSON.parse( + await readFile(join(root, 'generated', 'blog.json'), 'utf8') + ); + + assert.equal(posts[0].date, '2026-09-22T00:00:00.000Z'); +}); + +test('reports the file for an invalid blog date', async t => { + const root = await createFixture('---\ndate: not-a-date\n---\n# Test\n'); + t.after(() => rm(root, { recursive: true, force: true })); + + await assert.rejects(runFixture(root), error => { + assert.match(error.stderr, /Invalid or missing blog date/); + assert.match(error.stderr, /pages[\\/]blog[\\/]posts[\\/]example\.md/); + assert.match(error.stderr, /not-a-date/); + return true; + }); +}); + +test('reports the file for a missing blog date', async t => { + const root = await createFixture('---\ntitle: Test\n---\n# Test\n'); + t.after(() => rm(root, { recursive: true, force: true })); + + await assert.rejects(runFixture(root), error => { + assert.match(error.stderr, /Invalid or missing blog date/); + assert.match(error.stderr, /pages[\\/]blog[\\/]posts[\\/]example\.md/); + assert.match(error.stderr, //); + return true; + }); +});