santifer/career-ops · error · Error
careerviet: ${url} still contains job cards but none could b
Error message
careerviet: ${url} still contains job cards but none could be parsed — the listing markup changed What it means
careerviet's scraper fetched a listing page whose HTML still contains job-card DOM markers (`id="job-item-..."`) but `parseListingPage` extracted zero jobs. The library throws instead of returning [] because an empty result would be indistinguishable from a healthy board with no openings, hiding a real scraping failure. It means the board's markup changed such that the card-window detection still matches but the inner title/href extraction regexes no longer do.
Source
Thrown at providers/careerviet.mjs:284
});
}
return [...byId.values()];
}
/**
* A listing page that parses to nothing is either a markup change or a block
* — both are failures, and both must be reported. Returning [] would show up
* as a board with no openings, indistinguishable from a healthy quiet board.
*
* The emptiness test is the card-marker SHAPE (a job-item DOM id) rather than
* a marker word, so it survives the board's own "no results" copy changing.
* @param {string} html
* @param {string} url
*/
export function assertParsedSomething(html, url) {
if (!/id=["']job-item-[A-Za-z0-9]+["']/.test(String(html ?? ''))) return;
throw new Error(
`careerviet: ${url} still contains job cards but none could be parsed — the listing markup changed`,
);
}
/** @type {Provider} */
export default {
id: 'careerviet',
detect(entry) {
return entry?.provider === 'careerviet' ? { url: buildListUrl(entry, 1) } : null;
},
async fetch(entry, ctx) {
// `max_pages` on the portals entry is the user's setting; `ctx.maxPages` is a
// caller-side bound — verify-portals' health probe passes 1. Same shape as itviec.mjs.
const entryMaxPages = Number.isInteger(entry?.max_pages) && entry.max_pages > 0
? Math.min(entry.max_pages, MAX_PAGES_CAP)
: DEFAULT_MAX_PAGES;View on GitHub (pinned to 1696bec4d0)
Solutions
- Inspect the live listing HTML at the thrown URL and update the card/title/href regexes in providers/careerviet.mjs to the new markup.
- Confirm whether the page is a real listing (not a bot-block or ad-only page) by opening the URL in a browser; if blocked, address the block.
- Update or pin the library to a version with current careerviet selectors; file/await an upstream fix for the markup change.
- If the board is no longer needed, disable the careerviet entry in portals.yml.
Example fix
// providers/careerviet.mjs — markup redesign moved the title anchor // before const TITLE_LINK_RE = /<a[^>]+class="[^"]*job-title[^"]*"[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>/s; // after const TITLE_LINK_RE = /<a[^>]+data-qa="job-item-link"[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>/s;
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the listing HTML before running the provider
const res = await fetch('https://www.careerviet.vn/viec-lam-it-trang-1-vi.html');
const html = await res.text();
if (/id=["']job-item-[A-Za-z0-9]+["']/.test(html)) {
console.log('board serves job-card markup — provider selectors may be stale if this scan fails');
} Try / catch
try {
await runScan('careerviet');
} catch (e) {
if (e.message.includes('listing markup changed')) {
// stale selectors, not an empty board — alert, don't record 0 openings
reportScrapeDrift('careerviet', e.message);
} else throw e;
} Prevention
- Pin the provider to a maintained version and watch for careerviet markup-change releases.
- Alert on this error specifically — it means 0 jobs recorded, which silently empties the board if swallowed.
- Periodically spot-check the live listing page against the provider's selectors.
- Never treat an empty careerviet result as 'no openings' without checking for this error upstream.
When it happens
Trigger: Provider scan of a careerviet.vn listing URL where: (1) the board changed the anchor class/structure inside job-item cards so TITLE_LINK_RE no longer matches; (2) cards now resolve hrefs to a non-https or non-careerviet.vn host so every card is skipped; (3) titles or hrefs are empty for all cards. assertParsedSomething(html, url) fires whenever the job-item marker shape exists but parseListingPage returned nothing.
Common situations: Careerviet deployed a frontend redesign; an ad slot or partner card reuses the job-item id shape while real postings moved to new markup; a CDN serves a stale/partial page; the scraper version lags behind a site update.
Related errors
- Extracted text too short (likely blocked or empty)
- Could not fetch job page: ${e.message}
- [fetch] Playwright error: ${e.message} — falling back to pla
- [fetch] Playwright unavailable — falling back to plain fetch
- bamboohr: untrusted hostname "${parsed.hostname}" — must mat
AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01).
Data as JSON: /api/errors/5bb361159fea50b4.
Report an issue: GitHub.