{"record":{"id":"639468073d5f5228","repo":"koala73/worldmonitor","slug":"missing-comparison-narrative-for-page-slug","errorCode":null,"errorMessage":"Missing comparison narrative for ' + page.slug","messagePattern":"Missing comparison narrative for ' \\+ page\\.slug","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/build-comparison-pages.mjs","lineNumber":447,"sourceCode":"    concessions: [\n      ['International SOS', 'global assistance delivery: 27 assistance centers, medical evacuation, and case response'],\n      ['Crisis24', 'duty-of-care coordination and traveler-tracking workflows'],\n      ['Everbridge', 'mass notification and enterprise incident management'],\n      ['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');","sourceCodeStart":429,"sourceCodeEnd":465,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/build-comparison-pages.mjs#L429-L465","documentation":"applyComparisonNarrative looks up a per-page narrative entry in COMPARISON_NARRATIVES keyed by page.slug and throws when no narrative exists. Every comparison page must have authored narrative content (FAQs, body copy, section prose) that this function merges in; a page without one cannot be rendered.","triggerScenarios":"Adding a new comparison page slug to the pages list in scripts/build-comparison-pages.mjs without adding a matching entry to the COMPARISON_NARRATIVES map, then running the build.","commonSituations":"Registering a new 'x-vs-y' comparison page but forgetting the narrative authoring step; renaming a slug so it no longer matches its narrative key; typo in the slug key.","solutions":["Add a COMPARISON_NARRATIVES entry keyed by the exact page.slug value.","If the slug was renamed, update the narrative map key to the new slug.","Check for typos between the page definition slug and the narrative key.","Consider a fallback or explicit list of missing slugs so the error names all offenders at once."],"exampleFix":"// before\nconst COMPARISON_NARRATIVES = { 'worldmonitor-vs-flightradar24': { ... } };\n// after\nconst COMPARISON_NARRATIVES = {\n  'worldmonitor-vs-flightradar24': { ... },\n  'worldmonitor-vs-marinetraffic': { faqs: [], whyWeWinBody: [...], ... },\n};","handlingStrategy":"validation","validationCode":"const missing = PAGES.filter((p) => !COMPARISON_NARRATIVES[p.slug]);\nif (missing.length) throw new Error('Missing narratives for: ' + missing.map((p) => p.slug).join(', '));","typeGuard":"const hasNarrative = (page) => Object.prototype.hasOwnProperty.call(COMPARISON_NARRATIVES, page.slug);","tryCatchPattern":"try {\n  pages = pages.map(applyComparisonNarrative);\n} catch (e) {\n  if (String(e.message).startsWith('Missing comparison narrative')) {\n    console.error('Add a COMPARISON_NARRATIVES entry for this slug:', e.message);\n    process.exitCode = 1;\n  } else throw e;\n}","preventionTips":["Keep slugs and narrative keys in one shared constant list so they cannot drift.","Add a CI check that fails early listing ALL slugs without narratives.","Copy an existing narrative entry when adding a new comparison page."],"tags":["build-time","content-validation","missing-content","keyerror"],"backgroundTag":"record-not-found","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"}