santifer/career-ops · error · Error

softgarden: cannot resolve widget URL for

Error message

softgarden: cannot resolve widget URL for ${entry.name}

What it means

The softgarden provider's fetch calls resolveWidgetUrl(entry), which must produce a {tenant}.softgarden.io widget URL from entry.api or entry.careers_url. If the configured URL is unparseable, non-HTTP(S), or not on a *.softgarden.io host, fetch cannot proceed and throws this error — the entry is not recognizable as a softgarden tenant.

Solutions

  1. Set api (or careers_url) to the tenant widget URL, e.g. https://renk-group.softgarden.io/de/widgets/jobs — an explicit /widgets/jobs path passes through unchanged
  2. Ensure the host is exactly softgarden.io or ends with .softgarden.io; fix typos/TLD
  3. Use https:// or http:// only — other schemes are rejected by resolveWidgetUrl
  4. If the careers site is branded, keep careers_url for humans and put the softgarden.io widget URL in api

Example fix

// before
- name: RENK
careers_url: 'https://careers.renk-group.com'  # not a softgarden.io host
// after
- name: RENK
api: 'https://renk-group.softgarden.io/de/widgets/jobs'
Defensive patterns

Strategy: validation

Validate before calling

function canResolveSoftgardenWidget(entry) {
  const raw = entry?.api || entry?.careers_url || '';
  try {
    const u = new URL(raw);
    if (u.protocol !== 'https:' && u.protocol !== 'http:') return false;
    const host = u.host.toLowerCase();
    return host === 'softgarden.io' || host.endsWith('.softgarden.io');
  } catch { return false; }
}
if (!canResolveSoftgardenWidget(entry)) console.error(`${entry.name}: api/careers_url must be a *.softgarden.io URL`);

Type guard

const isSoftgardenUrl = (raw) => {
  try {
    const u = new URL(String(raw));
    const host = u.host.toLowerCase();
    return (u.protocol === 'https:' || u.protocol === 'http:') &&
      (host === 'softgarden.io' || host.endsWith('.softgarden.io'));
  } catch { return false; }
};

Try / catch

try {
  await softgardenProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).includes('cannot resolve widget URL')) {
    console.error(`Entry ${entry.name}: set api to https://<tenant>.softgarden.io/<lang>/widgets/jobs`);
  } else throw e;
}

Prevention

When it happens

Trigger: A portals entry with provider: softgarden whose api/careers_url is missing, empty, on a branded domain (e.g. careers.renk-group.com) with no softgarden.io URL, or uses another scheme (ftp:, about:); a typo like renk-group.softgarden.com (wrong TLD).

Common situations: Company moved its careers page to a custom domain while the widget still lives on softgarden.io; misconfigured host TLD; leaving the api field blank expecting detection from another field; pasting the iframe's outer page URL instead of the widget URL (the resolver actually tolerates that, but only if the host is *.softgarden.io).

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/e8971aaae5887763. Report an issue: GitHub.

Appendix: source

Thrown at providers/softgarden.mjs:133

      postedAt: parseSoftgardenDate(dateM ? clean(dateM[1]) : undefined),
    });
  }
  return out;
}

/** @type {Provider} */
export default {
  id: 'softgarden',

  detect(entry) {
    const url = entry.api || entry.careers_url || '';
    if (typeof url !== 'string') return null;
    return resolveWidgetUrl({ api: url }) ? { url } : null;
  },

  async fetch(entry, ctx) {
    const widgetUrl = resolveWidgetUrl(entry);
    if (!widgetUrl) throw new Error(`softgarden: cannot resolve widget URL for ${entry.name}`);

    const html = await ctx.fetchText(widgetUrl, { headers: { accept: 'text/html' } });
    const rows = parseWidget(html, widgetUrl);
    const jobs = [];
    for (const row of rows) {
      const job = { title: row.title, url: row.url, company: entry.name, location: row.location };
      if (typeof row.postedAt === 'number') job.postedAt = row.postedAt;
      jobs.push(job);
      if (jobs.length >= MAX_JOBS) break;
    }
    return jobs;
  },
};

View on GitHub (pinned to aac998c7ed)