feat(qajobsboard): fetch job detail pages for concrete location text
CI / Linting (Biome) (push) Failing after 40s
CI / Tests (push) Successful in 5m13s
CI / Type Check (adzuna-extractor) (push) Successful in 1m10s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m14s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m11s
CI / Type Check (orchestrator) (push) Successful in 1m30s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m12s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m13s
CI / Documentation (push) Successful in 1m57s

The jobs.json feed often labels roles Remote/Worldwide while the public
job page JSON-LD and description include the real city (e.g. Mumbai/Nagpur).
Enrich vague rows by reading each QAJobsBoard detail URL before import.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-16 17:20:53 -04:00
co-authored by Cursor
parent 0a63316100
commit 03d293699a
3 changed files with 263 additions and 34 deletions
+57 -34
View File
@@ -9,6 +9,11 @@ import type {
ExtractorRunResult,
} from "@shared/types/extractors";
import type { CreateJobInput } from "@shared/types/jobs";
import {
extractJobLocationFromText,
fetchQaJobDetailEnrichment,
stripHtml,
} from "./src/detail-page.js";
const JOBS_URL = "https://qajobsboard.jobboardly.com/jobs.json";
@@ -46,21 +51,6 @@ function asString(value: unknown): string | undefined {
return trimmed ? trimmed : undefined;
}
function decodeHtmlEntities(value: string): string {
return value
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, " ");
}
function stripHtml(html: string): string {
const noTags = html.replace(/<[^>]+>/g, " ");
return decodeHtmlEntities(noTags).replace(/\s+/g, " ").trim();
}
function salaryLabel(raw: SalaryBand | undefined): string | undefined {
if (!raw) return undefined;
const schedule = raw.schedule ? `${raw.schedule}: ` : "";
@@ -93,19 +83,6 @@ function isVagueLocationLabel(value: string): boolean {
return VAGUE_LOCATION_LABELS.has(value.trim().toLowerCase());
}
function extractJobLocationFromDescription(
html: string | undefined,
): string | undefined {
if (!html) return undefined;
const text = stripHtml(html);
const match = text.match(
/\bjob\s+location\s*:\s*([^\n.]{2,120})/i,
);
if (!match?.[1]) return undefined;
const extracted = match[1].trim();
return extracted || undefined;
}
function locationLabel(job: QaJobBoardlyJob): string {
const limits = Array.isArray(job.location_limits)
? job.location_limits
@@ -117,9 +94,9 @@ function locationLabel(job: QaJobBoardlyJob): string {
const loc = asString(job.location);
if (loc && !isVagueLocationLabel(loc)) return loc;
const fromDescription = extractJobLocationFromDescription(
job.description?.html,
);
const fromDescription = job.description?.html
? extractJobLocationFromText(stripHtml(job.description.html))
: undefined;
if (fromDescription) return fromDescription;
if (loc) return loc;
@@ -179,6 +156,47 @@ function mapJob(raw: QaJobBoardlyJob): CreateJobInput | null {
};
}
const DETAIL_ENRICH_CONCURRENCY = 4;
function needsDetailEnrichment(location: string | undefined): boolean {
if (!location?.trim()) return true;
return isVagueLocationLabel(location);
}
async function enrichJobsFromDetailPages(
jobs: CreateJobInput[],
shouldCancel?: () => boolean,
): Promise<CreateJobInput[]> {
const enriched = jobs.map((job) => ({ ...job }));
const targets = enriched
.map((job, index) => ({ job, index }))
.filter(({ job }) => needsDetailEnrichment(job.location));
for (let offset = 0; offset < targets.length; offset += DETAIL_ENRICH_CONCURRENCY) {
if (shouldCancel?.()) break;
const batch = targets.slice(offset, offset + DETAIL_ENRICH_CONCURRENCY);
await Promise.all(
batch.map(async ({ job, index }) => {
try {
const detail = await fetchQaJobDetailEnrichment(job.jobUrl);
if (!detail) return;
enriched[index] = {
...job,
...(detail.location ? { location: detail.location } : {}),
...(detail.jobDescription
? { jobDescription: detail.jobDescription }
: {}),
};
} catch {
// keep feed row when detail page fetch fails
}
}),
);
}
return enriched;
}
export const manifest: ExtractorManifest = {
id: "qajobsboard",
displayName: "QAJobsBoard",
@@ -230,16 +248,21 @@ export const manifest: ExtractorManifest = {
out.push(mapped);
}
const withDetails = await enrichJobsFromDetailPages(
out,
context.shouldCancel,
);
context.onProgress?.({
phase: "list",
termsProcessed: 1,
termsTotal: 1,
currentUrl: JOBS_URL,
jobPagesProcessed: out.length,
detail: `QAJobsBoard: ${out.length} matched (${rows.length} total listings)`,
jobPagesProcessed: withDetails.length,
detail: `QAJobsBoard: ${withDetails.length} matched (${rows.length} total listings, detail pages for vague locations)`,
});
return { success: true, jobs: out };
return { success: true, jobs: withDetails };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return { success: false, jobs: [], error: message };