Blog//7 min read
Plumber website template for Next.js: the 10 blocks that get calls
A plumber website template for Next.js built from 10 blocks: local hero, ZIP checker, quote form and a mobile call bar. Block order and code included.
MSMarco Schulz, founder of Facade UI
A plumber's website has one job: turn a person with water on the floor into a phone call. Everything on the page either helps that or gets in the way. I have built enough sites for trades businesses to know that the layout is not where the time goes. The time goes into the five or six sections that answer the visitor's questions before they bounce back to the search results. This is the page plan I use now, built from Facade blocks, and the reasoning behind each one.
What a plumber visitor actually wants
Think about who lands on the page. Some have an emergency: a burst pipe, a water heater that quit on a Sunday. Others are planning a remodel or a repipe. Both groups ask the same four questions, in roughly this order:
- Is someone available right now, or at least today?
- Do you come to where I live?
- Can I trust you in my house?
- What will it cost, roughly?
A template that answers those four questions above the fold and in the first two scrolls will beat a prettier one that does not. The ten blocks below map to the questions, and the order is the order of the questions.
The block order
This is the sequence I ship for a plumbing company, top to bottom. The same list is the default on the plumber industry page, where you can see the stacked preview.
- Hero, local business
- Stats band
- Process steps
- Service area
- Testimonials wall
- FAQ section
- Multi step lead form
- CTA section
- Footer, big
- Sticky contact bar (mobile only, floats over everything)
1. Hero with a live answer to "are you available"
The Hero, local business block is built for exactly this page. It shows the star rating and review count up top, a headline with a rotating service word, a primary button, a phone link and three trust items. The floating cards on the right show an appointment card, a review and a technician card with an ETA, which is the visual version of "yes, someone is coming."
The rotating words matter more than they look. A plumber does drain cleaning, water heaters, leak repair and remodels, and a static headline can only name one. The rotatingWords prop cycles through them.
import { HeroLocalBusiness } from "@/components/facade/hero-local-business";
<HeroLocalBusiness
rating={4.9}
reviewCount="612"
eyebrowSuffix="Google reviews in Austin"
headlinePrefix="Same day"
rotatingWords={["drain cleaning", "water heater repair", "leak detection", "repiping"]}
headlineSuffix="by licensed plumbers"
description="Upfront pricing, two hour arrival windows and a 2 year warranty on every repair."
primaryCta={{ label: "Book a visit", href: "#quote" }}
phone={{ label: "(512) 555-0142", href: "tel:+15125550142" }}
trustItems={["Licensed and insured", "No overtime charges", "Family owned since 2004"]}
technician={{ name: "Mike R.", eta: "Arriving in 25 min", vehicle: "Van 7" }}
interval={2600}
/>2. Stats band: trust in numbers
Years in business, jobs completed, average rating, response time. Four numbers that count up when they scroll into view. Only use figures the client can defend.
3. Process steps: what happens after I call
Call, diagnose, fixed price quote, repair. The connecting line draws as the visitor scrolls, and each step lights up as it passes. For an emergency visitor this is the section that lowers the heart rate.
Service area with a ZIP checker
"Do you come to my town" is the second question, and a paragraph listing suburbs does not answer it. The Service area block does two things: it shows a stylized SVG map with numbered zones, and it offers a ZIP code checker. The visitor types five digits, hits Enter, and gets a yes with the zone's ETA or a polite no with a waitlist link.
Zones are plain data. Each one has a name, an SVG path in a 400 by 320 viewBox, a label position, a list of cities and a list of ZIP codes. The checker does a zones.find(z => z.zips.includes(zip)) lookup, nothing more, which means it works without any API and the data lives next to your city pages.
import { ServiceArea, type ServiceZone } from "@/components/facade/service-area";
const zones: ServiceZone[] = [
{
id: "central",
name: "Central Austin",
path: "M156 172 L190 214 L248 200 L270 252 L214 296 L140 284 L118 228 Z",
labelX: 190,
labelY: 250,
cities: ["Zilker", "Travis Heights", "Bouldin Creek"],
zips: ["78704", "78745", "78748"],
eta: "Same day",
},
];
<ServiceArea
heading="Serving all of Austin and Round Rock"
zones={zones}
hqLabel="Shop on Burnet Rd"
checkerLabel="Do we come to you?"
successMessage={(zip, zone) => `Yes. ${zone.name} gets ${zone.eta?.toLowerCase()} service.`}
failureMessage={(zip) => `${zip} is just outside our area right now.`}
ctaLabel="Book a visit"
ctaHref="#quote"
/>The Service area is a Pro block, so it installs from the private registry after the one time license setup described in the installation docs:
npx shadcn@latest add @facade/service-areaReviews, FAQ and the quote form
The testimonials wall and the FAQ section answer question three. Real reviews with first names and neighborhoods, and an FAQ that covers "do you charge for estimates", "are you licensed", "do you offer financing". The FAQ block can also emit FAQPage JSON-LD from the same array that renders the accordion, which is how those answers end up in search results.
Question four, cost, is where most plumber sites lose the lead. A blank textarea labeled "Describe your issue" is work for the visitor. The Multi-step lead form replaces it with three steps: pick the service from option cards, pick a timeline and enter your ZIP, then name, email and phone. Each step validates before it moves on, the progress bar is a real role="progressbar", and the form slides in the direction of travel.
import { MultiStepLeadForm, type LeadFormData } from "@/components/facade/multi-step-lead-form";
<MultiStepLeadForm
title="Get a fixed price quote"
timelines={["Today", "This week", "Within a month", "Just planning"]}
onSubmit={async (data: LeadFormData) => {
await fetch("/api/leads", { method: "POST", body: JSON.stringify(data) });
}}
successTitle="Got it"
successMessage="A dispatcher will call you back within the hour during business hours."
/>LeadFormData gives you service, timeline, zip, details, name, email and phone, with the phone stripped to digits. Post it to your CRM, a webhook, or an email route.
The mobile call bar
On a phone, visitors scroll past the hero and lose the phone number. The Sticky contact bar fixes that with four actions pinned to the bottom edge: Call, Text, Directions, Book. It appears after a short scroll, tucks away when the visitor keeps scrolling down and comes back the moment they scroll up. The mobileOnly prop adds md:hidden, so desktop visitors never see it.
import { StickyContactBar } from "@/components/facade/sticky-contact-bar";
<StickyContactBar
phone="(512) 555-0142"
address="5600 Burnet Rd, Austin, TX 78756"
bookingUrl="#quote"
threshold={240}
mobileOnly
/>It is a free, MIT licensed component:
npx shadcn@latest add https://facadeui.com/r/sticky-contact-bar.jsonMeasuring calls
A plumber does not care about sessions. They care about calls. Two things I wire up on every site:
- Every
tel:link fires an analytics event. The sticky bar, the hero phone link and the footer phone count as separate sources, so the client sees which one earns its place. - The lead form's
onSubmitfires a second event with the chosen service, which tells you which services the website brings in.
I keep one phone number everywhere and track clicks instead of swapping in tracking numbers, which can hurt the Google Business Profile match. Ship these ten blocks, swap the copy and the colors, and the layout work is done.
FAQ
Can I use these blocks with a CMS?
Yes. Every block takes plain props, so the content can come from Sanity, Contentful, a Markdown file or a database. Map your CMS fields to the props in a server component and pass them down.
Do I need all ten blocks?
No. A two person shop can drop the stats band and the testimonials wall and still have a page that converts. Keep the hero, the service area, the lead form and the sticky bar; those four answer the four questions.
Does the ZIP checker work for Canadian postal codes?
Not out of the box. The checker validates five digits before it looks up the zone. The source is yours after installation, so the regex and the zips matching are a one line change.
Components in this article
- Hero, local businessFreeHome services hero with live rating, rotating service words and floating appointment, review and ETA cards.
- Service areaProZone map with numbered service areas, city and ZIP lists, and a ZIP code checker with arrival windows.
- Sticky contact barFreeMobile bottom bar with Call, Text, Directions and Book that hides on scroll down and returns on scroll up.
- Multi-step lead formProThree step quote request with option cards, validation, progress bar and direction-aware transitions.