import { slugify, USER_AGENT } from "./http"; import type { ScrapedJob, Scraper } from "./types"; // Adecco has no public API, but its job search page loads results from this internal JSON endpoint // (a Solr-style query passed as `queryString`). Unknown filter fields are silently ignored by the // API, so results are checked against the requested category. const API_URL = "https://www.adecco.com/api/data/jobs/summarized"; const JOB_PAGE = "https://www.adecco.com/sl-si/prosta-delovna-mesta"; const MAX_PAGES = 50; // 10 jobs per page interface AdeccoJob { jobId: string; jobTitle: string; jobLocation: string | null; cityName: string | null; postedDate: string | null; jobCategoryId: string | null; jobSubCategoryId: string | null; contractTypeTitle: string | null; employmentTypeTitle: string | null; industryTypeTitle: string | null; } interface AdeccoResponse { jobs?: AdeccoJob[]; pagination?: { total: number; nextRange: number }; } /** The site builds "-<location>/<jobid>" URLs; only the job ID matters, the slug is cosmetic. */ function jobUrl(j: AdeccoJob): string { const slug = slugify(`${j.jobTitle}-${j.jobLocation ?? ""}`); return `${JOB_PAGE}/${slug}/${j.jobId.toLowerCase()}`; } /** * Scraper for adecco.com (Slovenia), filtered by job category. * `category` is the CategoryFilter value from the site's URL, e.g. "ADCSICAT014" (IT). */ export function adecco(category: string, label: string): Scraper { return { id: `adecco-si-${category.toLowerCase()}`, name: `Adecco (${label})`, async scrape() { const raw: AdeccoJob[] = []; for (let page = 0, range = 0; page < MAX_PAGES; page++) { const res = await fetch(API_URL, { method: "POST", headers: { "User-Agent": USER_AGENT, "content-type": "text/plain;charset=UTF-8" }, body: JSON.stringify({ queryString: `&fq=JobCategoryId:${category}`, range, siteName: "adecco", brand: "adecco", countryCode: "SI", languageCode: "sl-SI", }), }); if (!res.ok) throw new Error(`POST ${API_URL} -> HTTP ${res.status}`); const data = (await res.json()) as AdeccoResponse; if (!Array.isArray(data.jobs)) throw new Error("unexpected API response (no `jobs` array)"); raw.push(...data.jobs); const total = data.pagination?.total ?? 0; if (data.jobs.length === 0 || raw.length >= total || !data.pagination?.nextRange) break; range = data.pagination.nextRange; } const jobs = raw.filter((j) => j.jobCategoryId === category || j.jobSubCategoryId === category); if (raw.length > 0 && jobs.length === 0) { throw new Error(`category filter ${category} was ignored by the API (query format changed?)`); } return jobs.map((j) => ({ externalId: j.jobId, title: j.jobTitle.trim(), url: jobUrl(j), location: j.cityName || j.jobLocation || undefined, tags: [j.contractTypeTitle, j.employmentTypeTitle, j.industryTypeTitle].filter( (t): t is string => !!t?.trim(), ), postedAt: j.postedDate ?? undefined, })); }, }; }