A Singapore home-maintenance company had a complete Figma design and no website.
The brief was pixel-perfect — the client would be comparing the build against thedesign frame by frame. But the handoff had gaps. The exported JPGs covered sevenscreens; the Figma file actually contained twelve, including Services, About,Gallery, FAQ and Contact pages that were never exported. The design alsospecified four licensed typefaces the client hadn't purchased.
On top of that, the site needed to be genuinely maintainable. This is a smallbusiness that will want to add a service, change a phone number, or reordertheir trades without hiring a developer back for every edit.
- 0110 pages pre-rendered as static HTML — nothing rendered at request time
- 02Four service pages generated from a single template and one data file
- 03Design tokens pulled straight from Figma variables, not eyeballed from screenshots
- 04WhatsApp-first enquiry flow — the form opens a pre-filled chat instead of needing a backend
- 05Responsive from 320px up, verified at eight viewport widths
- 06All copy lives in two data files, so content edits never touch JSX Per-page SEO metadata and semantic heading structure
- 07Keyboard-operable FAQ accordion with correct aria-expanded state
Recovering the full design. Rather than build only what was exported, I pulledthe file directly through the Figma API and found five pages missing from thehandoff. I also pulled the real design tokens — #FF780A primary, #FFF7E9background, the full type scale — instead of sampling colours from compressedJPGs, and downloaded every source photo at full resolution rather than croppingthem out of page exports.
Fonts that didn't exist. The design called for "ZT Nature" and three otherlicensed faces. I wired the stack so headings fall back to Inter — which is whatthe client's own Figma exports actually render — and left an @font-face rulepointed at /public/fonts. When the licence is bought, dropping in one .woff2file switches every heading with no code change.
Catching what looked fine. Reviewing screenshots isn't enough for "pixelperfect", so I scripted a Playwright sweep across eight widths from 320px to1440px that fails on any horizontal overflow. It caught four real defects I'dotherwise have shipped: a hero badge stretched full-width instead of hugging itstext, CTA buttons that couldn't fit side by side on an iPhone SE, a footer emailbreaking mid-word, and a stat figure overflowing its cell on mobile.
A bug in the source design. The Figma file labelled one FAQ "Is my employee datasecure?" but answered it with copy about emergency response times — a leftoverfrom a template. I shipped the corrected question and flagged it rather thanreproducing an obvious mistake in the name of fidelity.
Next.js 16 App Router, fully static. Every page is pre-rendered at build time,including the four service pages, which come from generateStaticParams over asingle data array.
The core decision was content-as-data. All site copy lives in two files —site.ts for business details and shared FAQs, services.ts for everything on theservice pages. The route, the homepage card, the footer link and the staticbuild entry for a service all derive from one object in that array, so adding afifth trade means appending one object, not creating a page.
That paid off when the client asked to make plumbing their lead service: theServices page, homepage cards, footer menu and build output all reordered from asingle array change.
Design tokens are declared once in globals.css under Tailwind v4's @theme, sothe Figma palette is available as ordinary utility classes (bg-cream,text-primary, border-line) and there are no hex codes scattered throughcomponents. Sections that appear on more than one page — the about band, themission/vision block, the feature grid — are single components imported by both,so an edit updates every instance.
The contact form has no backend by design: it composes a pre-filled WhatsAppmessage, matching the design's "Send via WhatsApp" button and the way thisclient actually receives enquiries.