koala73/worldmonitor · error · Error

Report slug/id must match ${SLUG_PATTERN}: ${report.slug} /

Error message

Report slug/id must match ${SLUG_PATTERN}: ${report.slug} / ${report.id}

What it means

The research-reports builder validates each report's slug and id against /^[a-z0-9][a-z0-9-]*$/ before rendering pages, since both become URL path segments and file names. A slug/id that is empty, has uppercase, underscores, spaces, or starts with a hyphen throws this error naming the offending values.

Solutions

  1. Rewrite report.slug to lowercase alphanumeric words separated by hyphens, not starting with a hyphen.
  2. Apply the same pattern to report.id.
  3. Add a slugify helper at data-entry time so new reports are normalized automatically.
  4. Re-run the reports build to confirm validation passes.

Example fix

// before
{ id: 'Chokepoint_01', slug: 'Strait of Hormuz' }
// after
{ id: 'chokepoint-01', slug: 'strait-of-hormuz' }
Defensive patterns

Strategy: validation

Validate before calling

const SLUG = /^[a-z0-9][a-z0-9-]*$/;
if (!SLUG.test(report.slug) || !SLUG.test(report.id)) throw new Error('report slug/id must match ' + SLUG);

Prevention

When it happens

Trigger: Adding or editing a research report entry whose slug or id violates the pattern — e.g. slug 'Q1_2026_Report', an id with uppercase, or an empty string from missing front-matter — then running the reports build.

Common situations: Hand-authored report metadata with human-friendly titles pasted into slug fields; renamed reports using underscores; automated imports that don't slugify; empty ids from template scaffolds.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/54b7b30cf3272126. Report an issue: GitHub.

Appendix: source

Thrown at scripts/build-research-reports.mjs:710

function trackedLink(href, text, target, escapeHtml, extraAttrs = '') {
  return `<a href="${escapeHtml(href)}" data-umami-event="research-cta" data-umami-event-target="${escapeHtml(target)}"${extraAttrs}>${text}</a>`;
}

export function renderResearchReportPage({
  report,
  snapshot,
  metrics,
  tpl,
  baseUrl,
  lastmod,
  chokepointSlugById,
  dataCatalog,
  includedInDataCatalog,
}) {
  const { escapeHtml, absoluteUrl, breadcrumbLd, withUtmSource, pageDocument } = tpl;
  const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
  if (!SLUG_PATTERN.test(report.slug) || !SLUG_PATTERN.test(report.id)) {
    throw new Error(`Report slug/id must match ${SLUG_PATTERN}: ${report.slug} / ${report.id}`);
  }
  for (const [role, ids] of [
    ['focusChokepointId', [report.focusChokepointId]],
    ['contextChokepointId', report.contextChokepointIds],
  ]) {
    for (const id of ids) {
      if (!SLUG_PATTERN.test(chokepointSlugById.get(id) ?? '')) {
        throw new Error(`Report ${report.id}: ${role} ${id} has no valid route in the chokepoint registry`);
      }
    }
  }
  const chokepointSlug = chokepointSlugById.get(report.focusChokepointId);
  const path = `/research/${report.slug}/`;
  const canonical = absoluteUrl(baseUrl, path);
  const focus = snapshot.chokepoints[report.focusChokepointId];
  const resolve = (text) => resolveMetricTokens(escapeHtml(text), metrics, escapeHtml);
  const m = (id) => {
    const metric = metrics.get(id);

View on GitHub (pinned to 7d06c8633d)