santifer/career-ops · error · Error
generalist-world: gw-job-card anchor(s) matched but none…
Error message
generalist-world: ${cards} gw-job-card anchor(s) matched but none carried a usable title, employer and /jobs/{slug}/ link; the card markup likely changed What it means
parseGeneralistWorldJobs throws when gw-job-card anchors were found on the Generalist World board page but every one failed to yield a usable title, employer and /jobs/{slug}/ link. Anchors matching yet all failing means an inner field (title, company or href) moved inside the card — a card redesign — not an empty board. Individual malformed cards among usable ones are simply skipped; only an all-fail page throws.
Solutions
- Fetch the live board page and update TITLE_RE, COMPANY_RE or HREF_RE in providers/generalist-world.mjs to match the new card inner markup
- Diff against a known-good fixture to see which inner field moved, then fix that one regex
- Confirm the page is a real board render and not a bot challenge or partial hydration shell
- Update the test fixtures to the current markup so cardToJob yields jobs again
Example fix
// before: company selector misses moved field const COMPANY_RE = /<span class="gw-card__company">([\s\S]*?)<\/span>/; // after: accept the redesigned wrapper const COMPANY_RE = /<div class="gw-card__employer[^"]*"[^>]*>([\s\S]*?)<\/div>/;
Defensive patterns
Strategy: try-catch
Validate before calling
function willParseGwCards(html) {
const cards = [...String(html).matchAll(/<a class="gw-job-card"[\s\S]*?<\/a>/g)];
return cards.length === 0 || cards.some(c => /\/jobs\/[\w-]+\//.test(c[0]) && /gw-card__title|<h[1-4]/.test(c[0]));
} Type guard
function isGwBoardHtml(html) {
return typeof html === 'string' && /gw-job-card/.test(html);
} Try / catch
try {
const jobs = await gwProvider.fetch(entry, ctx);
} catch (e) {
if (e.message.includes('gw-job-card') && e.message.includes('markup likely changed')) {
alertScrapeRegression('generalist-world'); // card internals moved; refresh regexes/fixtures
} else throw e;
} Prevention
- Keep a live-captured fixture of a populated board page and assert parseGeneralistWorldJobs returns > 0 jobs from it in CI
- When updating any inner-card regex (title/company/href), update the fixture in the same change
- Distinguish all-cards-fail (throw) from some-cards-fail (skip) when interpreting logs — a few skips are normal
- Run board audits periodically so a card redesign is caught within days, not months
When it happens
Trigger: Calling parseGeneralistWorldJobs (or the provider's fetch) on a page where CARDS_RE matches cards > 0 but cardToJob returns null for every anchor — e.g. the title, employer, or /jobs/{slug}/ href selector inside the card changed.
Common situations: Generalist World redesigns the card inner layout; a field like the company label is renamed or moved into a nested element; testing against a stale HTML fixture whose card internals no longer match the regexes; a challenge/partial page renders card shells without real content.
Related errors
- generalist-world: no gw-job-card anchors and no listing…
- avature: still contains JobDetail links but no article…
- itviec: still contains job cards but none could be parsed —…
- careerviet: still contains job cards but none could be…
- join: __NEXT_DATA__ not found or unexpected structure
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-22).
Data as JSON: /api/errors/ad3524a4c6e580de.
Report an issue: GitHub.
Appendix: source
Thrown at providers/generalist-world.mjs:292
if (typeof html !== 'string' || !html.trim()) return [];
const rendered = stripNonRendered(html);
/** @type {Job[]} */
const jobs = [];
const seen = new Set();
let cards = 0;
for (const m of rendered.matchAll(CARDS_RE)) {
cards++;
const job = cardToJob(m[1], m[2]);
if (!job || seen.has(job.url)) continue;
seen.add(job.url);
jobs.push(job);
}
// Anchors that all fail to yield a job mean an inner field moved, not an
// empty board: the listing container is still there, so without this check
// a card redesign would read as zero postings forever. A malformed card
// among usable ones is still just skipped.
if (jobs.length === 0 && cards > 0) {
throw new Error(
`generalist-world: ${cards} gw-job-card anchor(s) matched but none carried a usable title, employer and /jobs/{slug}/ link; the card markup likely changed`,
);
}
if (jobs.length === 0 && !BOARD_MARKER_RE.test(rendered)) {
throw new Error(
'generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed',
);
}
return jobs;
}
/** @param {import('./_types.js').PortalEntry | null | undefined} entry */
function detectGeneralistWorldEntry(entry) {
if (!entry || typeof entry !== 'object') return null;
if (entry.provider === 'generalist-world') return { url: LIST_URL };
if (entry.provider) return null;
for (const value of [entry.api, entry.careers_url]) {View on GitHub (pinned to e7abd431fc)