Files
linkme/scripts/global-remote-rails-lead-temp.ts
rodado fb67c38edd chore: reorganize repo structure and update docs
- move temp scripts to scripts/, cv files to cv/, jobs.csv to results/
- update index.ts and recheck.ts CSV path to results/jobs.csv
- fix imports in scripts/ to use ../config.ts and ../fetch.ts
- update CLAUDE.md and README.md to reflect new structure
- add plans/ with LinkedIn post drafts
- update .gitignore: ignore results/, jobs/, cv binaries, .DS_Store
2026-06-03 16:01:32 +02:00

106 lines
4.3 KiB
TypeScript

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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/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(/<h3 class="base-search-card__title">\s*([\s\S]*?)\s*<\/h3>/);
if (!titleM) continue;
const companyM = card.match(/<h4 class="base-search-card__subtitle">[\s\S]{0,300}?<a[^>]*>\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<string>();
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<string>();
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}`);