The first-week SEO audit that read the site like a crawler
AHA’s site looked fine in a browser. Crawlers saw 21 duplicate titles, no FAQs, no contact form and facts that disagreed. What the audit fixed, and the check that keeps it fixed.
The problem
Crawlers saw duplicate titles, no FAQs, no form and facts that disagreed.
The solution
Fix the HTML that ships, then have every build check it before release.
A website can look finished and still tell search engines very little. A crawler starts from the HTML the server sends, and some crawlers never run the JavaScript that fills in the rest. On AHA’s site, the HTML and the screen told two different stories.
The audit
I started with a full technical and content audit, from 11 to 17 August. It produced a fix list of 38 items, a page-by-page scorecard for keyword targeting, crawlability and search intent, and a split between fixes that needed real writing and fixes that were code only. The first batch went live on 18 August.
One rule ran through all of it: judge the built HTML, not the page in a browser.
What crawlers saw
- 21 pages with a duplicate title. 19 shared one title and meta description through a template fallback flag that was meant to be a safety net. Two more collided on their own.
- Structured data on about 5 pages. The rest had none at all.
- No FAQs. The FAQ page’s 40 questions were rendered by JavaScript, so the HTML held none of them.
- No contact form. On 28 pages the built HTML had zero
<input>elements. JavaScript injected the form after load. - Facts that disagreed. In 56 places, copy or schema said or implied that AHA installs the hardware. It doesn’t: AHA designs, supplies, configures and commissions, and the client’s electrician installs. The site also used 4 different phone numbers.
Two more turned up when I re-checked in early September. An lp. subdomain was serving a byte-identical, crawlable copy of the site, with 20 URLs indexed by Google. And an em dash inside a comment in the shared page head was rendering as a garbled character on all 36 pages.
The fixes
I directed the fixes, and Claude Code wrote much of the code.
- Remove the safety net. Every indexable page now has its own title and description. A fallback that hides a missing value doesn’t fail once; it fails quietly on 19 pages at the same time.
- Render it on the server. All 40 FAQs are in the HTML with FAQPage schema, and the form is in the HTML on all 28 pages.
- Add schema where it fits. 59 valid JSON-LD blocks: Organization, BreadcrumbList, Product and FAQPage.
- One fact, one wording. All 56 install claims were corrected, and four phone numbers became one.
- One copy of the site.
lp.now 301s towww. That took until October: the host first redirected only the home page and let the 20 indexed URLs return 404, which throws their ranking away instead of handing it over. - Drop the dashes. Mojibake went from 36 pages to 0.
The sitemap had its own problem, and its own post.
Making the audit permanent
An audit is a snapshot, and the site kept changing. So in September the checklist became a script. tools/preflight.py reads the built site, not the source, before each release. It checks for duplicate titles and descriptions, missing descriptions, title and description lengths, em and en dashes, “AHA installs” phrasing, and whether the sitemap agrees with the pages.
Two exclusions matter as much as the checks: noindex pages, and redirect stubs. Without the second, every run would report 26 duplicate “Redirecting…” titles, and everyone would learn to ignore it. A check that cries wolf is worse than no check.
It has earned its place. In October it caught “AHA install” wording on two new pages before they shipped.
Results
- Duplicate titles: 21 to 0, verified across 36 indexable pages. The first pre-flight run in September found 0 across 53.
- Structured data: from about 5 pages to 59 valid blocks. A clean build on 22 October found 0 invalid JSON-LD blocks across 138 pages.
- FAQs crawlers can read: 0 to 40.
- On 21 September, Google’s URL Inspection API reported 83 of 85 sitemap URLs indexed. The two exceptions had been published that day.
What I’d keep
- Audit what ships. If JavaScript adds it, assume some crawler won’t see it.
- Distrust safety nets. A default that hides a missing value turns one bug into nineteen.
- Facts are part of SEO. A wrong claim in 56 places matters more than a missing tag.
- Turn the audit into a check, and exclude what is meant to repeat, or nobody will read it.