// Data source: Wellfound's public SEO landing pages /role/l// and // /role/r/ (remote). Each page embeds a Next.js __NEXT_DATA__ blob whose Apollo // cache holds StartupResult and JobListingSearchResult records, including full // descriptions. Detail pages /jobs/- carry a schema.org JobPosting JSON-LD // block. /search is disallowed by robots.txt, so free-text queries are slugified // into the role path instead. export const BASE = "https://wellfound.com" const UA = "Mozilla/5.0 (compatible; wellfound-cli/1.0)" export function writeError(error: string, code: string): void { process.stderr.write(JSON.stringify({ error, code }) + "\n") } /** Fetch HTML with exponential backoff on 429/5xx. Returns "" on a 404. */ export async function htmlFetch(url: string): Promise { const maxRetries = 6 let delay = 500 for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, { headers: { "User-Agent": UA, Accept: "text/html,application/xhtml+xml", "Accept-Language": "en-US,en;q=0.9" }, redirect: "follow", signal: AbortSignal.timeout(20000), }) if (response.status === 429 || response.status >= 500) { if (attempt === maxRetries) throw new Error(`Request failed: ${response.status} ${response.statusText}`) const jitter = Math.floor(Math.random() * 500) await new Promise((r) => setTimeout(r, delay + jitter)) delay = Math.min(delay * 2, 8000) continue } if (response.status === 404) return "" if (!response.ok) throw new Error(`Request failed: ${response.status} ${response.statusText}`) return response.text() } throw new Error("Request failed after max retries") } export function slugify(s: string): string { return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") } export interface JobCard { id: string title: string company: string | null location: string | null date: string | null url: string description: string | null compensation: string | null remote: boolean | null jobType: string | null companySlug: string | null companySize: string | null ats: string | null } type ApolloRecord = Record /** Pull the Apollo cache out of a role page. Returns null when the page has no data blob. */ export function extractApollo(html: string): Record | null { const m = /