santifer/career-ops · error · Error

itviec: still contains job cards but none could be parsed —…

Error message

itviec: ${url} still contains job cards but none could be parsed — the listing markup changed

What it means

assertParsedSomething is itviec's HTML-parser smoke test: if the raw HTML still matches the job-card link pattern (href to /it-jobs/<slug>-<digits>) but the parser extracted nothing, the page is clearly a listing whose markup changed, so it throws rather than silently returning an empty board.

Solutions

  1. Fetch the listing page, inspect a job card's current markup, and update the parser's selectors to match
  2. Loosen overly strict selectors (the link regex is intentionally broad, so the parser is usually the stricter part)
  3. Add/update unit tests with a saved copy of the new HTML before redeploying
  4. Skip itviec in portals.yml temporarily and report the provider breakage

Example fix

// before
const cards = html.querySelectorAll('.job-item-2.search__job');
// after
const cards = html.querySelectorAll('.job-item-2, [data-search-result]');
Defensive patterns

Strategy: try-catch

Validate before calling

function listingLooksParseable(html) {
  const text = String(html ?? '');
  const hasCards = /href=["'](?:https?:\/\/itviec\.com)?\/it-jobs\/[a-z0-9-]+-\d+/i.test(text);
  return !hasCards; // true = nothing to parse; false = cards present, parser must succeed
}
// call assertParsedSomething in a probe before a full scan run

Type guard

const pageHasJobCards = (html) =>
  /href=["'](?:https?:\/\/itviec\.com)?\/it-jobs\/[a-z0-9-]+-\d+/i.test(String(html ?? ''));

Try / catch

try {
  assertParsedSomething(html, url);
} catch (err) {
  if (/still contains job cards/.test(err.message)) {
    console.error('itviec markup changed; update parser selectors. Page saved for inspection.');
    fs.writeFileSync(`itviec-breakage-${Date.now()}.html`, String(html));
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: The itviec listing page contains job-card anchors matching the regex, but the selector/extraction logic produced zero parsed jobs — i.e. CSS class names or DOM structure of the cards changed upstream.

Common situations: Itviec ships a frontend redesign; A/B test serves new markup to some clients; the parser was updated to a stricter selector that no longer matches valid cards.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/6255ef8bb5e63bf0. Report an issue: GitHub.

Appendix: source

Thrown at providers/itviec.mjs:253

/**
 * 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, which is indistinguishable from a healthy quiet board
 * and is the failure mode that makes scrapers untrustworthy.
 *
 * The emptiness test is the card-marker SHAPE rather than a marker word: if the
 * page still carries card slug attributes and the parser found none, the parser
 * is what broke.
 * @param {string} html
 * @param {string} url
 */
export function assertParsedSomething(html, url) {
  // The numeric suffix check is deliberately loose (\d+, not \d{4}): this is a
  // heuristic that answers "is this still a listing page", so it must err
  // toward throwing rather than toward silence.
  if (!/href=["'](?:https?:\/\/itviec\.com)?\/it-jobs\/[a-z0-9-]+-\d+/i.test(String(html ?? ''))) return;
  throw new Error(
    `itviec: ${url} still contains job cards but none could be parsed — the listing markup changed`,
  );
}

/** @type {Provider} */
export default {
  id: 'itviec',

  detect(entry) {
    return entry?.provider === 'itviec' ? { 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 senjob.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 aac998c7ed)