{"record":{"id":"94666a695cef6a0f","repo":"santifer/career-ops","slug":"rheinmetall-page-1-returned-no-vacancy-cards-for","errorCode":null,"errorMessage":"rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed","messagePattern":"rheinmetall: page 1 returned no vacancy cards for (.+?) — markup may have changed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"providers/rheinmetall.mjs","lineNumber":129,"sourceCode":"\n  async fetch(entry, ctx) {\n    const listUrl = resolveListUrl(entry);\n    if (!listUrl) throw new Error(`rheinmetall: cannot resolve vacancies URL for ${entry.name}`);\n    const origin = new URL(listUrl).origin;\n\n    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));\n    const maxPages = resolveMaxPages(entry);\n    const jobs = [];\n    const seen = new Set();\n\n    for (let page = 1; page <= maxPages; page++) {\n      if (page > 1) await wait(PAGE_DELAY_MS);\n      const html = await ctx.fetchText(`${listUrl}?page=${page}`, {\n        headers: { accept: 'text/html' },\n      });\n      const rows = parseVacancies(html, origin);\n      if (rows.length === 0) {\n        if (page === 1) console.warn(`rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed`);\n        break; // past the last page\n      }\n\n      let fresh = 0;\n      for (const row of rows) {\n        if (seen.has(row.id)) continue;\n        seen.add(row.id);\n        fresh++;\n        jobs.push({ title: row.title, url: row.url, company: entry.name, location: row.location });\n      }\n      // No new ids → the server clamped ?page= to the last page (or looped). Stop.\n      if (fresh === 0) break;\n      if (jobs.length >= MAX_JOBS) break;\n    }\n    return jobs.slice(0, MAX_JOBS);\n  },\n};\n","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/rheinmetall.mjs#L111-L147","documentation":"The Rheinmetall provider paginates an HTML careers list and parses vacancy cards from each page. When page 1 yields zero parsed rows, it warns that the site's markup may have changed (the parse selectors no longer match) and stops paginating, since page 1 empty means the parser is broken rather than past the last page.","triggerScenarios":"fetch() fetching `${listUrl}?page=1` returns HTML in which parseVacancies finds no card elements — a CSS/class rename on the careers site, a JS-rendered list replacing server-rendered cards, a bot-challenge page, or an error page returned with 200.","commonSituations":"Rheinmetall redesigns its careers portal; a CDN/WAF starts serving a challenge page; region/locale variant of the listUrl renders different markup; scan.mjs run after a long gap.","solutions":["Open `${origin}${listUrl}` in a browser and inspect the vacancy card markup; update parseVacancies selectors to match the new classes/structure.","Check with curl whether the page requires JS rendering or a bot challenge; if so, switch the provider to a browser-based fetch or the underlying API if one exists.","Confirm the locale/region variant of the URL still serves the same template.","Re-run the scan after fixing to verify rows are parsed again."],"exampleFix":"// before\nconst rows = html.querySelectorAll('.job-listing__card'); // old class\n// after\nconst rows = html.querySelectorAll('article.vacancy-card'); // new markup","handlingStrategy":"retry","validationCode":"const probe = await ctx.fetchText(listUrl, { headers: { accept: 'text/html' } });\nif (!probe.includes('vacancy') && !probe.includes('job')) console.warn('page markup unknown — parser likely stale');","typeGuard":null,"tryCatchPattern":"const rows = parseVacancies(html, origin);\nif (rows.length === 0 && page === 1) {\n  console.warn('page 1 returned no vacancy cards — markup may have changed');\n  return; // stop instead of paging a broken parse\n}","preventionTips":["Re-validate provider selectors after any careers-site redesign notice.","Keep a small fixture of known-good page HTML and unit-test parseVacancies against it.","Watch scan output for this warning — an empty provider silently shrinks coverage."],"tags":["scraping","parser","markup-change","provider"],"backgroundTag":"unexpected-response-shape","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"}