Rachel Wallis
Project

RGW Heating & Plumbing

Role
Designer and developer, solo
Stack
Next.js, React, TypeScript, Mantine, Sanity, Quote Direct API, Resend, GA4, Vercel
Year
2019–2026
What I owned
Design and build, end to end. The funnel, the content model, measurement, and the first-party integration with Quote Direct.
What I shipped
A live conversion site at rgwplumbing.co.uk. An instant boiler quote on the page, ten town pages, help articles, Google reviews, and GA4 events on every step of the funnel.
RGW homepage on desktop. Headline: A fitted boiler price in 90 seconds. From the engineer who'll actually fit it. Two buttons, Get my boiler price and Call 07969 110679, over a photo of a wall-mounted boiler in a utility room. A badge reads 5.0 on Google, 13 reviews.

Overview

A business-growth site for a two-person plumbing and heating firm in South Hampshire, not a redesign of a brochure. Homeowners searching for a boiler land on a town or service page and can get a fitted price in about 90 seconds, from the engineer who will actually fit it.

The homepage above exists to get a visitor to one of two buttons. The price and the phone number sit at the same weight, because for this firm the call is worth as much as the quote.

The problem

Paid traffic had nowhere honest to convert. Quoting still meant a phone call after hours. The site needed to bring in work, on their own domain, without turning the human call into a booking calendar.

No baseline was measured on the old site. The first 28 days after cutover, to 8 September 2026, gave 25 clicks from 10,400 impressions — a desktop click-through rate of 0.14%. The site ranked first for "boiler installation near me" and took no clicks from it. A lead audit the same month found no genuine enquiries in all 26 rows the funnel had ever recorded. That is the number the rebuild starts from, not one it has beaten.

The user journey

  1. Search "new boiler Eastleigh", land on a town page.
  2. Tap Get my boiler price.
  3. Give a postcode. Out of area ends there, rather than wasting anyone's afternoon.
  4. Answer twelve questions: fuel, current boiler, condition, whether it moves, property, floor, bedrooms, baths, showers, and one optional extra.
  5. Leave an email or a phone number, pick how to be contacted, and get the price. The call is the conversion.
A step in the quote funnel asking Do you know what type of boiler you currently have, with four choice cards: Combi Boiler, System Boiler, Standard Boiler, and I'm not sure. Each card carries a line-drawn icon and a plain-English description.
Every question offers a way out for someone who does not know the answer. "I'm not sure" is a real option, not a dead end — the funnel keeps going and the survey resolves it. No account, no checkout.
Funnel question on a phone: What sort of property do you have? Five illustrated cards — flat or apartment, terraced, semi-detached, detached, bungalow — under a note reading that this decides which follow-up question makes sense and does not change your price. Funnel question on a phone: Want warmer radiators and a longer-lasting system? A paragraph explains what a powerflush does and who needs one, then three options: No thanks, Yes add it plus £550, and I'll decide later. Funnel step on a phone: Where can we send your quote? An email field and a phone field, either one accepted, with a note that the details will not be used for marketing or shared.
Three of the twelve steps, on a phone. The middle one is an upsell that argues against itself — it names who actually needs a powerflush and says the engineer can advise at survey. The last one is the trade the funnel makes: the price arrives after a phone number, not before it.

Design decisions

The call is the conversion, not a booking. For a trusted local firm the survey call is the work. A booking calendar would put a form between the homeowner and the person who does the job. So the funnel ends on a phone number and a callback request, and that is what GA4 counts. It is enforced in platform config rather than convention: Quote Direct carries a bookingEnabled flag, and for RGW it is off.

The price sits behind a contact detail. The funnel asks for an email or a phone number before it shows the number. That costs completions — some people will not hand over either, and leave. It buys the only thing that actually helps a two-person firm: a way to follow up on a quote that was never going to close itself on a Sunday night. The screen says plainly what the details are for, that either one alone is enough, and that they are not used for marketing. If the funnel converts worse than a plain contact form, this is the decision to look at first.

First-party fetch, not an iframe. The quote tool is not embedded. The site calls Quote Direct's funnel API from its own origin, so Google Ads attribution and Quality Score never cross a frame boundary. An iframe path existed and was wired up; it was removed rather than left dormant, because a dormant second path is a thing that gets switched on by accident.

Ten town pages, not forty-three. The site had 43 near-identical location pages generated from one template with the town name substituted in. They were consolidated into ten, behind 308 permanent redirects. Every paragraph on the survivors is genuinely local — the housing stock, the flue routing problems that come with terraces built for the railway works, FAQs that differ by town. A page that says nothing a homeowner could not guess earns nothing.

Reviews sit next to the price. Reviews come from the Google Places API, cached for a day, rendered with the aggregate rating in Plumber schema. They sit by the quote rather than on a page of their own because they move two things at once: map-pack ranking, and whether someone believes the number they have just been shown.

The old RGW homepage in May 2025: orange branding, the headline 'Heating & Plumbing experts you can rely on', a two-item menu, and a Get a Quote button, with a cut-out photo of Rich and Kai. The rebuilt homepage in 2026: green branding over a photograph of a fitted boiler, the headline 'A fitted boiler price in 90 seconds. From the engineer who'll actually fit it', a price button beside a phone number, and a Google rating badge.
May 2025, from the Internet Archive, beside the rebuild. The old hero sells the firm — "experts you can rely on" — and asks you to request a quote. The new one sells the outcome and how long it takes, and puts the price and the phone number side by side. Same business, same two engineers, different job for the page.

