what a 200 response does not tell you: four companies, six founding years and a favicon spelling the wrong name
I was looking at a screenshot of Google results for my own domain and the favicon next to every row read "iexels". The build was green, the deployment was READY and the page returned 200, which is when I started writing down what a passing check actually proves.

A deploy went green, the dashboard said READY, and the page it shipped was serving a stale build against fresh chunks. Every check I had was passing. The only thing that was wrong was the page.
Nothing about it was broken in a way a machine reports. The build passed, the deployment was READY, the page returned 200.
Two files were doing it. src/app/icon.png, the file Next.js actually serves as the favicon, was a lowercase serif "p", the old Piexels mark. And public/brand/icon-512.png, the file my schema names as the Organization logo, which is what a Google Knowledge Panel would show, was the full "piexels" wordmark cropped to a square. Cropped, it read as "iexels". The same crop was sitting in favicon-16, favicon-32, apple-touch-icon and src/app/apple-icon.png. Both SVG variants contained the literal string "DlVX" inside the file and still drew a "p" glyph path.
I replaced all of them with a real DLVX letterform and archived the four leftover Piexels source files. Fixed, I thought.
A day later I found public/favicon.ico at the root, still 32,038 bytes of the Piexels wordmark. That is the file a browser requests first, and it overrides the app-router icons. At 16 pixels a wordmark is not a wordmark, it is a black smudge. Rewritten as the "d" mark at 16, 32, 48 and 64 it came to 512 bytes. Four more unreferenced Piexels images were still returning 200 to anyone who asked for them.
a status code is a claim about the request, not about the page
Every one of those failures lived on the correct side of a green check. A build result tells you the build finished. A 200 tells you a server answered.
The cleanest example I have is a ProPick deploy where the page returned 200 and every image on it was broken. next/image routes through Vercel's metered optimizer, and when the quota is gone the optimizer returns 402. The img tags still render, the layout is intact, the source files are all 200 in storage. The only signal is naturalWidth sitting at 0, which no status code will ever tell you. The fix was images.unoptimized: true. The lesson was the QA step: check that pixels loaded and that the live HTML contains no /_next/image URLs. Not eyeballing.
the alias that does not move
The second trap is the deploy that succeeded on something nobody is looking at.
A CLI production deploy can go READY with target production while the project's actual production alias still points at the previous deployment id. The live domain keeps serving the old build, indefinitely, with a green deployment sitting next to it in the dashboard. The fix is an explicit POST /v10/projects/{projectId}/promote/{deploymentId}. A deploy is not done at READY. It is done after you read the production target back and hit the live route.
There are sibling versions of this. Two git worktrees can share the same relative path, something like clients/x/nextjs, while only one of them is on the branch Vercel serves, and a deploy script that resolves the site by path alone ships the stale copy without complaining. The habit that fixes it is confirming a worktree's branch and HEAD commit against the live deployment's gitCommitSha before editing a single file.
And a Vercel project can have no GitHub integration at all, so a push to main triggers zero deploys. Two clients, same surprise.
the push that deletes something and stays green
On UnReal Gym, a one-line hotfix got rebased onto a stale copy of origin/main and pushed. It carried none of the intervening commits. That push removed over a thousand lines across dozens of files, an entire analytics and CSP layer, from production.
The build stayed green. The deployment stayed READY. The site stayed up. The only symptom was a metrics tool quietly under-reporting, for weeks.
git log , oneline on the branch you are pushing tells you what it adds. It tells you nothing about what it removes. The check that catches this is git merge-base , is-ancestor <last-known-good> origin/main, or a flat refusal to push a main that is not a descendant of the previous main.
Dating the regression was its own lesson. The code history showed nothing. What pinned the window was diffing a value the new code writes and the old one does not, a UTM tag format, against live database rows. Diff the data, not the repo.
schema that is well formed and describes the wrong company
Then there is the layer where correctness is not a code property. Until I fixed it on 4 September, dlvx.co's structured data described four different companies with no relationship to each other.
There was the root Organization node. There was a second Organization id, https://dlvx.co/th#organization, that every Thai Person node's worksFor pointed at, and which no page on the site ever declared. It existed only as a reference. And every Service node on the site, the leaf builder, the shared city hub builder, three national pages, both campaign pages, named an inline provider whose url was piexels.dev, a dead domain. The commit message I wrote for myself says each service page "declares a fourth unlinked company." Nothing caught it, because a dangling @id and a dead provider URL are not syntax errors. They are just false.
Underneath that, the Thai offer catalog advertised six URLs that returned 404. And the Thai render of the services hub canonicalised to the English URL, which tells Google the Thai page is a duplicate of the English one, which means 6,820 characters of Thai could never be indexed at all.
The best one is the founding year. A live audit across the repo found six different answers published at the same time: "since 2017" on 5 files, "since 2021" on 19, "since 2023" on 5, "since 2024" on 17, "since 2025" on 1 and "since 2026" on 1. The Thai surface contradicted itself three ways on its own. One translations file carried both 2021 and 2024. The only machine-readable foundingDate on the entire site said 2021 and lived on a Thai page, and the homepage published none at all. I made 2021 the default and put foundingDate on the root Organization node. It was a sitewide grep, not an edit.
Same shape as an FAQ I found on another client, answering yes, with a specific price, to a question about a service that business does not offer, baked into FAQPage structured data for months. FAQ content is a product claim rendered twice, once for people and once for machines to lift verbatim. Audit it as a claim, not as copy. Prices behave the same way: change one in a data file and it stays live everywhere it was hand-copied into prose.
the green check that proved nothing for four days
The worst one is not a false negative. It is a false positive.
A recurring lead health check on ProPick ran green for four days. It worked by tagging its own synthetic submission and suppressing the notification email so the client's inbox stayed clean. The suppression matched real traffic too. Two real enquiries from real people arrived in that window and nobody was notified about either.
The monitor was not measuring the thing it existed to measure. The fix is not suppressing the send, it is overriding the recipient to an internal address and asserting the send succeeded by capturing the provider's message id. Same reasoning as the fire-and-forget notification on that stack: await it, return the message id or the exact error, stamp notified_at on success, so a later query can name what never went out.
None of that would have saved UnReal Gym, whose inbound mail died for over a week with no deploy and no code change, because a Google Workspace MX and SPF rework on one domain dropped the DKIM and MX records a completely separate client's sending provider needed. The provider's own re-verification is the only thing that notices, and it notices silently, days later.
the list
This is what I run now, in order.
- Read the production alias back and confirm its deployment id is the one you just shipped. READY is not live.
- Confirm the checkout you edited is the one being served, branch and HEAD against the live
gitCommitSha. - Before pushing main, assert the new head is a descendant of the last known good one.
- Fetch the rendered HTML and grep for what must not be there: the dead domain, the old brand name, last quarter's price.
- Prove pixels loaded,
naturalWidth > 0, not that tags exist. - Render every locale and read the surrounding chrome, not just the server HTML. A client-side provider can decide the locale after your check has passed.
- Resolve every
@idin the JSON-LD against a node the site actually declares, and fetch every URL in the offer catalog. - Assert the side effect, not the row. Capture the provider's message id.
- Control-test any automated judge against a page you already approved. Mine once scored a page 40 with a fabricated overflow defect, then 100 with zero flags on the identical commit and preview.
- Look at it at 390 pixels wide. My own pricing table clipped its last column there, found in the first live mobile screenshot after shipping, not in any check.
- Request
/favicon.ico, the file the browser actually asks for, not the one your framework thinks it serves.
Before the schema work went out I ran those checks on a local production build, then repeated them live after the deploy: no dangling @id, provider resolving to the canonical entity, the Thai hub self-canonical, new routes 200, the dead domain absent. The icons still took two more commits after that.
what it actually costs
The temptation is to read this as diligence theatre for big sites. Search Console for dlvx.co over the 90 days to 1 September shows 23 clicks and 1,687 impressions. That is my own site, small numbers, nothing riding on it, and it still shipped four disconnected company identities, six founding years and a favicon spelling the name of a business that no longer exists.
Nobody reported any of it. No error, no alert, no failing check, no client email. The site was up the whole time. That is the property of this failure class: no symptom, and everyone downstream quietly accepts a slightly wrong version of you.
The most expensive version I have seen was three finished, committed, tested workstreams for one client sitting undeployed for about three weeks, because nothing in the workflow forced a decision to either ship them or formally hold them. Green build, clean repo, real work dark. Committed is not deployed, deployed is not promoted, promoted is not correct, and correct is only ever something you go and look at.