← all notes

dlvx notes · no. 001 · 7 min read · 2026-09-05

i ran my own entity audit on dlvx.co and found four different companies

In early September I ran a full entity and answer-engine audit on my own studio site. It failed on almost every axis I check for other people, and the worst defect had been sitting in Google's results in plain sight.

I was looking at a live Google results screenshot for dlvx.co and noticed the favicon sitting next to every result. It read iexels.

Not Piexels, which is what the studio used to be called. Not DLVX, which is what it is. iexels. The old wordmark had been cropped square at some point and the crop ate the first letter, and that mark had been sitting next to every one of my search results for as long as nobody looked.

So I ran a full entity audit on my own site, the same one I run on client work. It is dated 4 September and it failed on almost every axis I check for other people. The failure mode is worth writing down, because it is not laziness. It is what happens to a site that has been alive a few years, been renamed once, and been edited by someone who was always about to get to it properly.

four companies, none of them linked

Structured data is how you tell a machine which company is speaking. One Organization node, one identity, everything else referencing it by @id. Get that wrong and an assistant cannot attribute anything to you with confidence, because it cannot tell which of the entities on your site is making the claim.

Mine declared several.

There was the root Organization node on the homepage, which was correct. Then there was https://dlvx.co/th#organization, which every Person node on the Thai side pointed at with worksFor. No page on the site ever declared that @id. It existed only as a reference to a thing that had no definition anywhere. Every Thai Person node said "works for" and then named nothing.

Then every Service node, on the leaf builder, the shared city hub builder, the three national pages and both campaign pages, carried an inline provider object whose url was piexels.dev, a dead domain. My own commit message calls that a fourth unlinked company, which is accurate. In machine-readable terms, every service page named a different, undefined firm as the party that would actually do the work. Separately, the Thai ProfessionalService node had no parentOrganization at all. It floated.

The fix was mechanical once it was visible. One canonical Organization, name DLVX, alternateName Studio DLVX and Piexels, and every Person, Service and ProfessionalService referencing it by @id instead of restating half of it inline. piexels.dev no longer appears in any rendered schema or sameAs. hello@dlvx.co replaced bangkok@piexels.co in 45 files in the same pass. None of that was difficult. The difficult part is that it had been true for a long time and nothing ever told me.

the services catalog pointed at six 404s

The /th offer catalog is the machine-readable list of what I sell. It advertised six URLs. All six returned 404.

