{"record":{"id":"e8971aaae5887763","repo":"santifer/career-ops","slug":"softgarden-cannot-resolve-widget-url-for-entry","errorCode":null,"errorMessage":"softgarden: cannot resolve widget URL for ${entry.name}","messagePattern":"softgarden: cannot resolve widget URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/softgarden.mjs","lineNumber":146,"sourceCode":"      postedAt: parseSoftgardenDate(dateM ? clean(dateM[1]) : undefined),\n    });\n  }\n  return out;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'softgarden',\n\n  detect(entry) {\n    const url = entry.api || entry.careers_url || '';\n    if (typeof url !== 'string') return null;\n    return resolveWidgetUrl({ api: url }) ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const widgetUrl = resolveWidgetUrl(entry);\n    if (!widgetUrl) throw new Error(`softgarden: cannot resolve widget URL for ${entry.name}`);\n\n    const html = await ctx.fetchText(widgetUrl, { headers: { accept: 'text/html' } });\n    const rows = parseWidget(html, widgetUrl);\n    const jobs = [];\n    for (const row of rows) {\n      const job = { title: row.title, url: row.url, company: entry.name, location: row.location };\n      if (typeof row.postedAt === 'number') job.postedAt = row.postedAt;\n      jobs.push(job);\n      if (jobs.length >= MAX_JOBS) break;\n    }\n    return jobs;\n  },\n};\n","sourceCodeStart":128,"sourceCodeEnd":160,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/softgarden.mjs#L128-L160","documentation":"resolveWidgetUrl requires an http(s) URL whose host is exactly 'softgarden.io' or ends with '.softgarden.io'; it then derives the server-rendered widget page at {origin}/{lang}/widgets/jobs. If neither entry.api nor entry.careers_url satisfies the host check it returns null and fetch throws. The provider does not call an API — it scrapes that widget HTML directly.","triggerScenarios":"entry.careers_url points at the company's branded site rather than the {tenant}.softgarden.io host; the URL is malformed (new URL throws); or the protocol is neither http: nor https:.","commonSituations":"A company embeds the softgarden widget in an iframe on its own domain, and the entry stores the iframe parent URL instead of the softgarden tenant host. Also when an entry is force-tagged provider: softgarden without a softgarden host.","solutions":["Set careers_url (or api) to the softgarden tenant host, e.g. https://renk-group.softgarden.io","If only the branded site is known, open it, find the iframe src pointing at *.softgarden.io, and use that host","Ensure the host ends in .softgarden.io (the bare apex softgarden.io is also accepted)","If the company is not on softgarden, switch the entry to the correct provider"],"exampleFix":"# before\n- name: RENK Group\n  provider: softgarden\n  careers_url: https://karriere.renk.group\n# after\n- name: RENK Group\n  provider: softgarden\n  careers_url: https://renk-group.softgarden.io","handlingStrategy":"validation","validationCode":"// Pre-check that an entry can resolve a softgarden widget URL.\nimport { resolveWidgetUrl } from './providers/softgarden.mjs';\nif (!resolveWidgetUrl(entry)) {\n  console.warn(`${entry.name}: not a *.softgarden.io host — fix careers_url or switch provider`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run resolveWidgetUrl(entry) during a config-lint step before scanning.","Store the {tenant}.softgarden.io host (from the iframe src) rather than the branded parent URL.","Validate that every provider: softgarden entry has a host ending in .softgarden.io."],"tags":["config","softgarden","url-validation","portals-yml","detection"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}