Skip to main content
Back to insightsSEO

The five faults we found auditing our own website

Lewis Banks30 August 20265 min read
Share

On 23 August 2026 we ran a technical audit on bloodstone.co.uk. Not a client site. Ours. We build and run websites for other people, which is exactly why the audit was overdue: the cobbler's children, and so on.

Five faults. None of them appeared as an error in Search Console, none broke anything a visitor could see, and all five had been live for months. Every one is checkable on your own site with a single command, so the commands are here. What follows is what ours was doing wrong. We are not claiming anything about yours.

Fault one: the homepage told crawlers we had shipped nothing

The homepage has stat counters that animate up when they scroll into view. Nice effect. They were built with useState(0) as the initial value, which means the server-rendered HTML contained the initial value, not the real one.

The HTML that left our server said:

0+ Projects shipped
0k+ Hours automated

The real figures are 47 and 12,000. Googlebot renders JavaScript and would eventually have seen the right numbers. The crawlers behind AI answers do not. Vercel and MERJ analysed over 500 million GPTBot fetches and found no evidence of JavaScript execution at all; ClaudeBot downloads JavaScript files in roughly a quarter of requests and never runs them. Those crawlers took the raw HTML and recorded that we had shipped zero projects and automated zero hours.

The check, on any page with a number you care about:

curl -s https://yoursite.com | grep -i "projects shipped"

If the number in the response is not the number on the screen, the server is lying on your behalf. The fix was to seed the state with the true value and treat the count-up as progressive enhancement, so the animation only exists for people whose browsers run it.

Fault two: eighteen landing pages that nothing linked to

We have a set of commercial landing pages under /websites/. Eighteen of them had zero inbound internal links. They cross-linked to each other happily, but nothing pointed in from outside the cluster, including their own parent page.

This is the one-liner, and it is the most useful thing in this article:

curl -s https://yoursite.com/websites | grep -c 'href="/websites/'

Ours returned 0. The parent page did not link to a single one of its children.

For a full orphan sweep across the site, crawl your sitemap and count inbound links per URL:

curl -s https://yoursite.com/sitemap.xml | grep -o '<loc>[^<]*' | cut -d'>' -f2 > urls.txt
xargs -n1 -P4 curl -s < urls.txt | grep -o 'href="/[^"#?]*"' | sort | uniq -c | sort -n > inbound.txt
head -40 inbound.txt

Anything in urls.txt that never shows up in inbound.txt is an orphan. Ours were reachable from the sitemap and from nowhere a person would walk. A sitemap entry says a page exists. Internal links say it matters. We had told Google the first thing and never the second.

Fault three: every enquiry was attributed to us

The enquiries table had 19 rows. All 19 recorded the referrer as our own domain.

The client-side code was correct. It read document.referrer, filtered out same-host values deliberately (so an internal navigation does not overwrite the real source), and sent null when there was nothing worth recording. The server then did this:

referrer: body.referrer || req.headers.referer

On a form POST, the HTTP Referer header is the page the form was on. That is always your own site. The fallback silently undid the filtering the client had done on purpose, and every honest null became bloodstone.co.uk.

The cost was not traffic, it was knowledge. We could not answer which pages produce enquiries, which is the question the rest of the audit existed to serve. Check yours with one query:

SELECT referrer, COUNT(*) FROM enquiries GROUP BY 1 ORDER BY 2 DESC;

If your own domain is 100% of the rows, that is a bug, not a finding. We removed the fallback and now store the first-touch landing page and UTM parameters in localStorage over a 90-day window instead. A session-scoped store was the fault, not the fix: organic search frequently converts on a later visit, and anything that dies with the tab records that visitor as direct.

Fault four: the pricing page and its structured data disagreed

The visible pricing page sells monthly plans. The JSON-LD embedded in the same page published fixed one-off prices of £1,500 to £5,000, left over from an earlier version of the offer. Both were public simultaneously, so anything parsing the page for "what does this cost" got two incompatible answers depending on whether it read the copy or the markup.

curl -s https://yoursite.com/pricing | grep -o '"price":"[^"]*"'

Or dump the whole block and read it properly:

curl -s https://yoursite.com/pricing | python3 -c "import sys,re,json; [print(json.dumps(json.loads(m),indent=2)) for m in re.findall(r'application/ld\+json[^>]*>(.*?)</script>', sys.stdin.read(), re.S)]"

Google's structured data guidelines require markup to reflect content that is actually on the page, and mismatches can earn a manual action for spammy structured data. Nobody had flagged ours. It was still wrong.

Fault five: "From £3,500" became exactly £3,500

Four service pages said "From £3,500" in the body copy. The schema template derived its price by stripping non-digits out of that string and publishing 3500 as an exact price. The pages were honest. The machine-readable version was not, and the machine-readable version is what gets quoted back in an answer engine.

We stopped deriving structured data from marketing copy. Where a price genuinely is a range, the schema now models it as one; where it is a starting point rather than a price, we omit it entirely and let the pricing page carry the number.

The five checks

FaultCommandBad result
Placeholder values in server HTMLcurl -s https://site.com | grep -i "projects"The number is 0
Orphan child pagescurl -s https://site.com/parent | grep -c 'href="/parent/'Returns 0
Referrer attributionSELECT referrer, COUNT(*) ... GROUP BY 1Your own domain is 100%
Price mismatchcurl -s https://site.com/pricing | grep -o '"price":"[^"]*"'Does not match the page
"From" prices in schemaSame command, compare to the copyAn exact number for a range

All five run in under a minute each. All five found something on our site.

What to do with this

Run the orphan check first. It is one command, it needs no access to anything, and a 0 tells you that a section of your site exists only for your sitemap. Then diff your rendered page against curl output on the two or three pages that carry the numbers you would want an AI answer to repeat.

We found these because we finally treated our own site as a client project rather than a side effect of everything else. If you would rather someone else ran that pass, that is part of how we build and manage sites and it sits alongside the rest of what we do. Otherwise, take the commands and keep the findings to yourself. Or tell us what you found; we will happily compare notes.

Need the site itself? We build and manage websites from £125/month — website design london, website design manchester and website design birmingham.

Need help with this?

Bloodstone Projects helps businesses implement the strategies covered in this article. Talk to us about Website Build & Manage.

Get in touch

Get insights straight to your inbox

Practical writing on AI, automation, and building systems that work. No spam, unsubscribe anytime.