Build1 publisher3 min readPublished
The structured block carries images, currency and coordinates but no identifier of any kind, so the merge runs on titles in document order. On one capture that recovered 290 of 325 rows, and the shortfall arrives as null fields rather than missing listings.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
`_parse_ld_json` walks `itemListElement` once and buckets each entry into a deque keyed by its `name`, and the DOM walk then pops from that bucket as it hits each static row in document order [6][18]. Two identical postings of the same phone get their JSON-LD entries handed out in the order they appear in the list [6]. That is the correct behaviour for the common case and it is not a guarantee [6].
When a title's queue runs dry, because more static rows carry that title than JSON-LD has entries for, the row still ships [9]. Title, price and location come from the static HTML; `currency`, `latitude`, `longitude` and `images` come back null or empty [9]. Fault isolation is at the field level, not the row level.
That is what the 89% is measuring. Of 325 listings, 290 joined [7], so 35 rows [1], or 10.8% [3], arrive with the four JSON-LD-sourced fields empty. No listing is lost. A row count that looks complete and a map view that quietly loses one pin in nine are the same run.
The denominator deserves a note. The dev.to write-up reports 290 of 325 recovered end-to-end on a captured 298-item page [7], and 325 is 27 more than 298 [4]. Those two figures do not reconcile as stated, and the post does not explain which capture the denominator came from. Treat 89% as one measurement on one page shape, which is how the post itself frames it when it calls the number the ceiling of a title-only join and tells you to plan field completeness around it rather than assuming 100% [8].
For the figure to transfer you need two things to hold on your query: the same fraction of static rows covered by the JSON-LD block at all, and the same duplicate-title density. Those are different failure sources. Some static rows have no JSON-LD counterpart whatsoever [5], and that share is a property of how far the block extends down the page, not of the join key. Duplicate titles are the part the queue is actually arbitrating. A high-duplicate query in a large metro stresses the second; a short result page barely stresses either.
`position` is in every entry and looks like it solves this [4]. It holds for about the first 18 rows, enough to pass a smoke test, before the numbering drifts out of alignment with the static list [5].
Missing this second finding costs more: Craigslist has moved off per-subdomain numeric IDs onto a unified `/view/d/<slug>/<id>` shape, and fixture capture found opaque alphanumeric IDs such as `39wYA6wpdyRS7ynm63QsKf` [10]. A post-ID regex written against the numeric-only assumption drops every listing on the new scheme without erroring [11]. The pattern in the Actor accepts any alphanumeric run: `r"/view/d/[^/]+/([A-Za-z0-9]+)"` [12]. Craigslist's help pages do not mention the migration, so a spec written from documentation keeps the old assumption intact [13].
The robots.txt handling is the cleanest part of the design. The reply flow is disallowed, and the client refuses `/reply/`, `/fb/`, `/suggest`, `/flag`, `/mf`, `/mailflag` and `/eaf` before any network call is made [14][15]. `reply_url` still ships as a field, captured exactly as rendered and never opened, with no email de-obfuscation on top [16].
For anyone consuming this output, the practical consequence is that latitude, longitude, currency and images should not be marked non-null in your schema. On a normal page they will be missing on roughly a tenth of rows [3][9].
Ranked by verification strength, evidence, and original report placement.
Craigslist search pages ship two copies of every listing: a static HTML list, and a JSON-LD script block carrying images, currency, and geo-coordinates.
Craigslist's JSON-LD carries no shared identifier with the DOM listing at all: no bare post ID, no URL, no SKU, no @id.
The only field both the JSON-LD and the static HTML copies reliably share is the listing title, and titles repeat.
Each itemListElement entry has exactly name, image, offers, @type, and a position field.
Checked against the static list past the first roughly 18 entries, some static-list rows have no JSON-LD counterpart at all and the position numbering drifts out of alignment.
The join uses a FIFO queue per title: walk the static li list in document order and, for each title, pop the next unconsumed JSON-LD entry with a matching name, so duplicate titles get their entries in document order.
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanics you can check, a measurement that wobbles
The mechanics are unusually easy to test for a vendor post: the queue functions, the forbidden-path tuple and the post-ID pattern are quoted verbatim, so anyone with a captured page can confirm or kill them. The headline result is weaker. A 290-of-325 recovery rate attributed to a 298-item page leaves 27 listings unexplained, and no one outside the project has repeated the capture.
No use outside the builder
What exists is a product description and a benchmark the seller ran on its own fixture. No customer, no third-party run and no dataset produced by anyone else appears anywhere in this reporting, so there is nothing to measure uptake against.
Undersells its own numbers, oversells the field
Most of this post argues against its own completeness: 89% is offered as a ceiling rather than a win, and the fallback is described as null fields instead of a fix. The overreach sits at the edges. A recovery rate is published against a sample size it contradicts, the ceiling claim rests on one capture, and the competitive line about incumbents needing a run per metro names no incumbent and shows no numbers.
Written like a post-mortem, functions as a product page
The piece closes in feature bullets, run caps and a customer FAQ, which places it squarely as marketing for a paid scraper written in the register of an engineering debrief. That does not make the JSON-LD findings wrong; it does mean the sample, the metric and the framing were all chosen by the party selling the tool.
Confident on shape, shaky on the number
Confidence splits by claim type. The URL-scheme and robots.txt findings are the kind a single honest capture settles, and they are quoted in enough detail to check independently. The recovery rate is one run by one interested party with an internal arithmetic gap, so it establishes an order of magnitude rather than a figure worth planning field completeness around.
build
Force the tool call, then hand Lightsail a long-lived key1 publisher
build
A receipt intent written before dispatch turns an SMS timeout into a poll instead of a resend1 publisher
build
A cache hit is a quota refund: semantic caching with trigrams and no vector database1 publisher
build
Stop timing your GraphQL tests and start counting loader calls1 publisher
Publishers with included, body-backed reporting in this cluster.
1 article · September 7, 2026