ProjectLive

Rizqad

Next.jsTailwind CSSTypeScriptJavaScript
Rizqad — screenshot
Live · from the repository
Last push
9h ago
Stars
0
Primary
TypeScript
TypeScript 71%CSS 21%JavaScript 8%

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:3000
npm run build        # production build, all routes static
npm run verify       # build + the copy compliance gate

Stack: 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-soft and --color-gold both clear WCAG AA by their smallest margin on --color-panel-deep, the deepest band, and would fail there first if lightened. They're annotated in globals.css; keep npm run check:diag passing 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

  1. 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.commercials behind public: 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.
  2. 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.
  3. 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 placeholders

tools/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 390x844

tools/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 --reduced

tools/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.

  1. New Project → import this repo. Leave every build setting at its default.
  2. Environment Variables → add NEXT_PUBLIC_SITE_URL for Production, Preview and Development. Until a domain exists, Production can hold the *.vercel.app URL — the value only has to be true, not final.
  3. Deploy. Then, once a domain is bought: Settings → Domains → add it, and correct NEXT_PUBLIC_SITE_URL to 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.email is set in lib/site.ts and 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.ts are 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.