koala73/worldmonitor · error
H2 "' + merged.heading + '" has no following prose
Error message
H2 "' + merged.heading + '" has no following prose
What it means
applyComparisonNarrative requires that any page with a custom H2 heading has supporting content immediately following it — either headingProse paragraphs or competitorProfiles. A bare H2 with nothing under it fails the content-quality gate.
Solutions
- Add headingProse paragraphs to the slug's narrative entry.
- Or provide competitorProfiles entries for that section.
- Remove the heading if the section is intentionally omitted.
- Verify field spelling: headingProse and competitorProfiles.
Example fix
// before
heading: 'How the tools compare'
// after
heading: 'How the tools compare',
headingProse: ['WorldMonitor leads on data breadth; here is where each competitor fits.'],
competitorProfiles: [{ name: 'Flightradar24', strengths: '...', limits: '...' }] Defensive patterns
Strategy: validation
Validate before calling
if (narrative.heading && !narrative.headingProse?.length && !narrative.competitorProfiles?.length) {
throw new Error(page.slug + ' heading has no prose');
} Type guard
const headingHasProse = (n) => !n.heading || Boolean(n.headingProse?.length || n.competitorProfiles?.length);
Try / catch
try {
page = applyComparisonNarrative(page);
} catch (e) {
if (String(e.message).includes('has no following prose')) {
console.error('Add headingProse or competitorProfiles under this H2:', e.message);
} else throw e;
} Prevention
- Treat heading and its prose as one atomic edit pair.
- Copy the heading/prose structure from a passing narrative when adding sections.
- Add a content lint that pairs every custom H2 with content before CI.
When it happens
Trigger: Running the build when merged.heading is set (truthy) but both merged.headingProse is empty/undefined and merged.competitorProfiles is empty/undefined for that slug.
Common situations: Adding a custom heading to a narrative without writing the prose under it; authoring competitor profiles in the wrong field name; deleting prose during a copy pass but keeping the heading.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- /accuracy/ meta description must be 155–160 chars (got ${len
- FAQ count must be 8-12, got ' + faqs.length
- writeAccuracySection requires the canonical DataCatalog iden
- Missing comparison narrative for ' + page.slug
- duplicate FAQ question: ' + question
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/5550518ee4cb034b.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/build-comparison-pages.mjs:471
const faqNames = new Set();
for (const [question] of faqs) {
const key = String(question).trim().toLowerCase();
if (faqNames.has(key)) {
throw new Error(page.slug + ' duplicate FAQ question: ' + question);
}
faqNames.add(key);
}
const merged = { ...page, ...narrative, faqs };
delete merged.extraFaqs;
const whyBody = (merged.whyWeWinBody ?? []).join(' ').replace(/\s+/g, ' ').trim();
if (!whyBody) {
throw new Error(page.slug + ' is missing whyWeWinBody');
}
if (whyBody === merged.whyWeWin.replace(/\s+/g, ' ').trim()) {
throw new Error(page.slug + ' whyWeWinBody must not repeat whyWeWin');
}
if (merged.heading && !(merged.headingProse?.length || merged.competitorProfiles?.length)) {
throw new Error(page.slug + ' H2 "' + merged.heading + '" has no following prose');
}
if (!merged.evaluationHeading || !merged.evaluationProse?.length) {
throw new Error(page.slug + ' is missing the evaluation section');
}
if (!merged.switchHeading || !merged.switchProse?.length) {
throw new Error(page.slug + ' is missing the switch-trigger section');
}
if (!merged.methodologyProse?.length) {
throw new Error(page.slug + ' is missing methodology prose');
}
return merged;
}
export const COMPARISON_PAGES = COMPARISON_PAGE_SEEDS.map(applyComparisonNarrative);
function renderParagraphs(paragraphs, escapeHtml) {
return (paragraphs ?? []).map((paragraph) => ' <p>' + escapeHtml(paragraph) + '</p>');
}View on GitHub (pinned to 7d06c8633d)