{"record":{"id":"660ab03a53fad9ff","repo":"santifer/career-ops","slug":"join-cannot-extract-slug-from-careers-url","errorCode":null,"errorMessage":"join: cannot extract slug from careers_url","messagePattern":"join: cannot extract slug from careers_url","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/join.mjs","lineNumber":51,"sourceCode":"\nexport function extractNextData(html) {\n  if (typeof html !== 'string') return null;\n  const match = html.match(/<script[^>]+__NEXT_DATA__[^>]*>([\\s\\S]*?)<\\/script>/);\n  if (!match) return null;\n  try { return JSON.parse(match[1]); } catch { return null; }\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'join',\n\n  detect(entry) {\n    return extractSlug(entry.careers_url) ? { url: entry.careers_url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const slug = extractSlug(entry.careers_url);\n    if (!slug) throw new Error('join: cannot extract slug from careers_url');\n\n    const baseUrl = `https://join.com/companies/${slug}`;\n    const allItems = [];\n\n    // redirect:'error' prevents SSRF via server-side redirects; baseUrl is\n    // always reconstructed as https://join.com/... so the host is pinned\n    // regardless of the original careers_url.\n    const firstHtml = await ctx.fetchText(baseUrl, { redirect: 'error' });\n    const firstData = extractNextData(firstHtml);\n    const state = firstData?.props?.pageProps?.initialState;\n    if (!state) throw new Error('join: __NEXT_DATA__ not found or unexpected structure');\n\n    const firstJobs = state.jobs?.items;\n    if (!Array.isArray(firstJobs)) throw new Error('join: __NEXT_DATA__ not found or unexpected structure');\n    allItems.push(...firstJobs);\n\n    // Honor a context page cap — verify-portals' liveness probe sets\n    // `ctx.maxPages: 1` so it only needs to know a board is live, not its","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/join.mjs#L33-L69","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (portals.yml)\nprovider: join\ncareers_url: https://acme.join.com\n// after\nprovider: join\ncareers_url: https://join.com/companies/acme","handlingStrategy":"validation","validationCode":"const m = (entry.careers_url ?? '').match(/^https:\\/\\/join\\.com\\/companies\\/([^/?#]+)/);\nif (!m) throw new Error(`${entry.name}: careers_url must be https://join.com/companies/<slug>`);","typeGuard":"const isJoinBoardUrl = (s) => { try { const u = new URL(s); return u.hostname === 'join.com' && /^\\/companies\\/[^/?#]+/.test(u.pathname); } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('join: cannot extract slug')) {\n    console.error(`${entry.name} is not a join.com board — check provider/careers_url`);\n  }\n}","preventionTips":["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."],"tags":["url-parsing","config","join-com","slug"],"backgroundTag":"invalid-identifier-format","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"}