04Client · Local service + shop
Elatec Safety Systems
A service business site with a real shop, bookings, a WhatsApp order hand-off, a towns-served index computed from real project records, and structured data written by hand for local search.
- 18page templates
- 33towns with real projects
- 3service pillars
01
The problem
- A company with 15+ years of installs had no way to sell equipment, take deposits or show proof of work online.
- Three very different services (security, solar, shading) had to read as one brand.
02
Architecture
ContentTyped data files for services, products, projects and the blog, so every page is static and fast.
ProofA towns-served index whose counts are computed from the project records, so the claim cannot drift from the evidence.
CommerceCart and checkout hand off a full order summary to WhatsApp. An order API with staged events is built, ready to switch on.
MessagingNotifications try WhatsApp first and fall back to Africa's Talking SMS.
SearchHand-built schema.org JSON-LD for the organisation, services and products.
03
Key decisions
01
Deposits by product category
Installs need commitment up front: 30% by default, 40% for solar and gate automation.
TradeoffThe rule lives in the order API, which is not switched on yet; the live checkout hands off to WhatsApp today.
02
Leave out what is not real
Social profiles that did not exist yet were kept out of the structured data instead of being invented.
TradeoffWeaker social signals until the client sets up real profiles.
04
Real code
src/lib/utils/depositCalc.tsexcerpt
const DEFAULT_DEPOSIT_PCT = 30
const CATEGORY_DEPOSIT_PCT = { solar: 40, gate_automation: 40 }
export function calcDeposit(subtotal, category?, overridePct?) {
const pct = overridePct
?? (category ? CATEGORY_DEPOSIT_PCT[category] : undefined)
?? DEFAULT_DEPOSIT_PCT
const deposit = Math.ceil((subtotal * pct) / 100)
return { deposit, balance: subtotal - deposit, pct }
}src/data/projectsData.tsexcerpt
export function getAllLocations() {
const counts = new Map<string, number>()
for (const p of PROJECTS_DATA) {
counts.set(p.location, (counts.get(p.location) ?? 0) + 1)
}
return Array.from(counts.entries())
.map(([location, count]) => ({ location, count, region: getProjectRegion(location) }))
.sort((a, b) => b.count - a.count || a.location.localeCompare(b.location))
}05
How it was verified
- Structured data only states what exists: missing social profiles are left out, not invented.
- Town and region counts are derived from project records at render time.
06
Results
- Shop, bookings and project proof live under one brand.
- Real projects listed across 33 towns and counties.
07
What I'd do next
- Switch checkout onto the order API with private tracking links.
- Real social links in the footer and structured data.
- Server-render the stats so they never show zero.

