santifer/career-ops · error · Error
joinup: ${entry.name} page is missing __NEXT_DATA__ (structu
Error message
joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?) What it means
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.
Source
Thrown at providers/joinup.mjs:52
/** @type {Provider} */
export default {
id: 'joinup',
detect(entry) {
let host;
try { host = new URL(entry.careers_url || '').hostname; } catch { return null; }
return /(^|\.)joinup\.ch$/i.test(host) ? { url: BROWSE_URL } : null;
},
async fetch(entry, ctx) {
// redirect:'error' — BROWSE_URL is pinned to joinup.ch (https); a 3xx must
// not be followed to a private/metadata IP (matches every other provider).
const html = await ctx.fetchText(BROWSE_URL, { redirect: 'error' });
const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
// Fail closed: a missing/unparseable __NEXT_DATA__ is a scraper break, not an
// empty board — throw so the scan logs it instead of silently reporting zero.
if (!m) throw new Error(`joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?)`);
let hits = [];
try {
const data = JSON.parse(m[1]);
const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;
hits = Array.isArray(ir) && ir[0]?.hits ? ir[0].hits : [];
} catch (err) {
throw new Error(`joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}`);
}
return hits
.filter(h => h && h.slug && (h.title || h.headline))
.map(h => ({
title: h.title || h.headline || '',
url: `https://joinup.ch/job/${h.slug}`,
company: h.startup || entry.name || '',
location: typeof h.location === 'string' ? h.location
: (h.location?.name || h.location?.city || ''),
postedAt: toEpochMs(h.created),
}));View on GitHub (pinned to 9b17a8ac97)
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.
Defensive patterns
Strategy: try-catch
Validate before calling
async function joinupHasNextData(fetchText, browseUrl) {
const html = await fetchText(browseUrl, { redirect: 'error' });
return /<script id="__NEXT_DATA__"[^>]*>[\s\S]*?<\/script>/.test(html);
} Try / catch
try {
jobs = await provider.fetch(entry, ctx);
} catch (err) {
if (/missing __NEXT_DATA__/.test(err.message)) {
console.error(`joinup ${entry.name}: board missing __NEXT_DATA__ — redesign or block`);
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- join: __NEXT_DATA__ not found or unexpected structure
- joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.
- API error: ${json.errorMsg || json.errorCode || 'success=fal
- jobvite: could not find companyEId on ${boardUrl} for ${entr
- justjoin: unexpected API response — expected { data: [...] }
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/8251eda324c64758.
Report an issue: GitHub.