Broaden the search beyond engineer/developer titles and stop three
classes of bad result from ever reaching the CSV.
config.ts
- PLATFORM_KEYWORDS (architect, SRE, infra, security, MLOps),
RESEARCH_KEYWORDS (research/scientific software, HPC, data platform)
and FIELD_KEYWORDS (forward-deployed, customer/sales, founding).
Separate OR-strings, not folded into KEYWORDS: LinkedIn ranks a
focused query far better than one 50-term disjunction.
- sweep() generates one search per area per family. 7 searches -> 17.
fetch.ts
- BIO_DOMAIN_RE drops life-science roles (bioinformatics, biostats,
GMP/MSAT, genomics, pathology, drug discovery). Filters on the role,
never the employer: software jobs at Roche/Novartis/sciCORE stay.
- requiresGerman() drops English postings demanding German above B1.
Two rules, both from real false results: the nearest marker wins
("Infrastructure skills are a plus" sat 95 chars before "Good
knowledge of German"), and an intervening language name breaks the
binding ("Fluent in English; German and/or French advantageous").
- GERMAN_TITLE_RE catches "(German-speaking)" without a page fetch.
- isViableContent/isViableUrl run the DE/FR and German-requirement
checks on a single fetch; isEnglishContent/isEnglishUrl kept for
recheck.ts and temp scripts.
- SOFTWARE_TITLE_RE widened so the new families survive it.
index.ts
- both content-check paths use the combined check and report which
filter dropped a job.
linkme
Scrapes LinkedIn job listings into a local CSV. Run it daily, delete rows you've applied to.
Usage
bun fetch # scrape and append new jobs to results/jobs.csv
bun recheck # show jobs currently filtered out + keyword suggestions
No setup needed. Results are appended to results/jobs.csv — reruns are safe, already-seen jobs are skipped by ID.
Structure
config.ts # search keywords and areas
fetch.ts # scraper, HTML parser, filters
fetch-other.ts # extra sources (datacareer.ch, stellen-basel.ch)
index.ts # orchestrator: runs searches, writes CSV
recheck.ts # raw-mode fetch, lists filtered-out jobs
scripts/ # one-off search scripts (temp, not in config.ts)
cv/ # polo.md (profile) + CV files
results/ # jobs.csv output
jobs/ # notes on specific job leads
plans/ # LinkedIn post drafts
Output
results/jobs.csv — one row per job:
| column | description |
|---|---|
date_found |
date you ran the fetch |
posted_at |
date the job was posted |
search |
which search config matched it |
title |
job title |
company |
hiring company |
link |
canonical job URL |
source |
site the job came from |
Open in any spreadsheet app. Delete rows as you apply or dismiss.
How it works
- Paginated scraping — fetches up to 6 pages per search (150 jobs) with a 400 ms delay between pages
- Card-boundary-safe parsing — each job card is isolated before extracting fields
- Language filter — drops German (
m/w/d, umlauts, gender markers) and French (H/F, accents) titles - Software-role filter — requires a relevant term in the title (software, developer, data, AI, backend, frontend, devops, platform, architect, tech lead, typescript, python, rails, node.js, ML, cloud, security, SRE, etc.)
- Seniority filter — drops junior, intern, trainee, apprentice, werkstudent, entry-level
- Global deduplication — IDs indexed from existing CSV on startup; already-seen jobs are never re-added
- 30-day recency — only jobs posted in the last month
recheck
bun recheck fetches all searches in raw mode (bypassing the software-role filter) and lists every job that would be filtered out — useful for spotting false negatives. Also prints a word-frequency table of filtered titles to help decide what to add to SOFTWARE_TITLE_RE.
Temporary searches
Write a throwaway script in scripts/ instead of editing config.ts:
bun run scripts/bern-temp.ts
Scripts in scripts/ import from ../config.ts and ../fetch.ts.
Configure searches
Edit config.ts:
export const KEYWORDS = "Software Engineer OR Data Engineer OR ...";
export const searches: SearchConfig[] = [
{
label: "Basel (city)",
keywords: KEYWORDS,
geoId: "103383283",
location: "Basel",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
];
Known Swiss geoIds:
103383283— Basel (city)90009885— Basel Metropolitan Area107117530— Basel-Country102436504— Zürich (city)107814425— Zürich Metropolitan Area90009888— Zürich Area
You can also paste any LinkedIn search URL directly as url: instead of building from filters.
SearchConfig also supports jobType (full-time, part-time, contract, temporary, internship) and seniority (internship, entry, associate, mid-senior, director, executive).