Reading a page: a JobPosting's identifier is matched by its name, so every posting on a careers site counts as this page's #15

Closed
opened 2026-10-01 10:47:15 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo-chromium at 7998de372, reading how the popup picks the posting a page is about, and checked against the core's copy of the same code at postulo 95348908d.

Related to postulo/postulo#177.

matchesUrl falls back to comparing a posting's identifier with the page's address, but reads the identifier through text(), which prefers name over value. In schema.org an identifier is usually a PropertyValue whose name is the issuing organisation and whose value is the id (Google's own JobPosting example is {"@type": "PropertyValue", "name": "<company>", "value": "1234567"}). So the test is really "does the company's name appear in the address". On a careers site whose host or path holds a one-word company name, every posting on the page matches: the popup fills its form with the first posting rather than the one being looked at, and a results page is never offered as a list to tick.

  • Where:
    • src/lib/parse.js:403-412, text(): return String(one.name || one.value || "").trim();
    • src/lib/parse.js:556-570, matchesUrl(): const identifier = text(posting.identifier); return identifier.length >= 4 && here.includes(identifier.toLowerCase());, a substring test against host, path and query together.
    • src/lib/parse.js:581-587, bestPosting(): found.find((posting) => matchesUrl(posting, url)) || found[0], so an earlier posting that matches by identifier wins over a later one whose url names the page exactly.
    • src/lib/parse.js:799-830, readPostings(): sets here: matchesUrl(posting, url) on every listed posting.
    • src/popup.js:90-96: offers the tick list only when listed.length > 1 && !listed.some((posting) => posting.here).
    • The core has the same code: plugins/builtin/__init__.py:110-117 (_text), :237-253 (_matches_url) and :267-275 (_best_posting).
  • What happens:
    1. A posting page at https://careers.blackmesa.example/jobs/202 carries JSON-LD for postings 101 ("Welder"), 202 ("Physicist") and 303, each with its own url and with identifier: {"@type": "PropertyValue", "name": "BlackMesa", "value": "<n>"}.
    2. text(posting.identifier) is "BlackMesa" for all three, which the address contains, so posting 101 matches first and the form opens with "Welder".
    3. On the same site's results page, careers.blackmesa.example/jobs?q=all, every posting is marked here, so the popup treats it as a single posting's page and the tick list (#177) never appears.
      Running the core's registry.parse_page on the snapshot with the page above gives "Welder"; with the identifier's name changed to "Black Mesa Research" it gives "Physicist", the right answer.
  • Why it matters: the form is filled with another posting's title, company details and description. Every field goes to Postulo as a correction, so a person who does not notice sends the wrong posting for review; on a results page they can capture only one posting instead of choosing several.
  • Fix: read an identifier's value (or the identifier itself when it is a plain string or number), never its name, in a helper of its own rather than text(). Compare it with whole path or query segments of the address, not as a substring of host, path and query. In bestPosting, try url, @id and sameAs across all postings before trying any identifier. Make the same change in the core's _matches_url and _best_posting (the extension mirrors the server's reading, as #3 asks), with the shared corpus fixture below on both sides.
    Related, same functions: sameUrl() (parse.js:541-549, core _same_url) keeps the whole query string, so the posting page opened from a link carrying ?utm_source=... never matches its own url and falls back to the first posting too. Ignoring well-known tracking parameters (utm_*, gclid, fbclid, ref) when comparing would close that as well.

Done when

  • A fixture with three JobPostings sharing a PropertyValue identifier named after the company, on an address that contains that name, reads as the posting whose url is the page in test/parse.test.mjs, and the same fixture in the core's parser tests reads the same.
  • readPostings on that site's results page marks no posting here, in test/parse.test.mjs.
Found on 2026-09-29 in a read-only review of postulo-chromium at 7998de372, reading how the popup picks the posting a page is about, and checked against the core's copy of the same code at postulo 95348908d. Related to postulo/postulo#177. `matchesUrl` falls back to comparing a posting's `identifier` with the page's address, but reads the identifier through `text()`, which prefers `name` over `value`. In schema.org an identifier is usually a `PropertyValue` whose `name` is the issuing organisation and whose `value` is the id (Google's own JobPosting example is `{"@type": "PropertyValue", "name": "<company>", "value": "1234567"}`). So the test is really "does the company's name appear in the address". On a careers site whose host or path holds a one-word company name, every posting on the page matches: the popup fills its form with the first posting rather than the one being looked at, and a results page is never offered as a list to tick. - **Where:** - `src/lib/parse.js:403-412`, `text()`: `return String(one.name || one.value || "").trim();` - `src/lib/parse.js:556-570`, `matchesUrl()`: `const identifier = text(posting.identifier); return identifier.length >= 4 && here.includes(identifier.toLowerCase());`, a substring test against host, path and query together. - `src/lib/parse.js:581-587`, `bestPosting()`: `found.find((posting) => matchesUrl(posting, url)) || found[0]`, so an earlier posting that matches by identifier wins over a later one whose `url` names the page exactly. - `src/lib/parse.js:799-830`, `readPostings()`: sets `here: matchesUrl(posting, url)` on every listed posting. - `src/popup.js:90-96`: offers the tick list only when `listed.length > 1 && !listed.some((posting) => posting.here)`. - The core has the same code: `plugins/builtin/__init__.py:110-117` (`_text`), `:237-253` (`_matches_url`) and `:267-275` (`_best_posting`). - **What happens:** 1. A posting page at `https://careers.blackmesa.example/jobs/202` carries JSON-LD for postings 101 ("Welder"), 202 ("Physicist") and 303, each with its own `url` and with `identifier: {"@type": "PropertyValue", "name": "BlackMesa", "value": "<n>"}`. 2. `text(posting.identifier)` is `"BlackMesa"` for all three, which the address contains, so posting 101 matches first and the form opens with "Welder". 3. On the same site's results page, `careers.blackmesa.example/jobs?q=all`, every posting is marked `here`, so the popup treats it as a single posting's page and the tick list (#177) never appears. Running the core's `registry.parse_page` on the snapshot with the page above gives "Welder"; with the identifier's name changed to "Black Mesa Research" it gives "Physicist", the right answer. - **Why it matters:** the form is filled with another posting's title, company details and description. Every field goes to Postulo as a correction, so a person who does not notice sends the wrong posting for review; on a results page they can capture only one posting instead of choosing several. - **Fix:** read an identifier's `value` (or the identifier itself when it is a plain string or number), never its `name`, in a helper of its own rather than `text()`. Compare it with whole path or query segments of the address, not as a substring of host, path and query. In `bestPosting`, try `url`, `@id` and `sameAs` across all postings before trying any identifier. Make the same change in the core's `_matches_url` and `_best_posting` (the extension mirrors the server's reading, as #3 asks), with the shared corpus fixture below on both sides. Related, same functions: `sameUrl()` (`parse.js:541-549`, core `_same_url`) keeps the whole query string, so the posting page opened from a link carrying `?utm_source=...` never matches its own `url` and falls back to the first posting too. Ignoring well-known tracking parameters (`utm_*`, `gclid`, `fbclid`, `ref`) when comparing would close that as well. ## Done when - A fixture with three JobPostings sharing a `PropertyValue` identifier named after the company, on an address that contains that name, reads as the posting whose `url` is the page in `test/parse.test.mjs`, and the same fixture in the core's parser tests reads the same. - `readPostings` on that site's results page marks no posting `here`, in `test/parse.test.mjs`.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:15 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-chromium#15
No description provided.