santifer/career-ops · warning
rheinmetall: page 1 returned no vacancy cards for
Error message
rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed What it means
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.
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.
Example fix
// before
const rows = html.querySelectorAll('.job-listing__card'); // old class
// after
const rows = html.querySelectorAll('article.vacancy-card'); // new markup Defensive patterns
Strategy: retry
Validate before calling
const probe = await ctx.fetchText(listUrl, { headers: { accept: 'text/html' } });
if (!probe.includes('vacancy') && !probe.includes('job')) console.warn('page markup unknown — parser likely stale'); Try / catch
const rows = parseVacancies(html, origin);
if (rows.length === 0 && page === 1) {
console.warn('page 1 returned no vacancy cards — markup may have changed');
return; // stop instead of paging a broken parse
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- itviec: still contains job cards but none could be parsed —…
- avature: still contains JobDetail links but no article…
- generalist-world: gw-job-card anchor(s) matched but none…
- generalist-world: no gw-job-card anchors and no listing…
- telegram-channel: @ served text posts but none parsed —…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/94666a695cef6a0f.
Report an issue: GitHub.
Appendix: source
Thrown at providers/rheinmetall.mjs:129
async fetch(entry, ctx) {
const listUrl = resolveListUrl(entry);
if (!listUrl) throw new Error(`rheinmetall: cannot resolve vacancies URL for ${entry.name}`);
const origin = new URL(listUrl).origin;
const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));
const maxPages = resolveMaxPages(entry);
const jobs = [];
const seen = new Set();
for (let page = 1; page <= maxPages; page++) {
if (page > 1) await wait(PAGE_DELAY_MS);
const html = await ctx.fetchText(`${listUrl}?page=${page}`, {
headers: { accept: 'text/html' },
});
const rows = parseVacancies(html, origin);
if (rows.length === 0) {
if (page === 1) console.warn(`rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed`);
break; // past the last page
}
let fresh = 0;
for (const row of rows) {
if (seen.has(row.id)) continue;
seen.add(row.id);
fresh++;
jobs.push({ title: row.title, url: row.url, company: entry.name, location: row.location });
}
// No new ids → the server clamped ?page= to the last page (or looped). Stop.
if (fresh === 0) break;
if (jobs.length >= MAX_JOBS) break;
}
return jobs.slice(0, MAX_JOBS);
},
};
View on GitHub (pinned to aac998c7ed)