Our own structured data told Google the belt was free

Every page of this site says the same thing in plain English: nothing is built, nothing is for sale, there is no price and no ship date. We have been careful about it to the point of repetition.

The machine-readable copy of the site said something else. In the JSON-LD that search engines actually parse, the product carried an Offer with `price: "0"` and `priceCurrency: "EUR"`, and an availability of `PreOrder`.

That is not a clumsy way of saying "not for sale". To a search engine it says the belt is free and can be ordered now.

How it got there

It was written to be honest. The intent was to publish the fact that there is no price, and Offer looked like the place to put it. The first version even had a priceSpecification whose description was a sentence explaining that no price had been set.

The problem is that Offer has no field for "there is no offer". A price of zero is a price. PreOrder is a claim that ordering is possible. Prose can carry a negation; a schema field cannot, because the vocabulary only has values that assert something.

What it actually caused

Search Console had opened its Merchant listings and Product snippets reports for the site. Those appear when Google reads pages as shop listings, which is exactly what it had been handed: a product, in euros, at zero, available to order.

So a project whose entire credibility rests on not overclaiming was, in the only version a machine reads, advertising a free wearable that does not exist. Nobody would have written that sentence. It was assembled out of four fields that each looked reasonable.

The fix is to say less

Offer is gone. Product describes a thing; Offer describes a transaction, and there is no transaction. A Product without an Offer is valid schema.org — it simply will not appear as a shopping result, which is correct, because it is not shopping.

Availability is now a plain property: "Not for sale. No price set, no ship date. Free waitlist only." A machine can read it, and it cannot be turned into a number.

Two smaller things went with it. `releaseDate` was an empty string on seventeen pages, and an empty value is not a blank, it is a malformed field that can cost the whole block. And the product description opened with "A black full-grain leather belt", which describes the dye rather than the point — the one phrasing we had explicitly ruled out in prose was still sitting in the structured data, where it counts for more.

Why nobody noticed

This is the third time on this project that something wrong survived because nothing was checking it. The first was arithmetic in a spec document. The second was a belt with no way to fasten it. Both were fixed by writing assertions into the model that builds the renders.

Structured data was outside all of that. It is code that no human reads and no test covered, and it fails silently by design: Google does not report a discarded block, it just quietly stops showing the result.

There is now a check that walks every page on the live site, parses all forty-four JSON-LD blocks, and fails on an empty string, a null, a missing @context or a price on something that is not for sale. It runs against the deployed site rather than the source, because what matters is what is being served.

It turned out to be worth having on its own, so it is published as a tool: schema-drift. It takes a list of claims your project must never make — for sale, reviewed, funded, has employees — and fails if the markup claims them anyway. No dependencies, and it reads the deployed site rather than your source.
schema-drift on GitHub: https://github.com/vojtisprime11/schema-drift

The design decision worth stating: a plain text property is allowed where a typed field is not. "Availability: not for sale, no price set" is readable by a machine and cannot be turned into a number. If you cannot state something truthfully in a typed field, state it in an untyped one rather than picking the nearest wrong value.

The pattern is getting hard to miss. Every one of these three was true in the source, invisible in review, and obvious the moment something measured it.