# Changelog of HelloWork Jobs Scraper (`devilscrapes/hellowork-jobs-scraper`) Actor

- **URL**: https://apify.com/devilscrapes/hellowork-jobs-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/hellowork-jobs-scraper.md

## HelloWork Jobs Scraper — Changelog

### 0.1.0 — 2026-10-08

- First working build: the server-rendered search page (`/fr-fr/emploi/recherche.html?k&l&p`), curl-cffi (chrome131),
  selectolax, Pydantic input and rows. No warm-up request and no detail fetch.
- Paging stops on `maxItems`, `ceil(total / 30)` pages, a page with zero cards, or a page with no unseen ids.
- Rows are deduplicated by `jobId` across queries, cities and pages. "Unseen" for the stop rule is per search;
  the run-wide set only gates emission.
- Salary is parsed from the card label: ranges, single values, per-day and per-month rates, decimal amounts.
  `€` maps to `EUR`; undisclosed salary is null.
- `postedAt` is derived from "il y a N ..." text from the run time; "plus de 1 mois" counts as 1 month back.
- `contractTag` mixes remote policy and contract duration ("2 ans"); only text containing "travail" is emitted as
  `remotePolicy`. Durations have no output field.
- A search that fails after retries is isolated; the run exits non-zero only when every search failed and no row
  was written. A parseable page with zero offers succeeds with zero rows.
- When a proxy is configured the exit country is pinned to FR and a 403/429 rotates the session id.
- Pricing: `actor-start` $0.20, `result` $0.002 ($2.00 per 1,000 jobs).

### Fixture provenance

- Captured 2026-10-08 (local, direct): `p1.html` (`k=python&l=Paris`, page 1), `p2.html` (page 2),
  `c1.html` (`k=comptable&l=Lyon`), `empty.html` (`k=zzqxkjwv&l=Paris`). Full pages (~620 KB each) are not committed.
- `tools/trim_fixtures.py` (offline, no network) keeps the original `<title>`, the original
  `button[data-cy=offerNumberButton]`, and a selected subset of real cards (26 per page for p1, p2 and c1; none for
  empty). From each kept card it removes only the hide-offer form and the bookmark turbo-frame, which the parser
  never reads. Totals stay real: 225, 225, 1565 and 0.
- Cards were chosen to cover the traps: empty company (anonymous), no salary, single-value salary, per-day and
  per-month salary, decimal salary, super recruiter, remote tags, duration tags, approximate matches,
  `Île-de-France` without a department, "plus de 1 mois".
- DERIVED (hand-built, not captures): `hw_challenge.DERIVED.html` (no cards, no total button) and
  `hw_sponsored.DERIVED.html` (one real card with `product_list` changed to a sponsored value). Cloud recon should
  confirm the real blocked body and whether a sponsored card exists.
- `make_page()` in `tests/conftest.py` tiles real trimmed cards into 30-card pages with rewritten ids for paging
  tests. It is a test helper only.
