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

  1. Populate headingProse with at least one paragraph under the page heading.
  2. Alternatively add competitorProfiles entries with paragraphs so profileBlocks is non-empty.
  3. 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 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


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)