A website redesign is supposed to be the good news. This one cut organic traffic by 64 percent in seven weeks, and quote requests went from about 85 a week to fewer than 25.
The company is a commercial and residential electrical contractor in the Dallas-Fort Worth area, 28 service vans, two divisions, and a business that lives on searches like “commercial electrician Dallas” and “24/7 electrical repair Fort Worth.” In mid-2025 it moved from WordPress to a headless build: a Next.js front end on a decoupled CMS, to support online booking, financing calculators and live dispatch tracking. Good reasons. The site looked great in a browser.
I was brought in as the technical SEO lead to find out why the rankings vanished and to get them back. The whole diagnosis came down to one question: what does Google get when it asks for the page, and how different is that from what a person sees? The rest of this article is how I answered it with a crawler, what it turned up, and what the fixes did.
The redesign went live and the leads went with it
The agency that built the site assumed modern search bots render JavaScript the way Chrome does, so putting the page content behind client-side hydration was fine. Sometimes it is. Here it was not, and there was no measurement in place to notice. Within seven weeks the transactional queries that fed the phones had dropped off page one entirely, and the internal team was arguing about whether it was the redesign, the URL changes, a Google update, or bad luck.
That argument is the reason I start with a crawl rather than opinions. A crawler can fetch a page the way a bot does, then render it the way a browser does, and show you the difference line by line. Nobody argues with a diff.
What the raw HTML showed versus what the rendered page showed
The gap was not subtle. On the commercial service pages, 78 percent of the body copy existed only in the rendered DOM. In the raw HTML there was a title tag, an empty root div and a script bundle. Headings, service descriptions, reviews, the lot, all arrived after JavaScript ran.
Worse, the render was not reliable. Two script bundles threw uncaught exceptions on some templates, and the rendering log showed pages that never finished inside Google’s patience. A bot that gives up at five seconds got the empty version. That matched the pattern in Search Console: pages indexed, but indexed as near-empty documents that ranked for nothing.
The internal links were the second casualty. The suburb pages for Frisco, McKinney, Arlington, Irving and the rest were linked only from a JavaScript menu that failed to attach during a bot pass. So even where a page was fine, the paths to it were not.
I did not ask the developers to take my word for it. I gave them the stack traces from the render log and the side-by-side extracts for a dozen templates. That turned a week of debate into a two-line decision: move the core content and the navigation to server-side rendering and static generation, and keep the interactive tools client-side where they belong.
| What I compared | Raw HTML | Rendered DOM | What it meant |
|---|---|---|---|
| Body copy on commercial service pages | Absent on 78% of pages | Present | Pages indexed as empty documents |
| Links to suburb service pages | Not present | Present in the menu | Location pages effectively orphaned |
| Structured data (JSON-LD) | None | Injected after load | No local pack or rich result eligibility |
| Render completion | n/a | Timeouts and 2 uncaught errors on key templates | Some bot visits never saw the content at all |
The URLs that never got mapped, and a staging site left open
About 180 location and commercial sub-service URLs had changed structure with no redirects. The old addresses returned soft 404s, which is the worst of both worlds: a 200 status with nothing on it, so Google keeps the old page in the index and slowly learns it is worthless. The new pages were reachable only through the same dynamic menu that bots could not use.
The staging environment, where the booking flow was being tested, had also been left crawlable without canonical tags pointing at production. Duplicate copies of core pages, on a host nobody wanted indexed, competing with the real ones.
Neither of these is a JavaScript problem. They are launch checklist problems, and they were compounding the rendering problem. A redirect map, canonicals on production, and a login gate on staging fixed them in the first sprint.
Custom extraction found 412 pages still showing placeholder text
The site generated pages for every service in every city it served. That is fine when the templates work. I extracted the pieces that had to be real on every page: the local dispatch response time (//div[@data-testid='local-dispatch']/span/text()), the license number in the sub-footer (span.license-verification), and the local tracking phone number by pattern.
The extraction came back with 412 suburb pages where the injection script had failed and the page was live with tokens still in it: “[City] Emergency Services Available” on a page that was supposed to say Grapevine. That is not a ranking problem, it is a trust problem, and no human review of 2,400 pages would have found all of them. A crawl found them in an afternoon and the templates were patched that week.
Schema that Googlebot never saw
The old WordPress site had ugly hardcoded microdata that at least declared a LocalBusiness. The new site injected JSON-LD through a tag manager container after page load, so the raw fetch had no structured data at all. Where the rendered version did have it, validation flagged conflicting @id values across nested LocalBusiness entities, a deprecated property in the Electrician markup, and FAQ schema on mobile that shipped empty answer objects.
We rebuilt the JSON-LD as part of the server-rendered template: Electrician as the business type, service areas as geo shapes for the counties served, opening hours, license IDs, and FAQ and breadcrumb markup that matched what was visibly on the page. Validation went to a 100 percent pass rate across all 2,400 URLs, and the review stars and FAQ accordions came back in the results.
Blending Search Console and GA4 into the crawl to find the expensive mistakes first
A crawl of 2,400 pages produces thousands of issues. The question is never “what is wrong,” it is “what is wrong on the pages that make money.” Joining performance data to the crawl answers that in one sort.
The best example: 43 commercial service pages with real external links from chamber of commerce directories and trade publications, and zero impressions for 60 days. The reason was a noindex directive set in a client-side header component that only some templates loaded. Nobody had put it there on purpose. Those 43 pages were the first fix after rendering, because they had authority waiting to be used.
Flattening the site from seven clicks deep to three
The graph made the structural problem visible to people who do not read crawl exports. High-margin commercial services such as switchgear and EV infrastructure floated as satellite clusters six or seven clicks from the homepage. Blog posts from 2018 sat one click off the main navigation. Project gallery filters generated thousands of near-identical URLs that soaked up crawl budget and internal link equity.
Before Commercial page, 7 clicks
After Same page, 3 clicks
Staging crawls before every release, and a weekly crawl after
The original launch failed because nobody crawled the new site before it replaced the old one. So every sprint after that, staging got crawled behind its login and compared against production: canonicals, indexability, redirects, heading structure, word counts and link counts, side by side. If a release was going to remove content from the raw HTML or introduce a redirect loop, we knew on a Thursday, not from Search Console three weeks later.
After the recovery, a small script runs the crawler headless every Sunday night, pulls the week’s Search Console and GA4 numbers into it, and drops a report of 4xx and 5xx responses, missing titles, schema failures, canonical mismatches and new orphan pages where the team sees it Monday morning. The point is not the report. The point is that the next regression gets caught before Google indexes it.
Two things I would rather not have had to do by hand, and did not: writing meta descriptions for hundreds of location pages that had none, which ran through an LLM call inside the crawl with the city, service and body extract as inputs, then got a human read before publishing; and expanding every FAQ accordion and tab to check what was inside, which a short browser script inside the render step handled across every template.
What changed over seven months
The rendering fix and the redirect map went out first and produced the fastest recovery. The architecture work and the schema rebuild followed over the next two sprints. Numbers below compare the five months after the rendering fix to the seven weeks of collapse before it.
“Commercial electrician Dallas” went from outside the top 100 to position 2. “24-hour emergency electrician Fort Worth” from page six to position 1. Residential emergency calls up 165 percent. The commercial division signed three multi-year design-build contracts traced to organic landing page inquiries, about $1.4 million in closed pipeline.
What I would not claim from these numbers
The fixes overlapped. Rendering, redirects, architecture and schema went out within a few sprints of each other, so I cannot tell you what share of the recovery each one earned. The rendering fix came first and the curve turned first, which is the strongest evidence I have, and it is still correlation.
The 142 leads a week is also well above the 85 the old site produced before the redesign, and some of that is the new site doing what it was built to do: 2,400 pages that did not exist before, online booking, and a much better mobile experience once the content was actually there. The redesign was a good idea. The launch was the problem.
The method, for any site that renders with JavaScript
- Crawl it twice. Once as plain HTML, once with JavaScript rendering. Diff word count, headings, links and structured data. The gap is your problem list.
- Read the render log, not just the render. Timeouts and uncaught errors tell you which templates bots see empty some of the time.
- Give developers stack traces, not opinions. The decision to server-render took an afternoon once they could see the failure.
- Extract what has to be true on every page. Phone numbers, license IDs, city names. Sort for anything that is not a real value.
- Validate schema on the raw fetch. If it only exists after load, assume the bot never saw it.
- Join performance data to the crawl. Fix the pages with authority and no impressions before the pages with neither.
- Draw the link graph. Click depth and orphaned clusters are visible in seconds and persuasive to people who will never open a spreadsheet.
- Crawl staging before every release, and production every week. Behind the login, compared against live. This is the step that would have prevented the whole thing.
The traffic loss in this case study is one of ten ways a website project quietly goes over budget. The old site is the largest unscoped deliverable, and that is only number five.
The crawl-to-Search-Console join that found this problem, and the weekly crawl diff that would have caught it, are two of thirteen things I now hand to Claude.