{"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":"exception","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/join.mjs#L33-L69","documentation":"Thrown by join.fetch() when extractSlug(entry.careers_url) returns null. extractSlug requires hostname to be exactly 'join.com' and the pathname to match /companies/<slug>; anything else yields null. Since detect() also gates on extractSlug, hitting this in fetch() means the entry passed detect by a different path or the careers_url was mutated between detect and fetch.","triggerScenarios":"A careers_url whose host is not exactly join.com (e.g. www.join.com, a CNAME, or a typo like join.companies); a pathname that doesn't start with /companies/; an empty or non-string careers_url; or a URL that fails new URL() parsing.","commonSituations":"Entry copied from another provider with the wrong URL; the company changed its careers URL to a custom domain that proxies join.com; a trailing slash or query string that shifts the pathname match.","solutions":["Set careers_url to the canonical https://join.com/companies/<slug> form.","If the company uses a custom domain that proxies join.com, find the underlying join.com/companies/<slug> URL and use it.","Verify hostname is exactly 'join.com' (no subdomain) and the path starts with /companies/."],"exampleFix":"# before\nacme:\n  provider: join\n  careers_url: https://www.join.com/companies/acme\n\n# after — exact host, canonical path\nacme:\n  provider: join\n  careers_url: https://join.com/companies/acme","handlingStrategy":"type-guard","validationCode":"import { extractSlug } from './providers/join.mjs';\n// detect() already calls extractSlug; mirror it in config validation:\nfor (const e of joinEntries) {\n  if (!extractSlug(e.careers_url)) console.warn(`join entry ${e.name}: careers_url must be https://join.com/companies/<slug>`);\n}","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isJoinCompanyUrl(url) {\n  try {\n    const p = new URL(url);\n    return p.hostname.toLowerCase() === 'join.com' && /^\\/companies\\/[^/?#]+/.test(p.pathname);\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":null,"preventionTips":["Only assign provider: join to entries whose careers_url matches https://join.com/companies/<slug>.","Lint join entries for the exact host (no www) and the /companies/ path prefix.","Run provider.detect() in a dry-run config check to catch mismatches before a real scan."],"tags":["join","url-validation","config","slug-extraction"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}