{"record":{"id":"8251eda324c64758","repo":"santifer/career-ops","slug":"joinup-entry-name-page-is-missing-next-data","errorCode":null,"errorMessage":"joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?)","messagePattern":"joinup: (.+?) page is missing __NEXT_DATA__ \\(structure changed\\?\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/joinup.mjs","lineNumber":52,"sourceCode":"\n/** @type {Provider} */\nexport default {\n  id: 'joinup',\n\n  detect(entry) {\n    let host;\n    try { host = new URL(entry.careers_url || '').hostname; } catch { return null; }\n    return /(^|\\.)joinup\\.ch$/i.test(host) ? { url: BROWSE_URL } : null;\n  },\n\n  async fetch(entry, ctx) {\n    // redirect:'error' — BROWSE_URL is pinned to joinup.ch (https); a 3xx must\n    // not be followed to a private/metadata IP (matches every other provider).\n    const html = await ctx.fetchText(BROWSE_URL, { redirect: 'error' });\n    const m = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>([\\s\\S]*?)<\\/script>/);\n    // Fail closed: a missing/unparseable __NEXT_DATA__ is a scraper break, not an\n    // empty board — throw so the scan logs it instead of silently reporting zero.\n    if (!m) throw new Error(`joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?)`);\n    let hits = [];\n    try {\n      const data = JSON.parse(m[1]);\n      const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;\n      hits = Array.isArray(ir) && ir[0]?.hits ? ir[0].hits : [];\n    } catch (err) {\n      throw new Error(`joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}`);\n    }\n    return hits\n      .filter(h => h && h.slug && (h.title || h.headline))\n      .map(h => ({\n        title: h.title || h.headline || '',\n        url: `https://joinup.ch/job/${h.slug}`,\n        company: h.startup || entry.name || '',\n        location: typeof h.location === 'string' ? h.location\n          : (h.location?.name || h.location?.city || ''),\n        postedAt: toEpochMs(h.created),\n      }));","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/joinup.mjs#L34-L70","documentation":"Thrown by joinup.fetch() when the fetched joinup.ch browse page HTML has no <script id=\"__NEXT_DATA__\"> tag matching the provider's regex. Like the join.com provider, joinup scrapes server-rendered Next.js data; a missing tag is treated as a scraper break (board redesign, error page, or bot challenge) and fails closed so the scan logs it instead of silently reporting zero jobs.","triggerScenarios":"joinup.ch redesigned its page and removed or renamed the __NEXT_DATA__ script; the request returned a non-board HTML (403/429/500, Cloudflare interstitial, maintenance page); the BROWSE_URL constant is stale and points at a retired route.","commonSituations":"joinup.ch shipped a frontend change; rate-limiting or geo-blocking returned a challenge page instead of the board; the site moved to a different rendering strategy (no SSR Next.js).","solutions":["Open the BROWSE_URL in a browser and confirm a __NEXT_DATA__ script tag is present.","If the page is a bot challenge, slow down scans or route through a residential context.","If joinup.ch redesigned, update the regex / data-access path in providers/joinup.mjs to match the new structure."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"async function joinupHasNextData(fetchText, browseUrl) {\n  const html = await fetchText(browseUrl, { redirect: 'error' });\n  return /<script id=\"__NEXT_DATA__\"[^>]*>[\\s\\S]*?<\\/script>/.test(html);\n}","typeGuard":null,"tryCatchPattern":"try {\n  jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/missing __NEXT_DATA__/.test(err.message)) {\n    console.error(`joinup ${entry.name}: board missing __NEXT_DATA__ — redesign or block`);\n  }\n  throw err;\n}","preventionTips":["Monitor joinup.ch for frontend changes; SSR scrapers break silently on redesign.","Keep a fixture of a known-good joinup __NEXT_DATA__ payload in tests.","If joinup.ch adds bot protection, slow scan cadence or route through a residential context."],"tags":["joinup","nextjs","scraping","structure-change","fail-closed"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}