OpSpring.ai
All case studiesSolar / Home Services

East Coast Solar

A Worcester solar installer whose site produced about one lead a month, rebuilt for the search channels that were about to matter and instrumented for the one nobody else can see yet.

18
estimate requests in 58 days
~9x
lead rate vs the old site
967
queries earning impressions
78.3%
page uniqueness, from 50.7%
4 months to launch
Timeline

The challenge

East Coast Solar designs, engineers, permits and installs solar across Massachusetts, and does all four in-house rather than subcontracting. That is a genuine differentiator in a market full of sales outfits that hand the actual work to whoever is cheapest that month. Almost none of it reached anybody searching.

The Squarespace site produced roughly one estimate request a month. Its estimate form was worse than that number suggests: it simulated a submission and discarded the lead. Hero imagery shipped as CSS background images at 971KB, bypassing any optimizer. Page titles rendered the brand twice. There was no analytics property, so nothing about any of this was visible.

The timing made it urgent rather than merely unfortunate. The federal 30% residential tax credit, worth roughly $9,000 on a $30,000 system, was eliminated at the end of 2025. Passive, incentive-driven demand went with it. When buyers stop arriving because the math is obvious, the ones still shopping are researching, and research happens in search results and increasingly inside AI assistants. A company that is invisible in both has no pipeline.

There was a structural problem underneath the visible one. The domain reads eastcoast.solar, implying coverage from Maine to Florida, while the business is Worcester and Central Massachusetts. And their Google Business Profile is a hidden-address service-area listing that does not surface in the Places API by exact name, while Worcester competitors do. Every geographic signal the site could send needed to point somewhere the profile itself was not pointing.

The solution

We replaced the Squarespace site with a custom Next.js build: about 40 route groups expanding into roughly 70 pages, including 35 town service-area pages, residential and commercial service trees, a portfolio, an incentives library and a savings calculator.

The calculator is the piece that changed the shape of the funnel. A visitor enters an address and a monthly bill and gets back a modeled system size, annual production, 25-year savings and CO2 offset, computed against Google's Solar API. It is a reason to hand over an address that has nothing to do with wanting a salesperson to call, which is the opposite of how a contact form works. Results are not email-gated; the client rejected that outright and asked for a "speak to a professional" prompt beside the numbers instead, which was the right call.

The incentives content was built to be the most useful page on the Massachusetts solar internet: SMART, net metering, the state credit, the federal ITC's actual status, MACRS depreciation and Class I RECs, each explained in a self-contained passage rather than scattered across a sales narrative.

Then the part that took the longest. The 35 town pages launched sharing about half their text, because only a description and a short local paragraph varied between them. That is the shape Google's doorway-page handling exists to suppress, and the pages behaved accordingly: high impressions, position 27 to 35, almost no clicks.

