Carefree Stone: A Quote Wizard for Two Different Buyers
A five-step estimate form that reads as bespoke for six different service types, backed by a serverless intake pipeline with its own audit trail.
Lead Generation & Edge Infrastructure
~6 Weeks (171 commits, solo)
Solo Full Stack Developer
The business problem
Carefree Stone sells concrete coatings to homeowners (garage floors, pool decks, driveways) and to facility managers and GCs (industrial and municipal flooring) through the same site. Those buyers use different language, trust different signals, and need to answer different qualifying questions — but a form that branches too hard for one audience alienates the other, and a form that's generic enough for both feels like it wasn't built for anyone specifically.
Approach
- 01
Build one five-step wizard, not two separate forms, and let a single Step 1 selection drive personalized copy through every later step.
- 02
Treat client-side validation as a UX layer only — re-validate every field independently on the server, since the client can't be trusted as a security boundary.
- 03
Design the backend for lead durability first: a submission should never be silently lost because an email provider had a bad five minutes.
What I built and changed
A Wizard That Tracks the Choice You Made
A single React component drives all five steps and six service branches — garage, pool deck, driveway, polished concrete, commercial/industrial, and an open-ended 'other' — with a per-service content lookup that swaps sidebar copy, step headers, size-range descriptions, placeholder prompts, and the submit button label.
- Model per-service copy as a typed lookup keyed by service id, not scattered inline ternaries.
- Give the commercial branch its own dedicated logic path (extra fields, intent-driven labels) rather than forcing it into the generic lookup.
- Move the required project description for the open-ended 'other' service to right after it's picked, not three steps later, so the size and condition steps that follow can soften their copy instead of assuming a coating project.
A Server That Doesn't Trust the Client
The Cloudflare Pages Function behind the form independently re-validates everything — required fields, per-field allowlists, length limits, email and phone format — and hardens photo uploads past what the browser reports about a file.
- Verify uploaded images by magic-byte file signature, not just the browser's declared MIME type.
- Rate-limit by IP, check Origin / Sec-Fetch-Site headers, and reject a silent honeypot field without tipping off bots that they were caught.
- Gate submission behind Cloudflare Turnstile and reject anything the server's own copy of the validation rules doesn't accept, regardless of what the client already allowed through.
Lead Durability Over Email Reliability
A submission writes to D1 first, then fires two emails in parallel and non-blocking — an internal team notification and a customer auto-reply with a full project summary — so a Resend outage can never cost the business a lead.
- Store the lead and uploaded photos in D1 / R2 before attempting any email delivery.
- Fire both emails via waitUntil so the request can complete even if delivery is slow or fails.
- Track per-lead notification status as pending / sent / failed in D1, so a failed send can be retried without ever re-running the whole submission.
Business results
Six service branches each carry personalized copy end to end, instead of the four non-commercial, non-'other' services sharing one generic form as they originally did.
Every submission is independently re-validated server-side, and every uploaded photo is verified by its actual file bytes, not its claimed type.
A lead is durably stored before any email is attempted, and delivery status is tracked per-lead so a failed notification is retryable, not lost.
Shipped alongside 16 SEO service pages, 35 city pages, and a 34-post blog — plus a structured content index built for AI answer engines and agentic browsing.
What the work proved
Personalization has to be audited, not just built — a size-range description that correctly said 'warehouse or municipal project' for a commercial lead was silently wrong for a homeowner's pool deck for weeks, because it worked and nobody had specifically checked whether it was consistent.