#!/usr/bin/env bun // Ruby/Rails sweep: Switzerland-wide (onsite+hybrid) + Switzerland remote + EU remote. // Title-keyword search is too narrow for Ruby — most Rails roles are titled // "Backend Engineer" — so this also runs a description-level Ruby check over // generic backend/fullstack titles. import { fetchJobs, isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE } from "../fetch.ts"; import type { SearchConfig } from "../config.ts"; const RUBY_KEYWORDS = [ "Ruby on Rails", "Rails Engineer", "Ruby Engineer", "Rails Developer", "Ruby Developer", "Ruby", "Rails", ].join(" OR "); const BACKEND_KEYWORDS = [ "Backend Engineer", "Full Stack Engineer", "Fullstack Developer", "Senior Software Engineer", "Tech Lead", "Staff Engineer", ].join(" OR "); const RUBY_RE = /\bruby\b|\brails\b|\bsidekiq\b|\bhotwire\b|\bactiverecord\b|\bactive record\b/i; const MS_RE = /\b(\.net|c#|sharepoint|dynamics|sccm|intune)\b/i; const csv = await Bun.file("./results/jobs.csv").text(); const existing = new Set([...csv.matchAll(/"(https?:\/\/[^"]+)"/g)].map((m) => m[1])); const searches: Array = [ { label: "CH — Ruby titles", keywords: RUBY_KEYWORDS, location: "Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" }, { label: "CH remote — Ruby titles", keywords: RUBY_KEYWORDS, location: "Switzerland", workType: ["remote"], timePosted: "month" }, { label: "Basel — Ruby titles", keywords: RUBY_KEYWORDS, geoId: "90009885", location: "Basel Metropolitan Area", workType: ["onsite", "hybrid"], timePosted: "month" }, { label: "Zürich — Ruby titles", keywords: RUBY_KEYWORDS, geoId: "90009888", location: "Zürich Area", workType: ["onsite", "hybrid"], timePosted: "month" }, { label: "CH — backend (desc check)", keywords: BACKEND_KEYWORDS, location: "Switzerland", workType: ["onsite", "hybrid", "remote"], timePosted: "month", descCheck: true }, ]; type Hit = { title: string; company: string; location: string; link: string; label: string; via: string }; const hits: Hit[] = []; const seen = new Set(); for (const config of searches) { console.log(`\n[${config.label}]`); let jobs; try { jobs = await fetchJobs(config, { raw: true }); } catch (e) { console.log(` error: ${e}`); continue; } console.log(` ${jobs.length} raw cards`); for (const j of jobs) { if (seen.has(j.link)) continue; if (NON_ENGLISH_RE.test(j.title) || JUNIOR_TITLE_RE.test(j.title)) continue; if (MS_RE.test(j.title)) continue; const titleRuby = RUBY_RE.test(j.title); if (!titleRuby && !config.descCheck) continue; // Confirm Ruby in the description (and that the posting is in English). let bodyRuby = false; try { const res = await fetch(`https://www.linkedin.com/jobs/view/${j.id}/`, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" }, }); const html = await res.text(); const body = (html.match(/
]*>([\s\S]*?)<\/div>/)?.[1] ?? "") .replace(/<[^>]+>/g, " ").replace(/\s+/g, " "); bodyRuby = RUBY_RE.test(body); await Bun.sleep(350); } catch { /* fall back to the title signal */ } if (!titleRuby && !bodyRuby) continue; if (!(await isEnglishContent(j.id))) { console.log(` dropped (non-English): ${j.title}`); continue; } seen.add(j.link); hits.push({ title: j.title, company: j.company, location: (j as any).location ?? "?", link: j.link, label: config.label, via: titleRuby ? (bodyRuby ? "title+desc" : "title") : "desc", }); console.log(` HIT ${existing.has(j.link) ? "(already in CSV)" : "(NEW)"}: ${j.title} | ${j.company}`); } } console.log(`\n=== ${hits.length} RUBY/RAILS HITS ===\n`); for (const h of hits) { console.log(`${existing.has(h.link) ? "[seen] " : "[NEW] "}${h.title} | ${h.company} | ${h.location} | via ${h.via} | ${h.label}`); console.log(` ${h.link}`); }