The Business Reality6 min read

My site rated 474 products. The inputs it described were not in the code.

The badge said 100-point score, and the copy called it a blended signal of search demand and ingredient relevance. The code never read search data, and 204 of 474 products scored exactly the same.

#gotchas#qa#automation#content-quality
Left: the site's 88/100 badge and its stated blend of search demand, ingredient relevance and comparison usefulness. Right: the real code, 76 plus a brand bump clamped to 72-88, and a bar showing 204 of 474 products at exactly 88
Left is what users saw. Right is what the code did. One of the stated inputs did not exist at all.

I run a product catalog site. A weekly bot collects products from public retailer listings, and a static build ships 692 pages. I already wrote about the two weeks when the deploy never fired. That one is fixed.

This time I opened it to check the data. 474 products, zero missing prices, zero missing scores, 97% image coverage. On a table it looked like a healthy catalog.

One question for you. Is the number your site shows users actually computed the way you described it? I had never checked.

The site was selling a score

Every product carried a badge out of 100. Hovering it showed this:

Radar score: our blended signal of search demand, ingredient relevance,
and comparison usefulness. Not a lab rating.

The homepage gave that explanation a whole section. "01 Search demand — we weight products people are genuinely searching for." Convincing. I wrote it, after all.

So I opened the scoring function to find where that weighting happened.

let score = 76 + brandBoost;
if (category === "sunscreen") score += 1;
if (ingredients.some(...)) score += 1;
if (concerns.includes(...)) score += 1;
return Math.max(72, Math.min(88, score));

That is the whole thing. Start at 76, add a brand bump, add +1 for three conditions, clamp to 72-88.

There is no line that reads search demand. No code in that file touches search data at all. I had published a nonexistent input as my methodology and left it there for weeks.

Counting the distribution made it worse

Separately from the wrong methodology, I checked whether the number carried any information. One line does it.

collections.Counter(round(p['score']) for p in products)

Here is what came back:

88: 204 products (43%)
87: 85
89: 73
83: 45
below 80: 0

474 products squeezed between 80 and 92, and 43% of them share the exact same value. A metric advertised out of 100 has a real spread of twelve points, and one mode covers nearly half the catalog.

You cannot pick between two products with that. A metric that produces no separation cannot support a decision.

What would you have done here

There were three options.

  1. Build the score properly. Wire in real search-demand data and tune the weights so it does what the copy claims.
  2. Make the copy match the code. Relabel it honestly as "a heuristic over brand and category tags" and keep the number.
  3. Delete the score.

I sat with option 1 for a while. Then I looked at the site's measured traffic:

Search Console, 28 days: 3 impressions, 0 clicks

There are no visitors. Nobody exists yet who would benefit from a more sophisticated score. Option 2 was briefly tempting, but it fixes the label and keeps the shape. A 100-point badge where 43% of items score 88 still reads as "this product was evaluated," no matter what caption sits under it.

So I took option 3. A metric with zero information content is not something to fix. It is something to remove.

The badge slot now shows data the catalog actually holds: a typical price, ~$18. The homepage "how the score works" band was rewritten as the three things the site really does — public listing data, ingredient tags, outbound retailer searches. The score field survives only as the internal sort key.

One more thing surfaced along the way. 99 of 474 products (21%) had an empty ingredient list, and those pages rendered the "Ingredient themes" row completely blank. One fifth of an ingredient-comparison site does not know the ingredients. That cell now says "Not published in the retailer listing." Saying nothing is there beats showing an empty box.

Deleted things come back, so I added a guard

Copy gets rewritten by humans. In a few months I will not remember this decision. So the commit that deleted the score also added a check. The link checker already walked every built HTML file, so it cost four lines:

const bannedClaims = [/Radar score/i, /blended signal/i, /aggregateRating/, /ratingValue/];
// any match across the 693 files in dist -> exit 1

Confirming a guard passes is not enough. You have to break it on purpose.

$ printf '<html>Radar score 88/100</html>' > dist/_guardtest.html
$ node scripts/check-links.mjs
Rating claims are not allowed (1 files):
- _guardtest.html matches /Radar score/i
exit=1

Now CI blocks that phrasing whatever path tries to publish it. Ever since a rule I enforced only in a prompt quietly stopped holding, I keep checkable rules in code rather than in sentences.

One thing I was glad to verify: the structured data never emitted aggregateRating. The fake rating was visual only; it was not being submitted to search engines as review markup. That was luck, not design.

Three things to check yourself

  1. If you publish a score, grade, or percentage, walk each input named in the copy next to it and point at the code path that produces it. Any input you cannot find is a false claim.
  2. Count the distribution. If the mode covers more than 30% of rows, or the real spread is under 20% of the nominal range, that metric separates nothing.
  3. Look at how empty fields render on the actual page. An empty array shipping as an empty cell reads to users as broken, not as "no data."

The honest part

This site has zero clicks. So this fix earned exactly zero revenue, and will keep earning zero for a while. Yes, I spent time on an asset with no traffic.

I still think it had to go. Zero traffic does not mean a false claim is acceptable; it means nobody has been misled by it yet. And the same defect may sit in my other apps — several of them emit scores and compatibility percentages. That is on the next audit list.

Cleaned up the same day: two of three image-candidate sources were failing 15 out of 15 probes (404 and 403) while the workflow kept reporting success. I deleted them instead of repairing them. Then I demoted two weekly crons that were grinding away for a site with no readers down to monthly, and set a date: on October 11, if clicks are still zero, the schedules come off and the site freezes as a static deploy. Without a date it becomes a zombie.

Try one thing today. Pick a single number your product shows users, then open the copy that explains it beside the function that computes it. Do they tell the same story?

Related