import assert from 'node:assert/strict';
import { after, before, test } from 'node:test';
import { cp, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { execFileSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import fg from 'fast-glob';
import matter from 'gray-matter';
import { load } from 'cheerio';
import { createPublicationManifest, hreflangLinksFor, renderSitemap, robotsFor } from '../src/utils/publication-policy.ts';
import { socialImageUrl } from '../src/utils/seo.ts';
import { blogPostingSchema, faqItemsFromSourceHtml, faqPageSchema, serializeJsonLd } from '../src/utils/structured-data.ts';
const project = fileURLToPath(new URL('../', import.meta.url));
const site = JSON.parse(await readFile(path.join(project, 'src/data/site.json'), 'utf8')).url;
const image = '/assets/images/hero-teach-self-advocacy.webp';
const languages = ['en', 'es', 'ar'];
const prefix = (lang) => lang === 'en' ? '' : `/${lang}`;
const artifacts = new Map();
let temporary;
let manifest;
let runner;
async function writeEntry(collection, lang, slug, data = {}) {
await writeFile(path.join(temporary, 'src/content', collection, lang, `${slug}.md`),
matter.stringify('Fixture body with **Markdown**.\n', {
title: `SEO title (${lang})`, description: `Description (${lang})`, slug, lang,
...(collection === 'blog' ? { date: '2026-10-08', author: 'rula-diab' } : {}), ...data
}));
}
function build(indexing = false) {
return execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: String(indexing), TZ: 'America/Phoenix' },
timeout: 60_000, maxBuffer: 2 * 1024 * 1024, stdio: 'pipe'
});
}
async function capture(name) {
const html = new Map();
for (const file of await fg('**/*.html', { cwd: path.join(temporary, 'dist') })) {
const route = '/' + file.replace(/(^|\/)index\.html$/, '').replace(/\/$/, '');
html.set(route, await readFile(path.join(temporary, 'dist', file), 'utf8'));
}
artifacts.set(name, { html, sitemap: await readFile(path.join(temporary, 'dist/sitemap.xml'), 'utf8') });
}
const page = (route, mode = 'staging') => {
const html = artifacts.get(mode).html.get(route);
assert.ok(html, `Missing ${mode} page: ${route}`);
return load(html);
};
const meta = ($, key) => $(`head meta[${key.startsWith('og:') ? 'property' : 'name'}="${key}"]`);
const jsonLd = ($) => $('script[type="application/ld+json"]').map((_, node) => JSON.parse($(node).text())).get();
const schemaOf = ($, type) => jsonLd($).filter((schema) => schema['@type'] === type);
const text = (value) => value.replace(/\s+/g, ' ').trim();
async function writeFaqFixture(slug, content, flags = {}) {
await writeFile(path.join(temporary, 'src/content/pages/en', `${slug}.mdx`), matter.stringify(
`import FAQAccordion from '../../../components/FAQAccordion.astro';\n\n${content}\n`,
{ title: 'FAQ fixture', slug, lang: 'en', ...flags }
));
}
before(async () => {
// The same real Astro configuration and temporary-copy approach as test:publication.
// Fixtures never enter the working tree or the project's dist directory.
temporary = await mkdtemp(path.join(os.tmpdir(), 'aia-seo-test-'));
await cp(path.join(project, 'src'), path.join(temporary, 'src'), { recursive: true });
await symlink(path.join(project, 'node_modules'), path.join(temporary, 'node_modules'), 'dir');
await writeFile(path.join(temporary, 'package.json'), '{"type":"module"}\n');
for (const lang of languages) {
await writeEntry('blog', lang, 'seo-default');
await writeEntry('blog', lang, 'seo-display', {
displayH1: `Display heading (${lang}) & text`, featuredImage: image,
alt: `Existing image description (${lang})`, date: '2024-12-02', updatedDate: '2026-10-08'
});
}
await writeEntry('blog', 'en', 'seo-minimal', { description: '' });
await writeEntry('blog', 'en', 'seo-noindex', { noindex: true });
await writeEntry('blog', 'en', 'seo-external', { canonical: 'https://publisher.example/original' });
await writeEntry('blog', 'en', 'seo-draft', { draft: true });
await writeEntry('blog', 'en', 'seo-escaped', { title: 'Literal & text' });
const accordion = 'Visible answer & link.
" }]} />';
await writeFaqFixture('seo-faq', accordion + '\n');
await writeFaqFixture('seo-faq-noindex', accordion, { noindex: true });
await writeFaqFixture('seo-faq-external', accordion, { canonical: 'https://publisher.example/faq' });
await writeFaqFixture('seo-faq-empty', '');
// An unknown runtime source cannot be advertised as server-verified FAQ content.
await writeFaqFixture('seo-faq-runtime-only', '');
await writeEntry('pages', 'en', 'seo-derived', { title: 'Derived banner | SEO suffix' });
await writeEntry('pages', 'en', 'seo-no-image', { alt: 'Must not describe a nonexistent image' });
// A fallback banner must not inherit unrelated alt text from the page record.
const abaPath = path.join(temporary, 'src/content/pages/en/aba-therapy.mdx');
await writeFile(abaPath, (await readFile(abaPath, 'utf8')).replace(/^---\n/, '---\nalt: Unrelated description without a featured image\n'));
const entries = await Promise.all((await fg('src/content/{pages,blog}/**/*.{md,mdx}', { cwd: temporary })).map(async (file) => {
const [, , collection, ...parts] = file.split('/');
return { id: parts.join('/').replace(/\.mdx?$/, ''), collection, data: matter(await readFile(path.join(temporary, file), 'utf8')).data };
}));
manifest = createPublicationManifest(entries, site);
// A preview Astro.site must not leak into social assets or publication metadata.
runner = `import {build} from ${JSON.stringify(pathToFileURL(path.join(project, 'node_modules/astro/dist/index.js')).href)};
await build({root:${JSON.stringify(temporary)},configFile:${JSON.stringify(path.relative(temporary, path.join(project, 'astro.config.mjs')))},site:'https://preview.example.invalid',logLevel:'silent',vite:{cacheDir:${JSON.stringify(path.join(temporary, '.vite'))}}});`;
for (const mode of ['staging', 'indexing']) {
build(mode === 'indexing');
await capture(mode);
}
for (const [file, heading] of [
['en/aba-therapy.mdx', 'Explicit service heading'],
['en/seo-derived.md', 'Explicit derived heading'],
['en/index.md', 'Explicit home heading '],
['es/library.md', 'Explicit Library heading']
]) {
const target = path.join(temporary, 'src/content/pages', file);
await writeFile(target, (await readFile(target, 'utf8')).replace(/^---\n/, `---\ndisplayH1: ${JSON.stringify(heading)}\n`));
}
build();
await capture('overrides');
}, { timeout: 120_000 });
after(async () => {
if (temporary) await rm(temporary, { recursive: true, force: true });
});
test('social images resolve root-relative, path-relative and absolute HTTP(S) URLs', () => {
for (const input of [image, image.slice(1)]) assert.equal(socialImageUrl(input, site), site + image);
assert.equal(socialImageUrl('https://images.example/image.jpg', site), 'https://images.example/image.jpg');
for (const input of [undefined, '', ' ']) assert.equal(socialImageUrl(input, site), undefined);
for (const input of ['javascript:alert(1)', 'data:image/png;base64,AA', 'https://user:secret@example.com/image', 'https://[invalid']) {
assert.throws(() => socialImageUrl(input, site));
}
});
test('articles without overrides retain their original H1 in English, Spanish and Arabic', () => {
for (const lang of languages) {
const $ = page(`${prefix(lang)}/library/seo-default`);
assert.equal($('main h1').text(), `SEO title (${lang})`);
assert.equal($('head title').text(), `SEO title (${lang})`);
}
const $ = page('/ar/library/autism-self-advocacy-skills-aba');
assert.equal($('main h1').text(), 'دليل تعليم مهارات الدفاع عن النفس في ABA');
});
test('explicit article H1 changes only the heading; SEO and card titles retain title', () => {
for (const lang of languages) {
const route = `${prefix(lang)}/library/seo-display`;
const $ = page(route);
assert.equal($('main h1').text(), `Display heading (${lang}) & text`);
assert.equal($('main h1').children().length, 0, 'Overrides are text, not injected markup');
for (const tag of ['og:title', 'twitter:title']) assert.equal(meta($, tag).attr('content'), `SEO title (${lang})`);
assert.equal($('head title').text(), `SEO title (${lang})`);
const index = page(`${prefix(lang)}/library`);
assert.equal(index(`.blog-card h3 a[href="${route}"]`).text(), `SEO title (${lang})`);
}
});
test('production-supported English and Spanish article headings retain their original SEO titles', () => {
for (const [lang, heading] of [
['en', 'Guide to Teaching Self‑Advocacy in ABA'],
['es', 'Guía para enseñar la autodefensa en el ABA']
]) {
const route = `${prefix(lang)}/library/autism-self-advocacy-skills-aba`;
const $ = page(route);
const record = manifest.find((item) => item.route === route).entry.data;
assert.equal($('main h1').text(), heading);
assert.notEqual(heading, record.title);
assert.equal($('head title').text(), record.title);
const index = page(`${prefix(lang)}/library`);
assert.equal(index(`.blog-card h3 a[href="${route}"]`).text(), record.title);
}
});
test('existing service, page and derived banner headings remain unless explicitly overridden', () => {
for (const [route, heading] of [
['/aba-therapy', 'Behavioral'], ['/es/aba-therapy', 'Conductual'],
['/services', 'ABA & Educational Services'], ['/seo-derived', 'Derived banner']
]) assert.equal(page(route)('main h1').text(), heading);
for (const [route, heading] of [['/aba-therapy', 'Explicit service heading'], ['/seo-derived', 'Explicit derived heading']]) {
const $ = page(route, 'overrides');
assert.equal($('main h1').text(), heading);
assert.equal($('head title').text(), page(route)('head title').text());
}
});
test('home and Library retain component-owned headings and support a single explicit H1', () => {
const $ = page('/');
assert.equal($('main h1').text(), 'Behavioral Health& SpecialEducation');
assert.equal($('main h1 br').length, 2, 'Preserve the existing homepage heading markup');
assert.equal(page('/library')('main h1').text(), 'The Library at AZ Institute for Autism');
for (const [route, heading] of [['/', 'Explicit home heading '], ['/es/library', 'Explicit Library heading']]) {
const changed = page(route, 'overrides');
assert.equal(changed('main h1').text(), heading);
assert.equal(changed('head title').text(), page(route)('head title').text());
}
});
test('every generated page has exactly one visible main H1 in every fixture build', () => {
for (const [mode, { html }] of artifacts) {
for (const [route, content] of html) {
const $ = load(content);
assert.equal($('h1').length, 1, `${mode}: ${route}`);
assert.equal($('main h1').length, 1, `${mode}: ${route}`);
assert.ok($('main h1').text().trim(), `${mode}: ${route}`);
}
}
});
test('featured images emit one absolute OG/Twitter image and supported alt in each locale', () => {
for (const lang of languages) {
const $ = page(`${prefix(lang)}/library/seo-display`);
for (const key of ['og:image', 'twitter:image']) {
assert.equal(meta($, key).length, 1);
assert.equal(meta($, key).attr('content'), site + image);
assert.equal(meta($, `${key}:alt`).attr('content'), `Existing image description (${lang})`);
}
assert.equal(meta($, 'twitter:card').attr('content'), 'summary_large_image');
assert.equal($('.article-featured img').attr('alt'), `Existing image description (${lang})`);
}
});
test('no image means no image-specific metadata and a valid summary card; no alt is invented', () => {
for (const route of ['/', '/seo-no-image', '/library/seo-default']) {
const $ = page(route);
assert.equal($('meta[property^="og:image"], meta[name^="twitter:image"]').length, 0, route);
assert.equal(meta($, 'twitter:card').attr('content'), 'summary');
}
for (const route of ['/aba-therapy', '/library/autism-self-advocacy-skills-aba']) {
const $ = page(route);
assert.equal(meta($, 'og:image').length, 1);
assert.equal(meta($, 'og:image:alt').length, 0);
assert.equal(meta($, 'twitter:image:alt').length, 0);
}
});
for (const mode of ['staging', 'indexing']) {
test(`${mode}: existing publication policy, metadata, language and navigation are unchanged`, () => {
const { html, sitemap } = artifacts.get(mode);
assert.deepEqual([...html.keys()].sort(), manifest.filter((item) => item.eligible).map((item) => item.route).sort());
assert.equal(sitemap, renderSitemap(manifest, mode === 'indexing'));
for (const publication of manifest.filter((item) => item.eligible)) {
const { route, entry, canonical } = publication;
const $ = page(route, mode);
assert.equal($('head link[rel="canonical"]').length, 1, route);
assert.equal($('head link[rel="canonical"]').attr('href'), canonical, route);
assert.equal(meta($, 'og:url').attr('content'), canonical, route);
assert.equal(meta($, 'robots').attr('content'), robotsFor(publication, mode === 'indexing'), route);
assert.deepEqual($('head link[hreflang]').map((_, tag) => ({ lang: $(tag).attr('hreflang'), href: $(tag).attr('href') })).get(), hreflangLinksFor(publication, mode === 'indexing'), route);
assert.deepEqual($('.lang-switcher__menu a').map((_, tag) => $(tag).attr('href')).get(), publication.translations.map((item) => item.route), route);
assert.equal($('html').attr('lang'), entry.data.lang, route);
assert.equal($('html').attr('dir'), entry.data.lang === 'ar' ? 'rtl' : 'ltr', route);
assert.equal(meta($, 'og:locale').length, 0, 'No unsupported regional locale assignments');
assert.equal(meta($, 'og:type').attr('content'), entry.collection === 'blog' ? 'article' : 'website', route);
for (const key of ['og:title', 'twitter:title']) assert.equal(meta($, key).attr('content'), $('head title').text(), route);
for (const key of ['og:description', 'twitter:description']) assert.equal(meta($, key).attr('content'), meta($, 'description').attr('content'), route);
const ogImage = meta($, 'og:image').attr('content');
assert.equal(meta($, 'twitter:image').attr('content'), ogImage, route);
if (ogImage) assert.equal(new URL(ogImage).origin, site, route);
}
for (const route of ['/schedule-consultation', '/employee-portal', '/library/community-highlight-meet-rula-diab', '/library/new-aia-scottsdale-office']) {
const $ = page(route, mode);
assert.match(meta($, 'robots').attr('content'), /^noindex,/);
assert.equal($('link[hreflang]').length, 0);
assert.ok(!sitemap.includes(`${site}${route}<`));
}
assert.ok(!html.has('/ar'));
});
}
test('BlogPosting uses the SEO title, canonical, declared day and matching locale author', () => {
for (const mode of ['staging', 'indexing']) {
for (const publication of manifest.filter((item) => item.eligible && item.entry.collection === 'blog')) {
const { entry, route } = publication;
const $ = page(route, mode);
const schemas = schemaOf($, 'BlogPosting');
assert.equal(schemas.length, publication.sitemapEligible ? 1 : 0, route);
if (!schemas.length) continue;
const schema = schemas[0];
assert.equal(schema.headline, entry.data.title, route);
assert.equal(schema['@id'], `${publication.canonical}#article`, route);
assert.equal(schema.url, publication.canonical, route);
assert.equal(schema.mainEntityOfPage, publication.canonical, route);
assert.equal(schema.inLanguage, entry.data.lang, route);
assert.equal(schema.datePublished, new Date(entry.data.date).toISOString().slice(0, 10), route);
assert.deepEqual(schema.author, { '@type': 'Person', name: 'Rula Diab' }, route);
assert.ok(!('publisher' in schema), 'Do not manufacture publisher entities');
assert.equal(schema.image, meta($, 'og:image').attr('content'), route);
}
for (const lang of languages) {
const $ = page(`${prefix(lang)}/library/seo-display`, mode);
const schema = schemaOf($, 'BlogPosting')[0];
assert.notEqual(schema.headline, $('main h1').text());
assert.equal(schema.dateModified, '2026-10-08');
assert.equal(schema.image, site + image);
}
}
});
test('missing optional article metadata stays absent, without fabricated modification dates or profile URLs', () => {
const schema = schemaOf(page('/library/seo-minimal'), 'BlogPosting')[0];
for (const key of ['image', 'description', 'dateModified', 'publisher']) assert.ok(!(key in schema), key);
assert.ok(!('url' in schema.author), 'Author archives are not generated local routes');
for (const lang of languages) {
const actual = schemaOf(page(`${prefix(lang)}/library/autism-self-advocacy-skills-aba`), 'BlogPosting')[0];
assert.ok(!('dateModified' in actual), 'Do not import a live timestamp into otherwise unreconciled local content');
}
});
test('article and card calendar dates match declared dates even on a negative-offset build host', () => {
for (const publication of manifest.filter((item) => item.eligible && item.entry.collection === 'blog')) {
const { data } = publication.entry;
const date = new Date(data.date);
const article = page(publication.route);
const byline = article('.article-byline time');
assert.equal(byline.attr('datetime'), date.toISOString());
assert.equal(byline.text(), date.toLocaleDateString(data.lang, { day: '2-digit', month: 'long', year: 'numeric', timeZone: 'UTC' }));
const index = page(`${prefix(data.lang)}/library`);
const card = index('.blog-card').filter((_, el) => index(el).find('h3 a').attr('href') === publication.route);
assert.equal(card.find('time').text(), date.toLocaleDateString(data.lang, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' }));
}
});
test('JSON-LD serialization preserves script-like text without creating HTML/script nodes', () => {
const schema = { text: ' & text' };
const html = ``;
const $ = load(html);
assert.equal($('script').length, 1);
assert.deepEqual(jsonLd($), [schema]);
const rendered = page('/library/seo-escaped');
assert.equal(schemaOf(rendered, 'BlogPosting')[0].headline, schema.text.replace(/^/, 'Literal '));
assert.equal(rendered('script:not([type="application/ld+json"])').filter((_, el) => rendered(el).text() === 'not executable').length, 0);
});
test('noindex, external-canonical and draft records cannot gain article or FAQ entities', () => {
for (const mode of ['staging', 'indexing']) {
for (const route of ['/schedule-consultation', '/employee-portal', '/library/community-highlight-meet-rula-diab', '/library/new-aia-scottsdale-office', '/library/seo-noindex', '/library/seo-external', '/seo-faq-noindex', '/seo-faq-external']) {
const $ = page(route, mode);
assert.equal(schemaOf($, 'BlogPosting').length, 0, route);
assert.equal(schemaOf($, 'FAQPage').length, 0, route);
assert.equal(schemaOf($, 'MedicalOrganization').length, 1, 'Existing site schema remains independent');
}
assert.ok(!artifacts.get(mode).html.has('/library/seo-draft'));
assert.ok(!artifacts.get(mode).html.has('/ar'));
}
const draft = manifest.find((item) => item.route === '/library/seo-draft');
assert.equal(blogPostingSchema(draft.entry, draft, [], site), undefined);
assert.equal(faqPageSchema([{ question: 'Q?', answer: 'A.' }], draft), undefined);
});
test('every server-item FAQ schema matches actual visible questions and answers', () => {
for (const mode of ['staging', 'indexing']) {
for (const [route, content] of artifacts.get(mode).html) {
const $ = load(content);
const schemas = schemaOf($, 'FAQPage');
const details = $('[data-faq-list] details.faq');
if (!schemas.length || !details.length) continue;
assert.equal(schemas.length, 1, route);
assert.equal(schemas[0]['@id'], `${site}${route}#faq`, route);
assert.equal(schemas[0].mainEntity.length, details.length, route);
details.each((i, el) => {
assert.deepEqual(schemas[0].mainEntity[i], {
'@type': 'Question', name: text($(el).find('summary').text()),
acceptedAnswer: { '@type': 'Answer', text: text($(el).find('.faq-answer__inner').text()) }
}, route);
});
}
}
const corrected = schemaOf(page('/seo-faq'), 'FAQPage')[0];
assert.equal(corrected.mainEntity[0].acceptedAnswer.text, 'Visible answer & link.');
assert.equal(schemaOf(page('/insurance'), 'FAQPage')[0].mainEntity.length, 2);
assert.equal(schemaOf(page('/es/library/parents-guide-to-autism-and-aba'), 'FAQPage')[0].mainEntity.length, 6);
});
test('standalone runtime accordion gets server schema from its exact rendered source, not invented items', () => {
const $ = page('/faqs');
const questions = $('#faq-page-source > h3');
const schema = schemaOf($, 'FAQPage')[0];
assert.equal(questions.length, 70);
assert.equal(schema.mainEntity.length, 70);
assert.equal($('[data-faq-list] details').length, 0, 'Preserve the existing runtime enhancement path');
questions.each((i, el) => {
assert.equal(schema.mainEntity[i].name, text($(el).text()));
// HTML compression and Cheerio's sibling selection discard block-boundary
// whitespace. Compare all answer characters independently of that spacing.
assert.equal(schema.mainEntity[i].acceptedAnswer.text.replace(/\s/g, ''), $(el).nextUntil('h3').text().replace(/\s/g, ''));
});
for (const route of ['/seo-faq-runtime-only', '/seo-faq-empty']) assert.equal(schemaOf(page(route), 'FAQPage').length, 0);
const nested = faqItemsFromSourceHtml('Introduction
Not an answer
One?
Text & more.
Detail
Two?
Next.
');
assert.deepEqual(nested.map(({ question }) => question), ['One?', 'Two?']);
assert.ok(nested[0].answerHtml.includes('Detail
'));
assert.ok(!nested[0].answer.includes('Not an answer'));
});
test('multiple JSON-LD blocks coexist without duplicate article, FAQ or entity IDs', () => {
for (const { html } of artifacts.values()) {
for (const [route, content] of html) {
const $ = load(content);
const schemas = jsonLd($);
assert.equal(schemaOf($, 'MedicalOrganization').length, 1, route);
assert.ok(schemaOf($, 'BlogPosting').length <= 1, route);
assert.ok(schemaOf($, 'FAQPage').length <= 1, route);
const ids = schemas.map((schema) => schema['@id']).filter(Boolean);
assert.equal(ids.length, new Set(ids).size, route);
}
}
assert.deepEqual(jsonLd(page('/library/parents-guide-to-autism-and-aba')).map((schema) => schema['@type']).sort(), ['BlogPosting', 'FAQPage', 'MedicalOrganization']);
assert.equal(schemaOf(page('/seo-faq'), 'WebPage')[0].name, 'Existing unrelated block');
});
test('duplicate questions, duplicate accordion schemas and conflicting canonicals fail explicitly', async () => {
const publication = manifest.find((item) => item.route === '/seo-faq');
assert.throws(() => faqPageSchema([{ question: 'Same?', answer: 'A' }, { question: ' same? ', answer: 'B' }], publication), /Duplicate FAQ question/);
assert.throws(() => faqPageSchema([{ question: 'Empty?', answer: ' ' }], publication), /Empty FAQ/);
for (const [content, expected] of [
['', /Multiple FAQ schemas/],
['', /FAQ canonical disagrees/],
['', /must not be mixed/]
]) {
try {
await writeFaqFixture('seo-invalid-faq', content);
assert.throws(() => build(), (error) => { assert.match(String(error.stdout) + String(error.stderr), expected); return true; });
} finally {
await rm(path.join(temporary, 'src/content/pages/en/seo-invalid-faq.mdx'), { force: true });
}
}
}, { timeout: 60_000 });
test('authors resolve by exact locale and slug; missing/ambiguous records and reversed dates fail', async () => {
const publication = manifest.find((item) => item.route === '/library/seo-display');
const entry = { ...publication.entry, data: { ...publication.entry.data, date: new Date('2024-12-02'), updatedDate: new Date('2026-10-08') } };
const author = (lang) => ({ id: `${lang}/rula-diab`, collection: 'authors', data: { slug: 'rula-diab', name: `Name (${lang})`, lang } });
assert.deepEqual(blogPostingSchema(entry, publication, [author('en'), author('es')], site).author, { '@type': 'Person', name: 'Name (en)' });
assert.throws(() => blogPostingSchema(entry, publication, [author('es')], site), /Missing or ambiguous article author/);
assert.throws(() => blogPostingSchema(entry, publication, [author('en'), author('en')], site), /Missing or ambiguous article author/);
assert.throws(() => blogPostingSchema({ ...entry, data: { ...entry.data, updatedDate: new Date('2020-01-01') } }, publication, [author('en')], site), /modification precedes publication/);
try {
await writeEntry('blog', 'en', 'seo-invalid-author', { author: 'missing-person' });
assert.throws(() => build(), (error) => { assert.match(String(error.stdout) + String(error.stderr), /Missing or ambiguous article author/); return true; });
} finally {
await rm(path.join(temporary, 'src/content/blog/en/seo-invalid-author.md'), { force: true });
}
}, { timeout: 60_000 });
test('actual content-schema builds reject empty, whitespace-only and non-string displayH1', async () => {
for (const value of ['', ' ', '\n\t', 123]) {
await writeEntry('pages', 'en', 'seo-invalid', { displayH1: value });
assert.throws(() => build(), (error) => {
const diagnostic = String(error.stdout) + String(error.stderr);
assert.match(diagnostic, /displayH1/);
assert.match(diagnostic, /schema|InvalidContentEntryDataError/i);
return true;
}, `Invalid displayH1: ${JSON.stringify(value)}`);
}
}, { timeout: 60_000 });