{"record":{"id":"6255ef8bb5e63bf0","repo":"santifer/career-ops","slug":"itviec-url-still-contains-job-cards-but-none-c","errorCode":null,"errorMessage":"itviec: ${url} still contains job cards but none could be parsed — the listing markup changed","messagePattern":"itviec: (.+?) still contains job cards but none could be parsed — the listing markup changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/itviec.mjs","lineNumber":253,"sourceCode":"\n/**\n * A listing page that parses to nothing is either a markup change or a block —\n * both are failures, and both must be reported. Returning [] would show up as a\n * board with no openings, which is indistinguishable from a healthy quiet board\n * and is the failure mode that makes scrapers untrustworthy.\n *\n * The emptiness test is the card-marker SHAPE rather than a marker word: if the\n * page still carries card slug attributes and the parser found none, the parser\n * is what broke.\n * @param {string} html\n * @param {string} url\n */\nexport function assertParsedSomething(html, url) {\n  // The numeric suffix check is deliberately loose (\\d+, not \\d{4}): this is a\n  // heuristic that answers \"is this still a listing page\", so it must err\n  // toward throwing rather than toward silence.\n  if (!/href=[\"'](?:https?:\\/\\/itviec\\.com)?\\/it-jobs\\/[a-z0-9-]+-\\d+/i.test(String(html ?? ''))) return;\n  throw new Error(\n    `itviec: ${url} still contains job cards but none could be parsed — the listing markup changed`,\n  );\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'itviec',\n\n  detect(entry) {\n    return entry?.provider === 'itviec' ? { url: buildListUrl(entry, 1) } : null;\n  },\n\n  async fetch(entry, ctx) {\n    // `max_pages` on the portals entry is the user's setting; `ctx.maxPages` is a\n    // caller-side bound — verify-portals' health probe passes 1. Same shape as senjob.mjs.\n    const entryMaxPages = Number.isInteger(entry?.max_pages) && entry.max_pages > 0\n      ? Math.min(entry.max_pages, MAX_PAGES_CAP)\n      : DEFAULT_MAX_PAGES;","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/itviec.mjs#L235-L271","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fetch the listing page, inspect a job card's current markup, and update the parser's selectors to match","Loosen overly strict selectors (the link regex is intentionally broad, so the parser is usually the stricter part)","Add/update unit tests with a saved copy of the new HTML before redeploying","Skip itviec in portals.yml temporarily and report the provider breakage"],"exampleFix":"// before\nconst cards = html.querySelectorAll('.job-item-2.search__job');\n// after\nconst cards = html.querySelectorAll('.job-item-2, [data-search-result]');","handlingStrategy":"try-catch","validationCode":"function listingLooksParseable(html) {\n  const text = String(html ?? '');\n  const hasCards = /href=[\"'](?:https?:\\/\\/itviec\\.com)?\\/it-jobs\\/[a-z0-9-]+-\\d+/i.test(text);\n  return !hasCards; // true = nothing to parse; false = cards present, parser must succeed\n}\n// call assertParsedSomething in a probe before a full scan run","typeGuard":"const pageHasJobCards = (html) =>\n  /href=[\"'](?:https?:\\/\\/itviec\\.com)?\\/it-jobs\\/[a-z0-9-]+-\\d+/i.test(String(html ?? ''));","tryCatchPattern":"try {\n  assertParsedSomething(html, url);\n} catch (err) {\n  if (/still contains job cards/.test(err.message)) {\n    console.error('itviec markup changed; update parser selectors. Page saved for inspection.');\n    fs.writeFileSync(`itviec-breakage-${Date.now()}.html`, String(html));\n    return [];\n  }\n  throw err;\n}","preventionTips":["Keep saved HTML fixtures of itviec listing pages in CI and run the parser against them","Prefer broad structural selectors over brittle class names","Alert on zero-result boards instead of treating them as quiet days","Re-check the parser after any itviec redesign or A/B rollout"],"tags":["scraping","html-parsing","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"}