How it is built

Next.js 16 on the App Router, React 19, TypeScript, Mantine 8, hosted on Vercel. Help articles and blog content come from Sanity and static TypeScript; towns, services and contact details come from typed modules so nothing is written down twice. Leads leave through Resend. GA4 runs behind a consent banner using Consent Mode v2, so no tag fires before someone says yes.

RGW architecture Browser rgwplumbing.co.uk Next.js on Vercel Quote Direct funnel API Wolseley pricing Sanity — content GA4 Resend — leads same origin live prices
The accent box is the only thing served from rgwplumbing.co.uk. Everything the homeowner sees comes from that origin, including the quote — which is the whole reason the Quote Direct call happens server-side rather than in a frame.

The tenancy seam. RGW is tenant one, and everything the quote engine knows about this business is injected in one file. The engine itself imports nothing from the app.

export const rgwQuoteTenant: QuoteTenantConfig = {
  businessName: business.name,
  phoneDisplay: business.phoneDisplay,
  phoneE164: business.phoneE164,
  primaryColor: 'rgwBlue',
  accentColor: 'rgwOrange',
  leadEndpoint: '/api/quote',
  servedPostcodeAreas: allPostcodes,
  pricing: createRemotePricing('/api/quote/options'),
};

Keeping supplier credentials off the client. Pricing is an interface, not a call site. The browser gets a provider that delegates to a server route, so the Wolseley key never enters the bundle — and a failure returns an empty list rather than an error state, which is what makes the fallback below work.

export function createRemotePricing(endpoint: string): PricingProvider {
  return {
    async getQuotes(answers: QuoteAnswers): Promise<QuoteOption[]> {
      try {
        const res = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ answers }),
        });
        if (!res.ok) return [];
        const data = (await res.json()) as { options?: QuoteOption[] };
        return Array.isArray(data.options) ? data.options : [];
      } catch {
        return [];
      }
    },
  };
}

Production concerns

  • Resilience. When pricing fails the funnel does not show an error, it falls through to a phone number. When the mail key is missing the lead route returns a 503 that names the phone number rather than swallowing the enquiry. A daily cron checks the site is up.
  • Performance. Mobile LCP was the one Core Web Vitals gap left open, and closing it was its own ticket. Rather than quote a number that goes stale, here is the live PageSpeed Insights report — generated when you click it.
  • Security. The supplier credentials never reach the browser. Pricing is requested through a server route, so the key stays server-side and the client only ever sees prices — the reason the PricingProvider indirection above exists at all.
  • Spam. A honeypot and a submit-timing check answer bots with a success response and send no email, so a bot learns nothing about what tripped it. Lead posts are rate-limited per IP.
  • Accessibility. Not signed off, and I will not claim it is. The wizard's choice cards are unstyled buttons, so focus visibility and Enter/Space activation need a keyboard pass; that and a Lighthouse accessibility target of 95 are on the pre-launch checklist, unticked.
  • Maintainability. Phone, email, hours and Gas Safe number resolve from one module. Towns live in one typed file with the redirect map beside them. Richard never edits code.

Repo. Private client work. Happy to walk through it on a call.

Measuring success

The yardstick is genuine enquiries once paid traffic is on: four a month by November 2026, from zero today. A genuine enquiry is a lead with real contact details from inside the service area, or a tracked phone tap, from someone who is not me, not Richard, and not a test.

GA4 fires quote_completed, phone_tap, whatsapp_tap, contact_form_sent and lead_email_sent, so the funnel can be read step by step rather than as one number. Four of those are verified firing on production; the contact form is not yet exercised. A public conversion rate is not honest yet, and Ads have not started — they are gated on the firm's repricing decision, which is not mine to make.

Outcomes

  • A live site that can price a boiler without anyone answering the phone, and hands the caller to a human at the point where a human is worth paying for.
  • Quote Direct exists because this integration needed it to. RGW is tenant one, and the quote UI built here became the design source of truth for that product's funnel — the same screens, ported rather than rebuilt. See the Quote Direct case study.
  • Live merchant pricing proved on a real job in August 2026, which is what made the platform sellable to anyone else.
  • 43 location pages consolidated to 10 behind 308 redirects, and a measurement baseline that exists at all — the first honest starting line this business has had.

What I would do differently

I built 43 location pages from a template and then spent a ticket deleting 33 of them. Ten good pages first would have cost less and ranked sooner.

I built the site before fixing the reviews. A review of the whole business, checked against an independent audit, found the site was never the binding constraint — thirteen Google reviews was. Reviews cost nothing, need no developer, and take about four months to build to a credible number. The engineering was the part I knew how to do, which is not the same as the part that mattered.

An icon map matched option keywords to glyphs, and "Standard boiler" quietly matched a military tank in production. It now renders nothing when a keyword is unknown, and shouts in development. Falling back to a coincidental match is worse than falling back to nothing.

More on where I draw the line between designing and building: On being a product designer who builds.