Lead-generation site for an outsourced HR & recruitment service for UK SMEs. One flow, one action: book a 15-minute call.
Built from the brief set in docs/. Read VISION.md first —
it's the design constitution and carries the thirteen rules everything answers to.
DECISIONS.md records the judgment calls; BUILDLOG.md
records what was built and what the critique loop caught;
MILESTONES.md tracks what is shipped and what still stands
between here and a live site taking real enquiries.
Run it
npm install
npm run dev # http://localhost:3000npm run build # production build, all routes static
npm run verify # build + the copy compliance gateStack: Next.js 16 (App Router) · React 19 · TypeScript strict · Tailwind CSS
v4 · lucide-react. Type is Space Grotesk / Instrument Sans / Space Mono,
self-hosted at build time by next/font — no runtime request leaves the origin.
Re-brand it
The company name isn't final (brief §3). Everything that changes with it lives in one file:
lib/site.ts
Change brand.name / brand.nameLead / brand.nameTail and the wordmark, page
titles, OG card, JSON-LD, manifest and body copy all follow. Nothing hard-codes
the name. nameTail is null for a one-word name, and the accent falls to the
globe mark instead of to the second word.
brand.tagline is the slogan ("Let's grow together.") and appears on the page.
brand.descriptor is what the company does, and it is what carries the
<title> and the OG tags — a slogan in a search result says nothing.
docs/still says RIZAD Global. That is the founders' supplied material, kept verbatim as the record of what was approved — it is not a rename that was missed.
Colour and type live in the @theme block at the top of app/globals.css —
the one place any hex appears. Change --color-accent and the whole site
re-tones, globe included.
Several palette values are set by their tightest ground rather than by taste:
--color-softand--color-goldboth clear WCAG AA by their smallest margin on--color-panel-deep, the deepest band, and would fail there first if lightened. They're annotated inglobals.css; keepnpm run check:diagpassing rather than reverting them.
Open items — these are null on purpose
Unknowns are null, never a placeholder string, so nothing fake can ship. A
null hides its row; the page degrades honestly. All of these live in
lib/site.ts:
| Item | Field | Effect while unset |
|---|---|---|
| Final name & domain | brand.name, NEXT_PUBLIC_SITE_URL |
Ships as "Rizqad"; sitemap/OG use localhost |
| Email · phone · LinkedIn | contact.* |
Footer contact column and "rather email" block are hidden |
| UK entity, company no., VAT | legal.* |
Footer shows the brand name only; no registration is claimed |
| Scheduling URL | scheduling.calendly |
The enquiry form is the CTA; no embed renders |
| Analytics domain | analytics.plausibleDomain |
No script loads, even if a visitor accepts |
| Trial length | trial.weeks |
Defaults to 2 weeks; every mention derives from this number |
| Logo | — | components/layout/Logo.tsx is a drawn globe mark plus type |
| Published pricing | commercials.public |
false — no surface shows a figure. See below |
The enquiry form does not deliver yet. With contact.email null, the route
handler returns 503 and logs the undelivered enquiry to the server — loudly, on
purpose, because a silently swallowed lead is worse than a visible error. Pick a
provider (Resend is the precedent in the other repos) and wire it at the marked
insertion point in app/api/enquiry/route.ts.
Three things to raise with the founders
- No price is published. The pricing table was replaced by the role
catalogue, and with no pricing surface to explain it the hero's "from
£1,200" went too. The approved figures are intact in
site.commercialsbehindpublic: false; nothing reads them, including the JSON-LD. Restoring the table is a flag flip plus a component. The site now says "one flat monthly fee, no recruitment fee, no long-term contract" and quotes against the real role on the call — which the FAQ states plainly. - Pricing vs the costing model, whenever a figure does go public. The
brief (§5) and the pitch document both approve £1,200 / £2,200. The costing
model in
docs/master-research-summary.md§7 runs its P&L on £1,500 / £2,500 — so the per-seat economics behind the ~£123k/yr headline are materially thinner at the approved prices. - PI insurance for the skilled tier. Research §7 flags professional indemnity cover as still outstanding for legal/accounting support work. The copy is written to the safe side of that line (support, never advice) but the tier shouldn't be sold hard until cover is in place.
The role catalogue is padded. site.serviceGroups ships six families and
sixty titles chosen to look like a plausible SME back office. The founders
should strike anything they will not actually staff — a role we cannot fill is
a lead we lose on the call rather than one we never got.
The rules are enforced, not remembered
Three of the brief's constraints are liability positions that live entirely in wording, so they're machine-checked:
npm run build && npm run check:copy # rules 1–4 + leftover placeholderstools/copycheck.mjs scans the built HTML and exits non-zero on: worker-directory
language (brief §8), "advice"/"legal services" (brief §5 + research §7), anything
implying the client is the employer (research §1's IR35 note), cost-arbitrage
claims from the retired positioning, and placeholders like [your email].
npm run dev & # or next start
node tools/diag.mjs http://localhost:3000 1440x900
node tools/diag.mjs http://localhost:3000 390x844tools/diag.mjs asserts the 8px rhythm, the ≤10% accent ceiling, and WCAG AA
contrast on every text element of every route.
node tools/shot.mjs http://localhost:3000/ shots/home.png 1440x900 0 1200 --full
node tools/shot.mjs http://localhost:3000/ shots/home-reduced.png 1440x900 0 1200 --reducedtools/shot.mjs screenshots and asserts console errors are []. It exits
non-zero if any appear. The only expected error anywhere is on the 404 route —
the document's own HTTP 404.
Deploy
Host-agnostic by rule 9 — nothing in the repo names a host. There is no
vercel.json, no netlify.toml, and no adapter: the build is stock
next build, eleven routes prerender as static HTML, and /api/enquiry is the
one dynamic route, which any Next-aware host turns into a function on its own.
Set one environment variable, on any host, so canonical URLs, the sitemap,
robots.txt and the OG card point at the real domain instead of localhost. See
.env.example:
NEXT_PUBLIC_SITE_URL=https://your-domain.example
Vercel (current target)
Import the GitHub repo — the framework preset, build command and output directory are all detected. Nothing needs overriding.
- New Project → import this repo. Leave every build setting at its default.
- Environment Variables → add
NEXT_PUBLIC_SITE_URLfor Production, Preview and Development. Until a domain exists, Production can hold the*.vercel.appURL — the value only has to be true, not final. - Deploy. Then, once a domain is bought: Settings → Domains → add it, and
correct
NEXT_PUBLIC_SITE_URLto match. Metadata is derived, so the whole site follows from that one edit.
Redeploy after changing the variable — it is inlined at build time, like every
NEXT_PUBLIC_* value.
Two things to know before the site takes traffic:
- The enquiry form still returns 503 until
contact.emailis set inlib/site.tsand a delivery provider is wired at the marked insertion point. Deploying does not change that; see the open items above. - The security headers in
next.config.tsare served by the Next runtime, not by host config, so they survive the move to any other host.
Moving off Vercel later means changing nothing in this repo.
What this site will never do
It is a managed service, not a marketplace. There is no worker directory, no worker profiles, no worker search, and no worker contact details — and no worker data model in the codebase at all. Candidate profiles are shared privately after a call. That's brief §8, and it's the business model, not a preference.
