All case studies
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.

Role
Design and full build through ShanTech
When
2026
Stack
Next.js 14, TypeScript, Supabase (Postgres), Typed data files
Links
Live site ↗Client repo, private. Walkthrough on request.
  • 18
    page templates
  • 33
    towns with real projects
  • 3
    service 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 }
}
Category-aware deposits with an override, rounded up so the business is never short. Built into the order API.
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))
}
The 'towns we have worked in' claim is computed from the project records, so the number cannot drift from the evidence.
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.
$ ./hire --re elatec-safety-systems

Want a build like Elatec Safety Systems?

Senior remote roles and contract builds. Replies within a day, EAT (UTC+3).