{"record":{"id":"c872733fc2fab115","repo":"koala73/worldmonitor","slug":"faq-count-must-be-8-12-got-faqs-length","errorCode":null,"errorMessage":" FAQ count must be 8-12, got ' + faqs.length","messagePattern":" FAQ count must be 8-12, got ' \\+ faqs\\.length","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/build-comparison-pages.mjs","lineNumber":451,"sourceCode":"      ['Samdesk and Factal', 'dedicated social-signal verification desks'],\n    ],\n    whyWeWin: 'This is an intelligence layer, not a response capability. World Monitor sits alongside an assistance retainer as the always-on awareness feed: continuous multi-domain monitoring of conflict, aviation, maritime, and market signals that affect travelers, priced to cover the whole organization rather than only enrolled travelers. We cannot act on what we detect, and the page says so.',\n    faqs: [\n      ['Is World Monitor a Crisis24 alternative?', 'Not as a replacement. Crisis24 coordinates assistance; World Monitor is the always-on awareness feed that sits alongside an assistance retainer, covering conflict, aviation, maritime, and market signals. Buyers keep both: intelligence for awareness, assistance for response.'],\n      ['Does World Monitor replace duty-of-care providers?', 'No. World Monitor has no assistance centers, no medical evacuation, and no mass notification, and it does not claim them. It covers the awareness layer that duty-of-care programs usually lack.'],\n      ['What does travel risk intelligence cost?', 'World Monitor is free without signup, with Pro from $39.99/month. Assistance providers such as International SOS and Crisis24 negotiate enterprise retainers with undisclosed list pricing.'],\n    ],\n  },\n];\n\nfunction applyComparisonNarrative(page) {\n  const narrative = COMPARISON_NARRATIVES[page.slug];\n  if (!narrative) {\n    throw new Error('Missing comparison narrative for ' + page.slug);\n  }\n  const faqs = [...page.faqs, ...(narrative.extraFaqs ?? [])];\n  if (faqs.length < 8 || faqs.length > 12) {\n    throw new Error(page.slug + ' FAQ count must be 8-12, got ' + faqs.length);\n  }\n  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  }","sourceCodeStart":433,"sourceCodeEnd":469,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/build-comparison-pages.mjs#L433-L469","documentation":"applyComparisonNarrative merges page.faqs with narrative.extraFaqs and enforces an SEO-driven rule that each comparison page carries 8 to 12 FAQ entries. Pages outside that window are rejected so the FAQPage structured data and on-page FAQ block stay substantive.","triggerScenarios":"Running the comparison-pages build when (page.faqs.length + narrative.extraFaqs.length) is less than 8 or greater than 12 for any page slug.","commonSituations":"Adding a new comparison page with too few authored FAQs; appending extraFaqs without noticing the base page already has many; removing FAQs during copy review and dropping below the minimum.","solutions":["Count page.faqs plus narrative.extraFaqs for the failing slug and adjust until the total is 8-12.","Add FAQs to the narrative's extraFaqs to reach the minimum.","Trim redundant FAQs (or move them to extraFaqs removal) to get under 12.","Note duplicates will throw separately (duplicate FAQ question), so dedupe while adjusting."],"exampleFix":"// before\nfaqs: [['Is it free?', 'Yes']] // 1 FAQ total\n// after\nfaqs: [/* ensure 8-12 total after merging extraFaqs */\n  ['Is it free?', 'Yes'], /* ...7 more unique questions */\n];","handlingStrategy":"validation","validationCode":"const total = page.faqs.length + (narrative.extraFaqs?.length ?? 0);\nif (total < 8 || total > 12) throw new Error(`${page.slug}: ${total} FAQs, need 8-12`);","typeGuard":"const faqCountOk = (n) => Number.isInteger(n) && n >= 8 && n <= 12;","tryCatchPattern":"try {\n  page = applyComparisonNarrative(page);\n} catch (e) {\n  if (String(e.message).includes('FAQ count must be 8-12')) {\n    console.error('Adjust FAQ count for this page:', e.message);\n  } else throw e;\n}","preventionTips":["Track running FAQ totals in the narrative authoring doc per slug.","Add a lint/test that computes base + extraFaqs counts for every slug in CI.","When adding extraFaqs, check the base page's faq length first."],"tags":["build-time","content-validation","seo","faq-count"],"backgroundTag":"value-out-of-range","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"}