{"record":{"id":"e3dd811378cd97f4","repo":"koala73/worldmonitor","slug":"chokepoint-id-editoriallinks-requires-a-canonical-blog-post","errorCode":null,"errorMessage":"${chokepoint.id}: editorialLinks requires a canonical blog post path and label: ${link?.href}","messagePattern":"(.+?): editorialLinks requires a canonical blog post path and label: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/build-crawlable-corpus.mjs","lineNumber":1376,"sourceCode":"    }))\n    .filter((entry) => entry.id && entry.displayName)\n    .sort((a, b) => a.displayName.localeCompare(b.displayName));\n}\n\nexport function buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths = new Set(), content = CHOKEPOINT_CONTENT }) {\n  const countryByCode = new Map(countries.map((country) => [country.code, country]));\n  const crisisBySlug = new Map(crises.map((crisis) => [crisis.slug, crisis]));\n  const byChokepointId = new Map();\n  const byCountryCode = new Map();\n  const byCrisisSlug = new Map();\n  for (const chokepoint of chokepoints) {\n    const declaration = content[chokepoint.id] || {};\n    const editorialLinks = declaration.editorialLinks ?? [];\n    if (!Array.isArray(editorialLinks)) throw new Error(`${chokepoint.id}: editorialLinks must be an array`);\n    const editorial = new Map();\n    for (const link of editorialLinks) {\n      if (!blogPostPaths.has(link?.href) || typeof link.label !== 'string' || !link.label.trim()) {\n        throw new Error(`${chokepoint.id}: editorialLinks requires a canonical blog post path and label: ${link?.href}`);\n      }\n      if (!editorial.has(link.href)) editorial.set(link.href, link);\n    }\n    const resolve = (field, targets, inverse) => {\n      const ids = declaration[field] ?? [];\n      if (!Array.isArray(ids)) throw new Error(`${chokepoint.id}: ${field} must be an array`);\n      return [...new Set(ids)].map((id) => {\n        const target = targets.get(id);\n        if (!target) throw new Error(`${chokepoint.id}: ${field} contains unknown target ${id}`);\n        const entries = inverse.get(id) || [];\n        entries.push(chokepoint);\n        inverse.set(id, entries);\n        return target;\n      });\n    };\n    byChokepointId.set(chokepoint.id, {\n      countries: resolve('countryCodes', countryByCode, byCountryCode),\n      crises: resolve('crisisSlugs', crisisBySlug, byCrisisSlug),","sourceCodeStart":1358,"sourceCodeEnd":1394,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/build-crawlable-corpus.mjs#L1358-L1394","documentation":"During the same chokepoint page-link build, every entry of declaration.editorialLinks must reference a canonical blog post (its href must exist in the blogPostPaths Set) and carry a non-blank string label. This throw fires when any link fails that check — including null/undefined entries, hrefs not in blogPostPaths, or missing/blank labels. It prevents dead or unlabeled editorial links in generated crawlable chokepoint pages.","triggerScenarios":"buildChokepointPageLinks() receives an editorialLinks entry where link?.href is not a member of blogPostPaths (stale path, typo, absolute URL, or null link), or link.label is not a non-whitespace string. Also fires when the caller omits the blogPostPaths option (defaults to an empty Set), making every href fail.","commonSituations":"A blog post was renamed or removed so its path no longer appears in blogPostPaths while a chokepoint still links to it; the label was omitted, set to '', or left as whitespace; buildChokepointPageLinks was called directly in a test without passing blogPostPaths; a contributor pasted a full https:// URL instead of the canonical site path.","solutions":["Correct the href to an existing canonical blog post path present in blogPostPaths, or restore/rename the referenced post.","Add a non-empty trimmed string label to the link entry.","If invoking the builder directly, pass the real blogPostPaths Set derived from the blog corpus instead of the empty default.","If the blog post was intentionally deleted, remove the editorialLinks entry that points at it."],"exampleFix":"// before\neditorialLinks: [{ href: 'https://worldmonitor.example/blog/old-post', label: '' }]\n// after\neditorialLinks: [{ href: '/blog/strait-of-hormuz-explainer', label: 'Strait of Hormuz explainer' }]","handlingStrategy":"validation","validationCode":"const bad = (content[chokepoint.id]?.editorialLinks ?? []).filter(\n  (l) => !blogPostPaths.has(l?.href) || typeof l.label !== 'string' || !l.label.trim()\n);\nif (bad.length) throw new Error(`invalid editorialLinks: ${bad.map((l) => l?.href).join(', ')}`);","typeGuard":"const isValidEditorialLink = (link, blogPostPaths) =>\n  !!link && blogPostPaths.has(link.href) && typeof link.label === 'string' && link.label.trim().length > 0;","tryCatchPattern":"try {\n  buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths, content });\n} catch (err) {\n  if (err.message.includes('editorialLinks requires a canonical blog post path')) {\n    console.error(`${err.message}; known paths: ${[...blogPostPaths].join(', ')}`);\n  }\n  throw err;\n}","preventionTips":["Generate blogPostPaths from the same source of truth as the blog routes.","Use canonical site paths only — no absolute URLs or trailing-slash variants.","Require a meaningful trimmed label on every editorial link.","When deleting or renaming a blog post, grep declarations for links to its path."],"tags":["build-time","link-validation","content"],"backgroundTag":"resource-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"}