fix(jobdanmark-search): render a JSON-LD description as text instead of passing the markup through (#516)

The JSON-LD branch of detail emitted jobPosting.description verbatim, so
`detail --format plain` printed
`description: <p>Vi søger en udvikler til R&D.</p><ul><li>Python & Go</li>...`
while the rendered-HTML branch of the same command printed one clean
line per paragraph and bullet. Two description shapes from one command
depending on which page layout it hit, and the markup landed in
/scrape's stored snippet and /rank's agent context as-is. The portal
contract (/add-portal Step 4) asks for readable text: entities decoded,
tags stripped, paragraph breaks preserved.

Render the HTML through node-html-parser's structuredText - one line per
block element, entities decoded, tags gone - which is the shape the
fallback already produces. A plain-text description is unchanged, an
absent one stays "".

The JSON-LD test that pinned the raw <p>...</p> now expects the text,
plus two new cases; the HTML case fails on master.
This commit is contained in:
Ayobami Adegoke authored and GitHub committed 2026-10-05 19:25:10 +02:00
1 parent 0c9bb3cb07
commit e9115b02f8
3 files changed
+64 -2

No files matched your search

@@ -56,6 +56,21 @@ function cleanText(text: string): string {
return text.replace(/\s+/g, " ").trim()
}
/**
* JSON-LD descriptions arrive as HTML (schema.org allows it, jobdanmark uses
* it). Render them to the same newline-separated plain text the rendered-HTML
* branch produces, so `detail` emits one description shape whichever page
* layout it hit: tags gone, entities decoded, one line per block element.
*/
function descriptionToText(html: string | undefined): string {
if (!html) return ""
return parse(html)
.structuredText.split("\n")
.map(cleanText)
.filter(Boolean)
.join("\n")
}
function normalizeUrl(value: string | null | undefined): string | null {
if (!value) return null
const decoded = value.replace(/&amp;/g, "&")
@@ -115,7 +130,7 @@ function fromJsonLd(jobPosting: JsonLdJobPosting, slug: string, url: string): De
postalCode: address?.postalCode ?? null,
addressCountry: address?.addressCountry ?? null,
},
description: jobPosting.description ?? "",
description: descriptionToText(jobPosting.description),
applyUrl: null,
}
}
@@ -50,11 +50,43 @@ describe("parseJobPostingFromHtml JSON-LD", () => {
postalCode: "5000",
addressCountry: "DK",
},
description: "<p>Build reliable pipelines.</p>",
description: "Build reliable pipelines.",
applyUrl: null,
});
});
test("renders an HTML description to the rendered-branch text shape", () => {
// add-portal.md Step 4: a detail description is "readable text (entities
// decoded, tags stripped, paragraph breaks preserved)". The rendered-HTML
// fallback already emits one line per <p>/<li>; the JSON-LD branch passed
// the markup through verbatim, so the same command produced two shapes.
const posting = {
"@type": "JobPosting",
title: "Udvikler",
description:
"<p>Vi s&oslash;ger en udvikler til R&amp;D.</p><p>Second para<br>line two</p><ul><li>Python &amp; Go</li><li>SQL</li></ul>",
};
const parsed = parseJobPostingFromHtml(
pageWithScripts(JSON.stringify(posting)),
"udvikler",
"https://jobdanmark.dk/job/udvikler",
);
expect(parsed.description).toBe("Vi søger en udvikler til R&D.\nSecond para\nline two\nPython & Go\nSQL");
expect(parsed.description).not.toMatch(/<[^>]+>/);
});
test("leaves a plain-text description unchanged and an absent one empty", () => {
const withText = { "@type": "JobPosting", title: "A", description: "Plain prose, no markup." };
const without = { "@type": "JobPosting", title: "B" };
expect(parseJobPostingFromHtml(pageWithScripts(JSON.stringify(withText)), "a", "https://jobdanmark.dk/job/a").description).toBe(
"Plain prose, no markup.",
);
expect(parseJobPostingFromHtml(pageWithScripts(JSON.stringify(without)), "b", "https://jobdanmark.dk/job/b").description).toBe("");
});
test("skips malformed scripts and finds a JobPosting in an array", () => {
const scripts = [
"{not-json",