{"record":{"id":"13edd6e846cb26cf","repo":"santifer/career-ops","slug":"collage-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"collage: cannot derive API URL for ${entry.name}","messagePattern":"collage: cannot derive API URL for (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/collage.mjs","lineNumber":101,"sourceCode":"    ].filter(Boolean).join('\\n');\n    const description = [text(j.descriptionPlain), metadata].filter(Boolean).join('\\n\\n');\n    return {\n      title: text(j.title), url, company: companyName,\n      location, description,\n      postedAt: toEpochMs(j.createdDate ?? j.createdAt ?? j.publishedAt),\n    };\n  }).filter(Boolean);\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'collage',\n  detect(entry) {\n    try { const url = resolveApiUrl(entry); return url ? { url } : null; } catch { return null; }\n  },\n  async fetch(entry, ctx) {\n    const apiUrl = resolveApiUrl(entry);\n    if (!apiUrl) throw new Error(`collage: cannot derive API URL for ${entry.name}`);\n    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n    return parseCollageResponse(json, entry.name);\n  },\n};\n","sourceCodeStart":83,"sourceCodeEnd":106,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/collage.mjs#L83-L106","documentation":"The collage provider's fetch() calls resolveApiUrl(entry) to turn a portal entry into a concrete Collage API URL. It accepts an explicit `entry.api` (validated /v1/positions/<address> URL) or derives one from an `entry.careers_url` on secure.collage.co of the form /jobs/<site>. When neither yields a URL, the provider cannot know which tenant to query and throws this error naming the entry.","triggerScenarios":"fetch() is called with a portals.yml entry whose `api` is missing/empty and whose `careers_url` is absent, not https, not on secure.collage.co, not a /jobs/<site> path, or whose last path segment contains a dot — resolveApiUrl returns null.","commonSituations":"Configuring a Collage company using its branded marketing URL (acme.com/careers) instead of the secure.collage.co/jobs/<site> address; typos in careers_url (http instead of https); forgetting the `api:` field entirely; using the Collage admin app URL which has a dotted subdomain segment.","solutions":["Add the full API URL to the entry: `api: https://api.collage.co/v1/positions/<job-site-address>` in portals.yml","Or set `careers_url:` to the public site URL shaped like https://secure.collage.co/jobs/<site-address> (no dots in the last segment)","Confirm detect() recognized the entry — if detect returned null the provider skipped it; fetch() only runs when detection raced or was bypassed","Run the entry through resolveApiUrl's checks manually: https scheme, secure.collage.co host, single-segment /jobs/ path"],"exampleFix":"// before (portals.yml)\n- name: Acme\n  provider: collage\n  careers_url: https://acme.com/careers\n// after\n- name: Acme\n  provider: collage\n  api: https://api.collage.co/v1/positions/acme-jobs\n  # or: careers_url: https://secure.collage.co/jobs/acme-jobs","handlingStrategy":"validation","validationCode":"function hasCollageTarget(entry) {\n  if (typeof entry.api === 'string' && entry.api.trim()) return true;\n  if (typeof entry.careers_url !== 'string') return false;\n  try {\n    const u = new URL(entry.careers_url.trim());\n    return u.protocol === 'https:' && u.hostname === 'secure.collage.co' && /^\\/jobs\\/[^/]+\\/?$/.test(u.pathname);\n  } catch { return false; }\n}\n// run over portals.yml entries before scanning","typeGuard":"function isDerivableCollageEntry(entry) {\n  const site = typeof entry?.careers_url === 'string' ? entry.careers_url.trim() : '';\n  return Boolean(typeof entry?.api === 'string' && entry.api.trim()) || /^https:\\/\\/secure\\.collage\\.co\\/jobs\\/[^/.]+\\/?$/.test(site);\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('collage: cannot derive API URL')) {\n    logger.error({entry: entry.name}, 'collage entry misconfigured: set api: to https://api.collage.co/v1/positions/<site>');\n  } else throw err;\n}","preventionTips":["Always prefer the explicit api: field with the full https://api.collage.co/v1/positions/<address> URL","Validate portals.yml entries at load time with a schema check per provider","Copy the secure.collage.co/jobs/<site> URL directly from the tenant's public jobs page, not their marketing site","Run detect()/dry-run over all entries after every portals.yml edit"],"tags":["config","url","provider","missing-config"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}