]*\bopen\b/.test(article), 'Inline Contents must start collapsed');
if (name === 'abstract-and-section') assert(article.includes('href="#abstract">Abstract'));
}
const postFixture = readFileSync(join(temp, 'out/writing/post-test/index.html'), 'utf8');
assert(!postFixture.includes('/js/contents.'), 'Unsectioned article loads unused navigation');
assert(postFixture.includes('An italic subtitle for a post'), 'Post subtitle missing');
assert(postFixture.includes('katex-mathml'), 'Post math missing');
for (const article of [page, postFixture]) {
assert(article.includes('class="footnote-ref"'), 'Footnote reference missing');
assert(article.includes('class="footnote-backref"'), 'Footnote return link missing');
assert(article.includes('/js/footnotes.'), 'Footnote previews missing');
}
assert(postFixture.includes('A second paragraph'), 'Multi-paragraph footnote missing');
assert(postFixture.match(/class="footnote-ref"/g).length === 2, 'Repeated footnote references missing');
assert(!home.includes('/js/footnotes.'), 'Homepage loads unused footnote script');
const post = readFileSync(join(temp, 'out/writing/tagged-post-test/index.html'), 'utf8');
assert(!post.includes('katex.min.css'), 'Non-math post loads math assets');
const rss = readFileSync(join(temp, 'out/writing/rss.xml'), 'utf8');
assert(rss.includes('A “quoted” title'), 'RSS typography inconsistent');
assert(rss.includes('Code & tools'), 'RSS titles must escape XML characters');
assert(rss.includes('Tools & code for a small project.'));
assert(page.includes(''), 'Fenced code should use plain semantic markup');
assert(!page.includes('class="chroma"'), 'Unused syntax-token markup should not be generated');
for (const [shipped, upstream] of [
['static/licenses/cc0.txt', 'LICENSE'],
['static/licenses/katex.txt', 'node_modules/katex/LICENSE'],
['static/licenses/newsreader.txt', 'node_modules/@fontsource-variable/newsreader/LICENSE'],
])
assert.equal(readFileSync(shipped, 'utf8'), readFileSync(upstream, 'utf8'), `Outdated license: ${shipped}`);
const checks = join(temp, 'checks');
mkdirSync(checks);
const checkFile = join(checks, 'index.html');
writeFileSync(checkFile, 'Heading
Link');
checkSite(checks);
for (const [html, expected] of [
['Broken fragment', /missing fragment/],
['
', /broken asset or link/],
['', /duplicate id/],
['', /missing accessible control target/],
]) {
writeFileSync(checkFile, html);
assert.throws(() => checkSite(checks), expected);
}
const originalURL = process.env.SITE_BASE_URL;
try {
process.env.SITE_BASE_URL = 'https://alternate.example/';
assert.equal(siteBaseURL().href, process.env.SITE_BASE_URL);
writeFileSync(checkFile, 'Broken absolute link');
assert.throws(() => checkSite(checks), /broken asset or link/);
for (const url of [
'https://example.test/subpath/',
'file:///tmp/',
'https://example.test/?q=1',
'https://example.test/#hash',
'https://user:pass@example.test/',
]) {
process.env.SITE_BASE_URL = url;
assert.throws(siteBaseURL, /must be an HTTP\(S\) origin/);
}
} finally {
if (originalURL === undefined) delete process.env.SITE_BASE_URL;
else process.env.SITE_BASE_URL = originalURL;
}
const writingIndex = join(temp, 'content/writing/_index.md');
const originalWriting = readFileSync(writingIndex, 'utf8');
const changedDescription = 'Notes & ideas about writing.';
writeFileSync(writingIndex, originalWriting.replace(/^description:.*$/m, `description: "${changedDescription}"`));
assert.equal(build().status, 0);
checkDescriptions(join(temp, 'out'), changedDescription);
assert.equal(
readFileSync(join(temp, 'out/index.html'), 'utf8'),
home,
'Writing changes must not alter the homepage',
);
writeFileSync(writingIndex, originalWriting);
const invalidFile = join(temp, 'content/writing/invalid.md');
for (const format of ['', 'format: typo\n']) {
writeFileSync(invalidFile, `---\ntitle: Invalid\ndate: 2026-01-01\n${format}---\nText.\n`);
const invalid = build();
assert.notEqual(invalid.status, 0, 'Invalid writing format must fail the build');
assert((invalid.stdout + invalid.stderr).includes('format must be post or essay'));
}
rmSync(invalidFile);
writeFileSync(
join(temp, 'content/writing/draft.md'),
'---\ntitle: Draft\ndate: 2026-01-01\nformat: post\ntags: [programming, draft-only]\ndraft: true\n---\nDraft text.\n',
);
assert.equal(build().status, 0);
assert(!existsSync(join(temp, 'out/writing/draft/index.html')), 'Draft leaked into the production build');
assert(!existsSync(join(temp, 'out/tags/draft-only/index.html')), 'Draft-only tag leaked into production');
assert(!readFileSync(join(temp, 'out/tags/index.html'), 'utf8').includes('#draft-only'));
for (const path of [
'writing/index.html',
'writing/posts/index.html',
'tags/programming/index.html',
'rss.xml',
'writing/rss.xml',
'writing/posts/rss.xml',
])
assert(!readFileSync(join(temp, 'out', path), 'utf8').includes('/writing/draft/'), `Draft leaked into ${path}`);
assert.equal(build('--buildDrafts').status, 0);
assert(existsSync(join(temp, 'out/writing/draft/index.html')), 'Draft preview omitted draft content');
assert(
readFileSync(join(temp, 'out/tags/draft-only/index.html'), 'utf8').includes('/writing/draft/'),
'Draft tag preview missing',
);
rmSync(join(temp, 'content/writing/math-test.md'));
assert.equal(build().status, 0);
const emptyEssays = readFileSync(join(temp, 'out/writing/essays/index.html'), 'utf8');
assert(emptyEssays.includes('No essays yet.'), 'Empty essay archive missing');
assert(emptyEssays.includes('/writing/essays/rss.xml'), 'Empty essay archive lost its feed');
assert(!readFileSync(join(temp, 'out/writing/essays/rss.xml'), 'utf8').includes('- '));
for (const name of readdirSync(join(temp, 'content/writing')))
if (name.endsWith('.md') && name !== '_index.md') rmSync(join(temp, 'content/writing', name));
assert.equal(build().status, 0);
checkSite(join(temp, 'out'));
assert(readFileSync(join(temp, 'out/writing/index.html'), 'utf8').includes('No writing yet.'));
assert(readFileSync(join(temp, 'out/tags/index.html'), 'utf8').includes('No tags yet.'));
console.log(
'Passed: writing archives, tags, drafts, empty states, math, equations, typography, dates, subtitles, biography, RSS, and conditional assets.',
);
} finally {
rmSync(temp, { recursive: true, force: true });
}