Optius publishes an RSS feed with region/field filters, so no HTML scraping is needed. Also move the duplicated slug helper into http.ts (output unchanged, so existing source IDs and URLs are unaffected). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
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 "<title>-<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,
|
|
}));
|
|
},
|
|
};
|
|
}
|