santifer/career-ops · error · Error
join: cannot extract slug from careers_url
Error message
join: cannot extract slug from careers_url
What it means
The join.com provider derives the company slug from entry.careers_url via extractSlug(), which requires hostname join.com and a path of the form /companies/<slug>. fetch() throws this error when the slug comes back null — detect() would normally have filtered the entry out, so hitting it in fetch means the URL changed or detect was bypassed (e.g. explicit provider: join with an incompatible careers_url).
Solutions
- Set careers_url to the canonical https://join.com/companies/<slug> form (find the company's board on join.com).
- Remove `provider: join` if the URL is not a join.com board so the entry falls through to the correct provider's detect().
- If the board lives on a custom domain, join.com provider cannot serve it — switch to the appropriate provider or a browser-based capture.
Example fix
// before (portals.yml) provider: join careers_url: https://acme.join.com // after provider: join careers_url: https://join.com/companies/acme
Defensive patterns
Strategy: validation
Validate before calling
const m = (entry.careers_url ?? '').match(/^https:\/\/join\.com\/companies\/([^/?#]+)/);
if (!m) throw new Error(`${entry.name}: careers_url must be https://join.com/companies/<slug>`); Type guard
const isJoinBoardUrl = (s) => { try { const u = new URL(s); return u.hostname === 'join.com' && /^\/companies\/[^/?#]+/.test(u.pathname); } catch { return false; } }; Try / catch
try {
await provider.fetch(entry, ctx);
} catch (e) {
if (e.message.includes('join: cannot extract slug')) {
console.error(`${entry.name} is not a join.com board — check provider/careers_url`);
}
} Prevention
- Only set provider: join for URLs of the form https://join.com/companies/<slug>.
- Strip locale prefixes and query strings from join.com URLs before saving to config.
- Let detect() auto-select the provider instead of forcing provider: join on uncertain URLs.
When it happens
Trigger: A portals.yml entry with `provider: join` whose careers_url is not https://join.com/companies/<slug> — different host (careers.company.com), different path (/company/x, /en/companies/x), missing path segment, or an unparseable/empty URL.
Common situations: Company uses a join.com vanity subdomain or custom domain instead of join.com/companies/...; localized URL variants with a locale prefix; empty careers_url in the config.
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
- getonbrd: invalid category
- jobvite: invalid URL
- rippling: cannot derive API URL for
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/660ab03a53fad9ff.
Report an issue: GitHub.
Appendix: source
Thrown at providers/join.mjs:51
export function extractNextData(html) {
if (typeof html !== 'string') return null;
const match = html.match(/<script[^>]+__NEXT_DATA__[^>]*>([\s\S]*?)<\/script>/);
if (!match) return null;
try { return JSON.parse(match[1]); } catch { return null; }
}
/** @type {Provider} */
export default {
id: 'join',
detect(entry) {
return extractSlug(entry.careers_url) ? { url: entry.careers_url } : null;
},
async fetch(entry, ctx) {
const slug = extractSlug(entry.careers_url);
if (!slug) throw new Error('join: cannot extract slug from careers_url');
const baseUrl = `https://join.com/companies/${slug}`;
const allItems = [];
// redirect:'error' prevents SSRF via server-side redirects; baseUrl is
// always reconstructed as https://join.com/... so the host is pinned
// regardless of the original careers_url.
const firstHtml = await ctx.fetchText(baseUrl, { redirect: 'error' });
const firstData = extractNextData(firstHtml);
const state = firstData?.props?.pageProps?.initialState;
if (!state) throw new Error('join: __NEXT_DATA__ not found or unexpected structure');
const firstJobs = state.jobs?.items;
if (!Array.isArray(firstJobs)) throw new Error('join: __NEXT_DATA__ not found or unexpected structure');
allItems.push(...firstJobs);
// Honor a context page cap — verify-portals' liveness probe sets
// `ctx.maxPages: 1` so it only needs to know a board is live, not itsView on GitHub (pinned to aac998c7ed)