{"record":{"id":"4be753549bf97178","repo":"santifer/career-ops","slug":"wttj-the-wttj-board-is-global-narrow-it-with-wttj-filters","errorCode":null,"errorMessage":"wttj: the WTTJ board is global — narrow it with `wttj: { filters: \"…\" }` and/or `wttj: { queries: [\"…\"] }`","messagePattern":"wttj: the WTTJ board is global — narrow it with `wttj: (.+?)` and/or `wttj: (.+?)`","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":173,"sourceCode":"      max: max || min,\n      currency: typeof h.salary_currency === 'string' ? h.salary_currency.trim().toUpperCase() : '',\n    };\n  }\n  return job;\n}\n\n/** Resolve config: queries and/or an Algolia filter expression, + per-query hit cap. */\nfunction resolveConfig(entry) {\n  const cfg = entry?.wttj && typeof entry.wttj === 'object' ? entry.wttj : {};\n  const queries = Array.isArray(cfg.queries)\n    ? cfg.queries.filter((q) => typeof q === 'string' && q.trim()).map((q) => q.trim())\n    : [];\n  const filters = typeof cfg.filters === 'string' && cfg.filters.trim() ? cfg.filters.trim() : '';\n  if (filters.length > FILTERS_MAX_LEN) {\n    throw new Error(`wttj: \\`filters\\` is too long (${filters.length} > ${FILTERS_MAX_LEN} chars)`);\n  }\n  if (queries.length === 0 && !filters) {\n    throw new Error(\n      'wttj: the WTTJ board is global — narrow it with `wttj: { filters: \"…\" }` and/or `wttj: { queries: [\"…\"] }`',\n    );\n  }\n  // A filter expression already narrows the board server-side, so the empty query\n  // (\"match everything that passes the filter\") is the useful default. Without a\n  // filter there is nothing to narrow the board, so a query list stays mandatory.\n  const effectiveQueries = queries.length > 0 ? queries : [''];\n  const cap = filters ? FILTERED_MAX_HITS_CAP : MAX_HITS_CAP;\n  const maxHits =\n    Number.isInteger(cfg.max_hits) && cfg.max_hits > 0 ? Math.min(cfg.max_hits, cap) : DEFAULT_MAX_HITS;\n  return { queries: effectiveQueries, filters, maxHits };\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'wttj',\n\n  detect(entry) {","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/wttj.mjs#L155-L191","documentation":"resolveConfig refuses to run a WTTJ scan when the entry defines neither any non-empty wttj.queries nor a wttj.filters expression. Welcometothejungle's board is global, so scanning it unfiltered would return arbitrary worldwide postings; the guard forces the config to narrow the board either server-side (filters) or via search terms (queries). The error text itself documents the exact fix shape.","triggerScenarios":"A portals.yml entry enables the wttj provider but its wttj block is missing, empty, contains only blank strings, or has queries: [] with no filters — e.g. wttj: {} after trimming.","commonSituations":"Adding a WTTJ entry to portals.yml and forgetting the narrowing block; deleting all queries during a cleanup; copy-pasting a template entry with placeholder values that trim to empty; a YAML indentation mistake nesting queries outside the wttj key so they aren't picked up.","solutions":["Add wttj.filters with an expression narrowing the board (e.g. country or jobs_profile slug)","Add wttj.queries with one or more non-empty search terms (e.g. \"senior backend\")","Verify YAML indentation: queries and filters must sit inside the entry's wttj: block, and strings must not be blank","Use both: filters to constrain location/contract and queries for titles"],"exampleFix":"# before\ncareer-ops:\n  enabled: true\n  wttj: {}\n# after\ncareer-ops:\n  enabled: true\n  wttj:\n    filters: \"country_slug=france\"\n    queries: [\"backend engineer\"]","handlingStrategy":"validation","validationCode":"// check the entry before enabling the wttj provider\nconst w = entry?.wttj ?? {};\nconst queries = Array.isArray(w.queries) ? w.queries.filter(q => typeof q === 'string' && q.trim()) : [];\nconst filters = typeof w.filters === 'string' ? w.filters.trim() : '';\nif (queries.length === 0 && !filters) {\n  throw new Error('wttj entry needs wttj.filters and/or non-empty wttj.queries');\n}","typeGuard":"function isNarrowedWttjEntry(entry) {\n  const w = entry?.wttj;\n  if (!w || typeof w !== 'object') return false;\n  const qs = Array.isArray(w.queries) ? w.queries.filter(q => typeof q === 'string' && q.trim()) : [];\n  const f = typeof w.filters === 'string' ? w.filters.trim() : '';\n  return qs.length > 0 || f.length > 0;\n}","tryCatchPattern":"try {\n  await scanWttj(entry);\n} catch (e) {\n  if (e.message.includes('the WTTJ board is global')) {\n    console.warn('Add wttj: { filters: \"…\" } or wttj: { queries: [\"…\"] } to the portals.yml entry.');\n  } else throw e;\n}","preventionTips":["Always ship a wttj block with filters and/or queries when adding a WTTJ board to portals.yml","Watch YAML indentation — queries nested outside wttj: silently become absent","Never leave placeholder strings that trim to empty in queries","Run a config preflight/doctor pass over portals.yml before scanning"],"tags":["config","wttj","portals-yml","missing-config"],"backgroundTag":"missing-required-config","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"}