koala73/worldmonitor · error · Error
${chokepoint.id}: editorialLinks must be an array
Error message
${chokepoint.id}: editorialLinks must be an array What it means
buildChokepointPageLinks() in the crawlable-corpus build script reads per-chokepoint content declarations (default: the CHOKEPOINT_CONTENT map) and expects declaration.editorialLinks to be an array of {href,label} objects. This throw is a build-time schema validation: if editorialLinks is present but not an array (e.g. an object, string, or number), the script refuses to continue so no corrupt chokepoint pages are rendered. An absent, undefined, or null key defaults to [] via ??, so only a non-nullish non-array value triggers this.
Solutions
- Open the offending chokepoint declaration in the content source (CHOKEPOINT_CONTENT in scripts/build-crawlable-corpus.mjs or the content option passed by the caller) and wrap the value in an array.
- If content is produced by another tool, inspect and fix the producer to emit an array of {href,label}.
- Add a schema/lint step over the content declarations so shape drift fails before the corpus build.
Example fix
// before
editorialLinks: { href: '/blog/strait-of-hormuz', label: 'Hormuz explainer' }
// after
editorialLinks: [{ href: '/blog/strait-of-hormuz', label: 'Hormuz explainer' }] Defensive patterns
Strategy: validation
Validate before calling
const links = content[chokepoint.id]?.editorialLinks;
if (links != null && !Array.isArray(links)) {
throw new TypeError(`${chokepoint.id}: editorialLinks must be an array, got ${typeof links}`);
} Type guard
const isValidEditorialLinks = (v) => v == null || (Array.isArray(v) && v.every((l) => !!l && typeof l.href === 'string' && typeof l.label === 'string'));
Prevention
- Always declare editorialLinks as an array, even for a single link.
- Run the corpus build locally before pushing content edits.
- Add a JSON schema or JSDoc @type check over CHOKEPOINT_CONTENT-style declarations in CI.
When it happens
Trigger: Calling buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths, content }) where content[chokepoint.id].editorialLinks is set to a truthy non-array value such as an object, string, number, or boolean. Undefined and null do not trigger it (they fall back to []).
Common situations: A contributor writes editorialLinks as a single object instead of an array of one link; a merge or codegen tool flattens a single-element array into a scalar; a content-module refactor changes the declared shape; an external CMS export emits the wrong JSON type.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- ${chokepoint.id}: ${field} must be an array
- Acquisition provider '${name}' is not configured. Set the re
- EXA_API_KEY is required for exa-search adapter
- Exa search failed HTTP ${resp.status}: ${text.slice(0, 120)}
- Generic adapter requires acquisition config (retailer: ${ctx
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/9c598b0e66011bc0.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/build-crawlable-corpus.mjs:1372
routeIds: Array.isArray(entry.routeIds) ? [...entry.routeIds] : [],
lat: Number(entry.lat),
lon: Number(entry.lon),
slug: slugify(entry.displayName || entry.id),
}))
.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;
});View on GitHub (pinned to 7d06c8633d)