#!/usr/bin/env bun // Ruby/Rails — GLOBAL remote sweep. // Most Rails roles are titled "Backend Engineer", so this runs both a // Ruby-title search and a description-level Ruby check over generic titles, // across every remote market that is realistic for an EU-citizen in Basel. import { NON_ENGLISH_RE, JUNIOR_TITLE_RE } from "../fetch.ts"; const RUBY_KEYWORDS = [ "Ruby on Rails", "Rails Engineer", "Ruby Engineer", "Rails Developer", "Ruby Developer", "Ruby", "Rails", ].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 AGENCY_RE = /\bjobgether\b|\ballps\b/i; 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", }; // geoId 92000000 = "Worldwide" on LinkedIn const MARKETS: { label: string; geoId?: string; location?: string }[] = [ { label: "Worldwide", geoId: "92000000" }, { label: "European Union", location: "European Union" }, { label: "Portugal", location: "Portugal" }, { label: "Germany", location: "Germany" }, { label: "United Kingdom", location: "United Kingdom" }, { label: "Spain", location: "Spain" }, { label: "Netherlands", location: "Netherlands" }, { label: "Switzerland", location: "Switzerland" }, { label: "Brazil", location: "Brazil" }, { label: "United States", location: "United States" }, ]; interface Job { id: string; title: string; company: string; location: string; link: string } function unescape(s: string) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") .replace(/'/g, "'").replace(/"/g, '"').replace(/\s+/g, " ").trim(); } function parseCards(html: string): Job[] { const jobs: Job[] = []; const idRe = /data-entity-urn="urn:li:jobPosting:(\d+)"/g; const pos: { id: string; start: number }[] = []; let m; while ((m = idRe.exec(html)) !== null) pos.push({ id: m[1], start: m.index }); for (let i = 0; i < pos.length; i++) { const { id, start } = pos[i]; const card = html.slice(start, Math.min(pos[i + 1]?.start ?? Infinity, start + 3000)); const titleM = card.match(/base-search-card__title[^>]*>\s*([\s\S]*?)\s*<\/h3>/); if (!titleM) continue; const companyM = card.match(/base-search-card__subtitle[\s\S]{0,300}?]*>\s*([\s\S]*?)\s*<\/a>/); const locM = card.match(/job-search-card__location[^>]*>\s*([^<]+)/); jobs.push({ id, title: unescape(titleM[1].replace(/<[^>]+>/g, "")), company: companyM ? unescape(companyM[1].replace(/<[^>]+>/g, "")) : "", location: locM ? unescape(locM[1]) : "", link: `https://www.linkedin.com/jobs/view/${id}/`, }); } return jobs; } const bodyCache = new Map(); async function fetchBody(id: string): Promise { if (bodyCache.has(id)) return bodyCache.get(id)!; let body = ""; try { const res = await fetch(`https://www.linkedin.com/jobs/view/${id}/`, { headers: HEADERS }); const html = await res.text(); body = (html.match(/show-more-less-html__markup[^"]*"[^>]*>([\s\S]*?)<\/div>/)?.[1] ?? "") .replace(/<[^>]+>/g, " ").replace(/\s+/g, " "); } catch { /* leave empty */ } bodyCache.set(id, body); await Bun.sleep(350); return body; } type Hit = Job & { market: string; via: string; snippet: string }; const hits: Hit[] = []; const seen = new Set(); const checked = new Set(); for (const market of MARKETS) { console.log(`\n[${market.label}]`); for (let page = 0; page < 3; page++) { const params = new URLSearchParams({ keywords: RUBY_KEYWORDS, f_TPR: "r2592000", f_WT: "2", // remote start: String(page * 25), }); if (market.geoId) params.set("geoId", market.geoId); if (market.location) params.set("location", market.location); let cards: Job[] = []; try { const res = await fetch(`https://www.linkedin.com/jobs/search/?${params}`, { headers: HEADERS }); if (!res.ok) { console.log(` page ${page + 1}: HTTP ${res.status}`); break; } cards = parseCards(await res.text()); } catch (e) { console.log(` page ${page + 1}: ${e}`); break; } if (!cards.length) break; let fresh = 0; for (const job of cards) { if (seen.has(job.id) || checked.has(job.id)) continue; if (NON_ENGLISH_RE.test(job.title) || JUNIOR_TITLE_RE.test(job.title)) continue; if (MS_RE.test(job.title) || AGENCY_RE.test(job.company)) continue; checked.add(job.id); fresh++; const titleRuby = RUBY_RE.test(job.title); const body = await fetchBody(job.id); const bodyRuby = RUBY_RE.test(body); if (!titleRuby && !bodyRuby) continue; if (MS_RE.test(body)) continue; const idx = body.search(RUBY_RE); seen.add(job.id); hits.push({ ...job, market: market.label, via: titleRuby ? (bodyRuby ? "title+desc" : "title") : "desc", snippet: idx >= 0 ? body.slice(Math.max(0, idx - 90), idx + 130).trim() : "", }); console.log(` HIT: ${job.title} | ${job.company} | ${job.location}`); } console.log(` page ${page + 1}: ${cards.length} cards, ${fresh} new`); if (fresh === 0) break; await Bun.sleep(400); } } console.log(`\n=== ${hits.length} RUBY/RAILS REMOTE HITS (global) ===\n`); for (const h of hits) { console.log(`${h.title} | ${h.company} | ${h.location} | ${h.market} | via ${h.via}`); console.log(` ${h.link}`); if (h.snippet) console.log(` …${h.snippet}…`); console.log(); }