import { buildUrl, isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE, JAVA_TITLE_RE } from "../fetch.ts"; import type { SearchConfig } from "../config.ts"; const 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", "Accept-Language": "en-US,en;q=0.9", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", }; function clean(raw: string): string { return raw.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim(); } // parseJobs without Switzerland location guard function parseJobsGlobal(html: string): { id: string; title: string; company: string; location: string }[] { const jobs = []; const idRe = /data-entity-urn="urn:li:jobPosting:(\d+)"/g; const positions: { id: string; start: number }[] = []; let m; while ((m = idRe.exec(html)) !== null) positions.push({ id: m[1], start: m.index }); for (let i = 0; i < positions.length; i++) { const { id, start } = positions[i]; const card = html.slice(start, Math.min(positions[i + 1]?.start ?? Infinity, start + 3000)); const titleM = card.match(/

\s*([\s\S]*?)\s*<\/h3>/); if (!titleM) continue; const companyM = card.match(/

[\s\S]{0,300}?]*>\s*([\s\S]*?)\s*<\/a>/); const locM = card.match(/job-search-card__location[^>]*>\s*([^<]+)/); jobs.push({ id, title: clean(titleM[1]), company: companyM ? clean(companyM[1]) : "", location: locM ? locM[1].trim() : "", }); } return jobs; } async function fetchGlobal(config: SearchConfig): Promise<{ id: string; title: string; company: string; location: string }[]> { const baseUrl = buildUrl(config); const all = []; const seen = new Set(); for (let page = 0; page < 6; page++) { if (page > 0) await Bun.sleep(400); const url = `${baseUrl}&start=${page * 25}`; process.stdout.write(` page ${page + 1} … `); const res = await fetch(url, { headers: HEADERS }); if (!res.ok) { console.error(`failed (${res.status})`); break; } const html = await res.text(); const jobs = parseJobsGlobal(html) .filter(j => !NON_ENGLISH_RE.test(j.title)) .filter(j => !JUNIOR_TITLE_RE.test(j.title)) .filter(j => !JAVA_TITLE_RE.test(j.title)); let newCount = 0; for (const j of jobs) { if (!seen.has(j.id)) { seen.add(j.id); all.push(j); newCount++; } } console.log(`${jobs.length} jobs, ${newCount} new`); if (newCount === 0) break; } return all; } const TECH_LEAD_RE = /\b(tech\s*lead|technical\s*lead|lead\s*(engineer|developer|backend|full.?stack)|staff\s*(engineer|developer)|principal\s*(engineer|developer)|engineering\s*lead|head\s*of\s*eng)\b/i; const searches: SearchConfig[] = [ { label: "Global Remote — Rails Tech Lead", keywords: "Ruby on Rails Tech Lead OR Rails Lead Engineer OR Rails Staff Engineer OR Rails Principal Engineer OR Ruby Lead Developer", workType: ["remote"], timePosted: "month", }, { label: "Global Remote — Ruby Lead (broader)", keywords: "Ruby on Rails Lead OR Ruby on Rails Staff OR Ruby on Rails Principal OR Ruby Tech Lead", workType: ["remote"], timePosted: "month", }, ]; type Result = { title: string; company: string; location: string; id: string; link: string }; const results: Result[] = []; const seen = new Set(); for (const config of searches) { console.log(`\n[${config.label}]`); const jobs = await fetchGlobal(config); for (const job of jobs) { if (seen.has(job.id)) continue; if (!TECH_LEAD_RE.test(job.title)) continue; seen.add(job.id); process.stdout.write(` checking: ${job.title} @ ${job.company} (${job.location}) … `); const english = await isEnglishContent(job.id); if (!english) { console.log("dropped (non-English)"); continue; } console.log("ok"); results.push({ title: job.title, company: job.company, location: job.location, id: job.id, link: `https://www.linkedin.com/jobs/view/${job.id}/` }); } } console.log(`\n${results.length} global remote Rails lead jobs.\n`); for (const r of results) console.log(`${r.title} | ${r.company} | ${r.location} | ${r.link}`);