koala73/worldmonitor · error · Error
${chokepoint.id}: editorialLinks requires a canonical blog p
Error message
${chokepoint.id}: editorialLinks requires a canonical blog post path and label: ${link?.href} What it means
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.
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.
Example fix
// before
editorialLinks: [{ href: 'https://worldmonitor.example/blog/old-post', label: '' }]
// after
editorialLinks: [{ href: '/blog/strait-of-hormuz-explainer', label: 'Strait of Hormuz explainer' }] Defensive patterns
Strategy: validation
Validate before calling
const bad = (content[chokepoint.id]?.editorialLinks ?? []).filter(
(l) => !blogPostPaths.has(l?.href) || typeof l.label !== 'string' || !l.label.trim()
);
if (bad.length) throw new Error(`invalid editorialLinks: ${bad.map((l) => l?.href).join(', ')}`); Type guard
const isValidEditorialLink = (link, blogPostPaths) => !!link && blogPostPaths.has(link.href) && typeof link.label === 'string' && link.label.trim().length > 0;
Try / catch
try {
buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths, content });
} catch (err) {
if (err.message.includes('editorialLinks requires a canonical blog post path')) {
console.error(`${err.message}; known paths: ${[...blogPostPaths].join(', ')}`);
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- /accuracy/ meta description must be 155–160 chars (got ${len
- writeAccuracySection requires the canonical DataCatalog iden
- Missing comparison narrative for ' + page.slug
- FAQ count must be 8-12, got ' + faqs.length
- duplicate FAQ question: ' + question
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/e3dd811378cd97f4.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/build-crawlable-corpus.mjs:1376
}))
.filter((entry) => entry.id && entry.displayName)
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
export function buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths = new Set(), content = CHOKEPOINT_CONTENT }) {
const countryByCode = new Map(countries.map((country) => [country.code, country]));
const crisisBySlug = new Map(crises.map((crisis) => [crisis.slug, crisis]));
const byChokepointId = new Map();
const byCountryCode = new Map();
const byCrisisSlug = new Map();
for (const chokepoint of chokepoints) {
const declaration = content[chokepoint.id] || {};
const editorialLinks = declaration.editorialLinks ?? [];
if (!Array.isArray(editorialLinks)) throw new Error(`${chokepoint.id}: editorialLinks must be an array`);
const editorial = new Map();
for (const link of editorialLinks) {
if (!blogPostPaths.has(link?.href) || typeof link.label !== 'string' || !link.label.trim()) {
throw new Error(`${chokepoint.id}: editorialLinks requires a canonical blog post path and label: ${link?.href}`);
}
if (!editorial.has(link.href)) editorial.set(link.href, link);
}
const resolve = (field, targets, inverse) => {
const ids = declaration[field] ?? [];
if (!Array.isArray(ids)) throw new Error(`${chokepoint.id}: ${field} must be an array`);
return [...new Set(ids)].map((id) => {
const target = targets.get(id);
if (!target) throw new Error(`${chokepoint.id}: ${field} contains unknown target ${id}`);
const entries = inverse.get(id) || [];
entries.push(chokepoint);
inverse.set(id, entries);
return target;
});
};
byChokepointId.set(chokepoint.id, {
countries: resolve('countryCodes', countryByCode, byCountryCode),
crises: resolve('crisisSlugs', crisisBySlug, byCrisisSlug),View on GitHub (pinned to 7d06c8633d)