Market Ready Painting · Local SEO & Marketing
Market Ready Painting
A painting contractor working in Apex, NC since 1995 was invisible to search: the whole site was a client-rendered React SPA that served crawlers an empty div, and a hidden route collected customers' bank account and routing numbers through a third-party email service. I rebuilt it as a 25-page static Astro site on Cloudflare Workers with its own lead pipeline.

- 1 → 25
- Crawlable pages
- −99%
- JavaScript per page
- Removed
- Plaintext bank-detail form
Context
Market Ready Painting is Danny Sweeny’s painting business in Apex, North Carolina — interior, exterior, cabinets and repair work across the Triangle since 1995. Customers find a painter by searching “painter near Apex NC,” which made the state of the website a direct commercial problem rather than a cosmetic one.
The site was a Vite and React Router single-page app on Vercel: a 257 KB bundle that rendered one hardcoded page entirely in the browser. The HTML actually served to crawlers was a 468-byte shell around an empty <div id="root"> — no description, no canonical, no structured data, not even an <h1>. Below 768px the navigation was simply hidden with display: none, so on a phone there was no way to move around the site at all.
The problem
Three things had to be fixed at once, and they pulled in different directions.
Search. One page cannot rank for ten services across eight towns. The fix is a real multi-page site with server-rendered content — which is exactly what a client-rendered SPA is structurally unable to provide.
A live security liability. An unlinked /billing route collected customers’ bank account and routing numbers and shipped them as plaintext email through EmailJS, with the service ID and public key sitting in the client bundle for anyone to read. That had to go before anything else shipped.
No photography. There were no job photos, and there was no budget or timeline to shoot any. Most contractor sites are carried by before-and-after galleries; this one had to look established without a single image of the work.
What I built
- Twenty-five statically rendered pages on Astro, deployed to Cloudflare Workers with static assets. Ten service pages and eight service-area pages are generated from Zod-validated Markdown collections, so adding a service means dropping in one file — the page, the index entry and the FAQ structured data all follow from it.
- A design system made of paint. With no photography available, the palette became the imagery: six named swatches — Hollow Rock, Jordan Blue, Beaver Creek, Tobacco Gold, Apex Brick, Porch Linen — colour the service cards, the strip under the hero and the logo mark. Each swatch carries the text colour that clears 4.5:1 on it, so contrast is a property of the token rather than something to remember.
- A JavaScript budget, enforced. Every content page ships 1.1 KB gzipped; React loads only on
/contact/, for the form. The mobile nav was deliberately not built as an island — as one it pulled the React client onto all 25 pages, which would have made it barely better than the SPA it replaced. - A first-party lead pipeline replacing EmailJS entirely:
POST /api/contactruns a content-type guard, a per-IP KV rate limit, Turnstile verification and validation, writes the lead to D1, then fires the notification email through Cloudflare Email Service inwaitUntil— so a mail failure can never turn a captured lead into an error page./billingnow 301s to/contact/. - Security headers as build output. HSTS,
nosniff,X-Frame-Options: DENY, Referrer-Policy and Permissions-Policy ship with the assets, and a post-build step derives a hash-based Content-Security-Policy from the inline scripts each build actually emits, rather than trusting a hand-maintained list to stay accurate. - Claims checked against their sources. The 5.0 rating is attributed to Houzz because that is where it comes from — an earlier draft cited Angi, which shows 4.0. “Licensed & insured” came off the site because there is no licence number to cite. BBB references were removed, since a listing page existed but the business is not accredited. Reviews stay gated behind an
approvedflag until there is permission to republish them.
Outcome
The business went from one page no search engine could read to 25 indexable ones, each targeting a service or a town people actually search for, on infrastructure that costs nothing to run at this traffic. Leads land in a database the owner controls instead of a third-party inbox, and the route that was collecting bank credentials in plaintext no longer exists.