Fixing it meant actually researching thirty-five towns. Each page now carries housing stock an installer would recognize (Framingham's Campanelli ranches have wide, low-pitched, uninterrupted roof planes and are among the easiest installs in MetroWest; Saxonville is Victorians with steep cut-up roofs), the real permitting path (which department, the OpenGov portal, whether a local historic district adopted under MGL Chapter 40C puts panel placement in front of a commission first), utility specifics, real municipal programs with the date each figure was confirmed, and town-specific questions with real answers. Every factual claim traces to a URL recorded in that town's sources array, and the citations render on the page.

Across all 528 page pairs, 7-gram text uniqueness went from 50.7% to 78.3%. The worst pair went from 57.0% to 26.8%, and no pair finished above 40%. Median page length went from 725 words to 1,374.

AI architecture

The site was built on the assumption that a growing share of its readers would be machines summarizing it for somebody else, and that those machines need different things than Googlebot does.

FAQ answers render in the server HTML. The accordions on every service page are force-mounted and collapsed with CSS rather than unmounted, so the answer text is present in the document a crawler receives instead of appearing only after a click that no crawler performs. Each set is backed by FAQPage JSON-LD. An answer an assistant cannot read is an answer that cannot be quoted.

llms.txt is generated from the same constants and data modules the pages render from, not maintained by hand. It had been a static file, and it drifted: it advertised "15+ reviews" while the LocalBusiness schema on every page said 14. Assistants read both the markup and that file, so a disagreement between them is a credibility problem on precisely the surface the file exists to serve. Deriving it from shared data means the two can no longer disagree.

The structured data was made honest rather than flattering. The self-serving aggregateRating was removed from LocalBusiness. Review nodes on /reviews reference the organization by @id instead of declaring a second LocalBusiness entity, so the page resolves to one entity with one rating rather than two competing ones. Review counts were aligned to what Google publicly shows.

Accuracy got enforced at the data layer, because an assistant that quotes a wrong claim propagates it. The town template had promised SMART eligibility to every town; SMART is limited to the investor-owned utilities, which made it wrong for Shrewsbury, Concord and Hudson, all served by municipal light plants. It now branches on utility. Federal ITC status was consolidated to one place so no two pages could describe it differently. The rule written into the data files is to omit a field rather than guess at it, and to date any figure that can change.

AI referral traffic is tracked as its own channel in GA4. That is the only reason there is a number to report at all: most sites cannot distinguish an assistant-referred visit from direct traffic, so the channel is invisible to them by default. IndexNow submits new and changed pages on deploy rather than waiting to be crawled.

Performance was treated as part of this rather than separate from it. The h1 animated in from opacity zero, which meant the server-rendered headline, usually the LCP element, was invisible until JavaScript hydrated. Hero backgrounds shipped as unoptimized CSS images. Both were fixed: hero images now go through the optimizer at roughly 50KB of AVIF instead of 971KB of JPEG, and the headline animates from a stylesheet keyframe that runs at parse. Throttled mobile LCP on /residential went from 9,152ms to 1,828ms, /portfolio from 7,840ms to 1,340ms, and the homepage from 7.3s to 4.2s with FCP from 2.9s to 0.9s.

The results

In the first 58 days after launch, the site produced 18 estimate requests from 18 distinct users, against roughly one a month from the site it replaced. That is a visitor-to-lead conversion rate of 4.0%.

Traffic barely moved. Sessions were up about 7% against the matched window a year earlier, and engagement improved modestly, with pages per visit going from 1.67 to 1.93. Almost all of the lead gain came from converting the visitors they already had, not from finding new ones. For a business whose category just lost its federal subsidy, that distinction matters: the improvement did not depend on demand recovering.

The search footprint was built from essentially nothing: 18,708 impressions across 967 distinct queries and 50 pages. Seven sessions arrived through the AI Assistant channel, and the AI referral event fired twice. Seven is a small number and worth naming as one. The point is not the volume; it is that the channel is visible at all, and that a competitor without the same instrumentation is looking at the same visits filed under direct traffic.

What has not happened is ranking. Average position sits at 26.9, page three, and it was flat across all eight weeks measured. There were 128 clicks in 58 days, 48 of them on the brand term. The visibility exists and has not yet converted into traffic.

The near-term shape is legible, though. MetroWest sits at position 8.6, Greater Boston at 11.9, Framingham at 16.5. Those are town pages, the ones that got the depth work, and they are the realistic wins in the next ninety days rather than the statewide head terms sitting on page three. The brand term averages 6.8, meaning the company does not reliably own its own name, which points squarely back at the Business Profile rather than at anything on the site.

Engineering notes

The decisions that shaped this build, and the reasoning behind each. Most of the interesting ones are about restraint: what was deliberately not shipped, and which numbers were thrown out before reporting.

01
Custom Next.js rather than another hosted site builder
The whole engagement is a bet on technical ceiling. Server-rendered FAQ answers, generated llms.txt, per-page JSON-LD, security headers, AVIF, IndexNow and route-level control of the document are not things a template platform lets you reach. Squarespace was not underperforming because it was configured badly.
02
35 researched town pages, not 351 generated ones
Massachusetts has 351 municipalities and generating a page for each was trivially possible. It would also have been an index-bloat machine. The three pages currently closest to page one are all town pages carrying real local research, which is the argument against the cheap version.
03
Uniqueness measured with 7-gram overlap across all 528 pairs
The doorway-page risk is about text similarity between pages, so it needed a similarity number rather than an impression that the pages felt different now. Pairwise 7-gram overlap gave a threshold to work against and a worst case to fix, rather than an average that hides the two pages that are nearly identical.
04
FAQ accordions force-mounted and hidden with CSS
Collapsed content that is unmounted from the DOM does not exist as far as a crawler or an assistant is concerned. Rendering it and hiding it visually costs a little HTML weight and keeps every answer quotable.
05
llms.txt generated from the data the pages already use
The hand-maintained version drifted out of agreement with the schema on the same site. Any AI-facing file maintained separately from the content it describes will do this eventually. Generating it makes the drift structurally impossible rather than something to remember to check.
06
Self-serving aggregateRating removed from LocalBusiness
A rating a site asserts about itself, unattached to a review platform, is the kind of markup that gets discounted at best and treated as manipulation at worst. Review counts were aligned to what Google publicly shows instead.
07
SMART eligibility branched on utility rather than promised everywhere
The town template had asserted SMART for all 35 towns. It applies only to investor-owned utilities, so it was wrong for Shrewsbury, Concord and Hudson. On pages built to be quoted by assistants, a confidently wrong local claim is worse than a missing one, because it travels.
08
Legacy HTTP impressions excluded from every reported figure
About 2,000 impressions were sitting at average position 1 with zero clicks, all on the old pre-HTTPS address, which is Google consolidating the previous URL rather than anything the new site earned. Including them would have inflated total impressions by 10.8% and taught us nothing.
Known limits

Rankings have not moved. Average position is 26.9 and was flat across the eight weeks measured, and 128 clicks in 58 days is a small number by any standard. The lead result is measured and real; the organic traffic result is not there yet. There is also no true search baseline: Search Console was verified at launch and does not backfill, so the only "before" for search is Squarespace analytics. The 18 leads are GA4 form events, counted from 18 distinct users but not de-duplicated against phone calls or reconciled against the client's inbox. The estimate form itself is leaking, at 66 starts to 18 submissions, which is a 27% completion rate and the cheapest available improvement. And the largest remaining constraint on local visibility, the hidden-address Google Business Profile, sat outside this engagement entirely.

The cleanest overhaul I could have asked for.
Matt·Co-owner, East Coast Solar

Tech stack

  • Next.js 15
  • TypeScript
  • Tailwind CSS
  • Vercel
  • Google Solar API
  • Google Places API
  • Resend
  • GA4
  • Search Console
  • IndexNow
  • JSON-LD structured data

Ready to streamline your operations?

Schedule a 15-minute diagnosis call to see if AI can solve your biggest bottlenecks.