/** * We Work Remotely — public RSS feed. * * https://weworkremotely.com/remote-jobs.rss * * No auth. Returns all recent listings in a single XML feed. * We filter client-side by matching title + skills + category * against each pipeline search term. * * Title format from WWR: "Company Name: Job Title" */ import type { ExtractorManifest, ExtractorRunResult, } from "@shared/types/extractors"; import type { CreateJobInput } from "@shared/types/jobs"; const RSS_URL = "https://weworkremotely.com/remote-jobs.rss"; interface WwrItem { title?: string; link?: string; guid?: string; description?: string; pubDate?: string; region?: string; country?: string; skills?: string; category?: string; type?: string; logoUrl?: string; } function xmlText(xml: string, tag: string): string | undefined { const pattern = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)`); const match = xml.match(pattern); if (!match?.[1]) return undefined; return ( match[1].replace(//g, "$1").trim() || undefined ); } function parseItems(xml: string): WwrItem[] { const items: WwrItem[] = []; const blocks = xml.match(/([\s\S]*?)<\/item>/g) ?? []; for (const raw of blocks) { const block = raw.replace(/^/, "").replace(/<\/item>$/, ""); const logoMatch = block.match(/media:content\s+url="([^"]+)"/); items.push({ title: xmlText(block, "title"), link: xmlText(block, "link"), guid: xmlText(block, "guid"), description: xmlText(block, "description"), pubDate: xmlText(block, "pubDate"), region: xmlText(block, "region"), country: xmlText(block, "country"), skills: xmlText(block, "skills"), category: xmlText(block, "category"), type: xmlText(block, "type"), logoUrl: logoMatch?.[1], }); } return items; } function parseTitle(raw: string): { employer: string; title: string } { const colonIdx = raw.indexOf(": "); if (colonIdx > 0) { return { employer: raw.slice(0, colonIdx).trim(), title: raw.slice(colonIdx + 2).trim(), }; } return { employer: "Unknown Employer", title: raw.trim() }; } function matchesTerm(item: WwrItem, term: string): boolean { const lower = term.toLowerCase(); if (item.title?.toLowerCase().includes(lower)) return true; if (item.skills?.toLowerCase().includes(lower)) return true; if (item.category?.toLowerCase().includes(lower)) return true; return false; } function decodeHtmlEntities(html: string): string { return html .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); } function mapJob(item: WwrItem): CreateJobInput | null { const jobUrl = item.link || item.guid; if (!jobUrl) return null; const rawTitle = item.title ? decodeHtmlEntities(item.title) : "Unknown Title"; const { employer, title } = parseTitle(rawTitle); const location = [item.region, item.country].filter(Boolean).join(" — ") || "Remote"; return { source: "weworkremotely", sourceJobId: item.guid ?? item.link, title, employer, jobUrl, applicationLink: jobUrl, location, isRemote: true, jobType: item.type || undefined, companyLogo: item.logoUrl, datePosted: item.pubDate, jobDescription: item.description ? decodeHtmlEntities(item.description) : undefined, disciplines: item.skills || undefined, companyIndustry: item.category || undefined, }; } export const manifest: ExtractorManifest = { id: "weworkremotely", displayName: "We Work Remotely", providesSources: ["weworkremotely"], async run(context): Promise { if (context.shouldCancel?.()) return { success: true, jobs: [] }; const maxJobs = context.settings.weworkremotelyMaxJobsPerTerm ? Number.parseInt(context.settings.weworkremotelyMaxJobsPerTerm, 10) : 100; const terms = context.searchTerms.length > 0 ? context.searchTerms : []; context.onProgress?.({ phase: "list", termsProcessed: 0, termsTotal: 1, currentUrl: RSS_URL, detail: "We Work Remotely: fetching RSS feed", }); try { const response = await fetch(RSS_URL, { headers: { Accept: "application/rss+xml, application/xml, text/xml" }, }); if (!response.ok) { throw new Error(`WWR RSS failed with status ${response.status}`); } const xml = await response.text(); const items = parseItems(xml); const seen = new Set(); const out: CreateJobInput[] = []; for (const item of items) { if (out.length >= maxJobs * Math.max(terms.length, 1)) break; if (terms.length > 0 && !terms.some((t) => matchesTerm(item, t))) { continue; } const mapped = mapJob(item); if (!mapped) continue; const key = mapped.sourceJobId || mapped.jobUrl; if (seen.has(key)) continue; seen.add(key); out.push(mapped); } context.onProgress?.({ phase: "list", termsProcessed: 1, termsTotal: 1, currentUrl: RSS_URL, jobPagesProcessed: out.length, detail: `We Work Remotely: ${out.length} matched from ${items.length} total`, }); return { success: true, jobs: out }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { success: false, jobs: [], error: message }; } }, }; export default manifest;