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

  1. Inspect the live listing HTML at the thrown URL and update the card/title/href regexes in providers/careerviet.mjs to the new markup.
  2. 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.
  3. Update or pin the library to a version with current careerviet selectors; file/await an upstream fix for the markup change.
  4. 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

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


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/5bb361159fea50b4. Report an issue: GitHub.