koala73/worldmonitor · error
H2 "' + page.heading + '" has no following content
Error message
H2 "' + page.heading + '" has no following content
What it means
When a keyword-section page defines a page.heading (H2), renderKeywordSection requires that the heading be followed by content: the heading prose paragraphs and/or competitor profile blocks. If both are empty, it throws '<slug> H2 "<heading>" has no following content' to prevent shipping a bare H2 with nothing under it.
Solutions
- Populate headingProse with at least one paragraph under the page heading.
- Alternatively add competitorProfiles entries with paragraphs so profileBlocks is non-empty.
- Verify field names match exactly (headingProse, competitorProfiles, paragraphs) since the ?? [] defaults mask missing keys.
Example fix
// before
{
heading: 'Country Instability Index comparisons',
headingProse: [],
competitorProfiles: [],
}
// after
{
heading: 'Country Instability Index comparisons',
headingProse: ['This page compares how leading tools measure instability ...'],
competitorProfiles: [{ name: 'Acme Risk', paragraphs: ['...'] }],
} Defensive patterns
Strategy: validation
Validate before calling
if (page.heading && !(page.headingProse?.length || page.competitorProfiles?.length)) {
throw new Error(`${page.slug}: heading "${page.heading}" needs headingProse or competitorProfiles`);
} Type guard
const headingHasContent = (p) => !p.heading || (p.headingProse?.length ?? 0) + (p.competitorProfiles?.length ?? 0) > 0;
Prevention
- When adding a keyword heading, always fill headingProse in the same change.
- Beware ?? [] defaults masking missing keys — validate seeds explicitly.
When it happens
Trigger: renderKeywordSection is called on a page where page.heading is truthy but headingProse is undefined/empty AND competitorProfiles is undefined/empty (or all profiles were rejected), so inner.length === 0.
Common situations: A page seed sets a heading for SEO keyword targeting but the prose and profiles were never filled in; both headingProse and competitorProfiles keys are misspelled so the ?? [] fallbacks silently produce empty arrays.
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
- is missing following prose
- is missing methodology prose
- profile "' + profile.name + '" has no prose
- needs a World Monitor measurement explanation
- needs a summary for the llms.txt Comparisons section
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/158abd02e3abe05d.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/build-comparison-pages.mjs:516
}
function renderKeywordSection(page, escapeHtml) {
const headingProse = page.headingProse ?? [];
const profiles = page.competitorProfiles ?? [];
const profileBlocks = profiles.flatMap((profile) => {
if (!profile.paragraphs?.length) {
throw new Error(page.slug + ' profile "' + profile.name + '" has no prose');
}
const tag = page.heading ? 'h3' : 'h2';
return [
' <' + tag + '>' + escapeHtml(profile.name) + '</' + tag + '>',
...renderParagraphs(profile.paragraphs, escapeHtml),
];
});
if (page.heading) {
const inner = [...renderParagraphs(headingProse, escapeHtml), ...profileBlocks];
if (inner.length === 0) {
throw new Error(page.slug + ' H2 "' + page.heading + '" has no following content');
}
return [' <h2>' + escapeHtml(page.heading) + '</h2>', ...inner];
}
return profileBlocks;
}
function renderMatrix(rows, escapeHtml) {
for (const row of rows) {
if (row.length !== COMPARISON_MATRIX_COLUMNS.length) {
throw new Error(
'matrix row for "' + row[0] + '" has ' + row.length +
' cells, expected ' + COMPARISON_MATRIX_COLUMNS.length
);
}
}
const header = COMPARISON_MATRIX_COLUMNS
.map((column) => '<th>' + escapeHtml(column) + '</th>')
.join('');View on GitHub (pinned to 7d06c8633d)