They now point at the real /th/services/* pages that had existed the whole time. The catalog had drifted from the routes as the site was restructured, and because nobody clicks a URL inside a JSON-LD block, nobody noticed. That is the specific quality of these bugs. They are invisible to every human who uses the site normally and completely visible to every machine that reads it.

Same shape elsewhere. /th/services/webflow was a card on the hub that 404'd, and the data object for that page already existed in the codebase. The route had simply never been built. The hub now shows nine services. /services and /pricing did not exist on the canonical host either, despite my own schema and my own llms.txt pointing at them, so I built both, with /pricing as the direct answer to what a website costs in Bangkok and every figure matching the corresponding service leaf. /work/horsetonic now 308s to the live case study.

the thai pages could never be indexed

This is the one that cost the most.

The Thai render of the services hub, /th/services?lang=th, canonicalised to the English URL. In plain language, the Thai page told Google it was a duplicate of the English one. So it could never be indexed. Not ranked badly. Structurally unindexable, because of one line of metadata.

That page carries 6,820 Thai characters. Written, translated, shipped, unreachable.

The fix was making buildHreflang take a locale and having the hub use generateMetadata, so the Thai URL is its own canonical with a Thai title and description and th-TH hreflang points at it. That is a small change. I do not know how long it had been live, which is its own answer.

All four llms.txt variants were rewritten answer-first in both languages while I was in there. The Thai ones no longer call the company "Piexels Thailand" or link to th.piexels.co, which is another sentence I did not enjoy typing.

six founding years

Then I grepped for the founding year expecting a quick confirmation.

Six answers were live on the site. since 2017 on 5 files across the /co surface. since 2021 on 19 files across /th. since 2023 on 5. since 2024 on 17. since 2025 on 1. since 2026 on 1, in src/proxy.ts, probably a comment.

The Thai surface contradicted itself three ways on its own, and translations.ts carried both 2021 and 2024 inside a single file. The only machine-readable foundingDate anywhere on the site was 2021, published on the Thai page. The homepage published none at all, so the page most likely to be read as canonical said nothing about when the company started while five other files disagreed with each other in public.

I made 2021 the default: nine since 2017 claims across five files on the /co surface changed, and foundingDate 2021 added to the root Organization node in src/app/layout.tsx, which had never carried one. Three things I left alone on purpose, because they are different facts rather than competing versions of one fact. "Bangkok studio since 2024" is when the studio opened. The since 2023 references are stack and project lineage. "Live on karman.ch since 2025" is a client's fact, not mine.

the favicon was wrong twice

Back to iexels. Two files were doing the damage. src/app/icon.png, which is what Next.js actually serves as the favicon, was a lowercase serif p, the old Piexels mark. public/brand/icon-512.png, the file my schema names as the Organization logo and therefore the image a Google Knowledge Panel would use, was the full piexels wordmark cropped square. That is where iexels came from.

The same crop was in favicon-16, favicon-32, apple-touch-icon and src/app/apple-icon.png. favicon.svg and favicon-dark.svg still drew a p glyph path while containing the literal string DlVX inside the file, which is a rename that touched the metadata and not the geometry. All replaced with a real DLVX letterform set in Fraunces, and four leftover Piexels source files archived.

A day later I found the file that was actually being served. public/favicon.ico at the root is what a browser requests first, and it overrides the app-router icons entirely. It was still the 32KB Piexels wordmark, which at 16 pixels renders as an illegible black smudge. It is now the DLVX d at 16, 32, 48 and 64px, 512 bytes. I also removed a stray zero-byte favicon.ico and archived four more Piexels files that nothing in src/ referenced but that were still answering 200 to anyone who asked for them.

So I fixed the favicon, verified the fix, and was still wrong for a day, because I had fixed the file the framework serves rather than the file the browser asks for.

what the numbers actually say

Scale, honestly. Search Console for sc-domain:dlvx.co, 6 June to 1 September: 23 clicks and 1,687 impressions. Mobile 13 clicks on 583 impressions, desktop 10 on 1,092, tablet 0 on 12. The last 28 days beat the 28 before, 12 clicks on 815 impressions at position 13.3 against 7 clicks on 635 at 15.1. Small site, small footprint. Nobody should read this as a rescue story.

Only two query rows with clicks are brand terms, dlvx at 8 clicks on 102 impressions and piexels at 1 on 12. Every named non-brand query shows zero clicks, including unreal gym at 188 impressions and daikin b2b at 34. About 14 of the 23 clicks come from queries Google does not name.

The row worth acting on is /th/ai-integration-thailand: 259 impressions at position 7.6, one click. The queries are "does claude work in thailand" at 13 impressions, "claude thailand" at 9, "is claude available in thailand" at 4. People arrive on that intent and leave. The page now answers that exact question first, in the first screen, before anything about me.

Nine service leaves got the same treatment: a dated block under the hero with three direct answers, cost, time and who, under question-style h3 headings, on the reasoning that assistants quote rendered text and not intentions. 75 English buyer questions were merged into the FAQs. 49 Thai ones render in a lang="th" section and emit as a second FAQPage block with inLanguage th-TH, so each leaf carries two. Every question was generated against the repo's own real prices and proof points, then fact-checked by a second model that struck any client claim it could not verify, which is the only reason I trust the Thai half.

the part that generalises

I verified on a local production build before shipping: no dangling @id on any probed page, two FAQPage blocks per leaf, Service.provider resolving to the canonical entity, the Thai hub self-canonical with its 6,820 characters rendered, new routes returning 200, piexels.dev absent from every rendered page. Then I ran the same checks again live after the deploy, because a green local build proves the build. Then I looked at the first mobile screenshot and found the five-column pricing table on /co/pricing clipping its last column, "by scope, fixed before work starts", off the right edge at 390px. It now scrolls sideways inside an overflow container with a 640px minimum width. No script was going to find that one.

None of these defects showed up in a normal review. Every page loaded. Every page returned 200. Anyone clicking around would have found nothing wrong. The site was done in the only sense most people ever check.

A rebrand is not a rename. It is an entity migration, and the entity lives in about forty places you cannot see, several of which are images. I found mine because I treated my own site like a client's, same audit, same live verification, instead of assuming that the person who built it knew what was in it. I built it. I did not know what was in it.

If you renamed your company in the last three years, open your favicon.ico at full size. Just that one, then decide whether you want to look at the rest.

I build brand, software and the ai layer behind both, from bangkok. every price is on the pricing page, and you own the repository from the first commit.

start a project