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

  1. Add headingProse paragraphs to the slug's narrative entry.
  2. Or provide competitorProfiles entries for that section.
  3. Remove the heading if the section is intentionally omitted.
  4. 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

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


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)