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 "
-/" 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,
}));
},
};
}