{"record":{"id":"5550518ee4cb034b","repo":"koala73/worldmonitor","slug":"h2-merged-heading-has-no-following-prose","errorCode":null,"errorMessage":" H2 \"' + merged.heading + '\" has no following prose","messagePattern":" H2 \"' \\+ merged\\.heading \\+ '\" has no following prose","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/build-comparison-pages.mjs","lineNumber":471,"sourceCode":"  const faqNames = new Set();\n  for (const [question] of faqs) {\n    const key = String(question).trim().toLowerCase();\n    if (faqNames.has(key)) {\n      throw new Error(page.slug + ' duplicate FAQ question: ' + question);\n    }\n    faqNames.add(key);\n  }\n  const merged = { ...page, ...narrative, faqs };\n  delete merged.extraFaqs;\n  const whyBody = (merged.whyWeWinBody ?? []).join(' ').replace(/\\s+/g, ' ').trim();\n  if (!whyBody) {\n    throw new Error(page.slug + ' is missing whyWeWinBody');\n  }\n  if (whyBody === merged.whyWeWin.replace(/\\s+/g, ' ').trim()) {\n    throw new Error(page.slug + ' whyWeWinBody must not repeat whyWeWin');\n  }\n  if (merged.heading && !(merged.headingProse?.length || merged.competitorProfiles?.length)) {\n    throw new Error(page.slug + ' H2 \"' + merged.heading + '\" has no following prose');\n  }\n  if (!merged.evaluationHeading || !merged.evaluationProse?.length) {\n    throw new Error(page.slug + ' is missing the evaluation section');\n  }\n  if (!merged.switchHeading || !merged.switchProse?.length) {\n    throw new Error(page.slug + ' is missing the switch-trigger section');\n  }\n  if (!merged.methodologyProse?.length) {\n    throw new Error(page.slug + ' is missing methodology prose');\n  }\n  return merged;\n}\n\nexport const COMPARISON_PAGES = COMPARISON_PAGE_SEEDS.map(applyComparisonNarrative);\n\nfunction renderParagraphs(paragraphs, escapeHtml) {\n  return (paragraphs ?? []).map((paragraph) => '      <p>' + escapeHtml(paragraph) + '</p>');\n}","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/build-comparison-pages.mjs#L453-L489","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nheading: 'How the tools compare'\n// after\nheading: 'How the tools compare',\nheadingProse: ['WorldMonitor leads on data breadth; here is where each competitor fits.'],\ncompetitorProfiles: [{ name: 'Flightradar24', strengths: '...', limits: '...' }]","handlingStrategy":"validation","validationCode":"if (narrative.heading && !narrative.headingProse?.length && !narrative.competitorProfiles?.length) {\n  throw new Error(page.slug + ' heading has no prose');\n}","typeGuard":"const headingHasProse = (n) => !n.heading || Boolean(n.headingProse?.length || n.competitorProfiles?.length);","tryCatchPattern":"try {\n  page = applyComparisonNarrative(page);\n} catch (e) {\n  if (String(e.message).includes('has no following prose')) {\n    console.error('Add headingProse or competitorProfiles under this H2:', e.message);\n  } else throw e;\n}","preventionTips":["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."],"tags":["build-time","content-validation","seo"],"backgroundTag":"empty-required-field","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}