The complete public Codezela website, rebuilt with Next.js 16, React 19, TypeScript, Tailwind CSS 4, and Bun. It includes the homepage, top-level company pages, all industry pages, all portfolio case studies, legal pages, local media, technical SEO, and the proposal email workflow.
The application contains 66 indexable public routes:
- Home, Services, Portfolio, Industries, About, and Contact
- 40 industry detail pages under
/industry/[slug] - 18 portfolio case studies under
/portfolio/[slug] - Privacy Policy and Terms and Conditions
/ca is a permanent redirect to cca.it.com. Internal navigation, cards, footer links, and detail links remain inside this application.
- Next.js 16 App Router and static generation
- React 19 and TypeScript
- Tailwind CSS 4
- Next.js Image and Font optimisation
- Motion, Embla Carousel, Lucide, and React Icons
- Next.js Third Parties for performance-conscious Google measurement
- Resend for proposal notifications and submitter confirmations
- Zod for server-side form validation
- Bun as the only package manager
Requirements:
- Bun 1.3.14 or newer
- Node.js 20.9 or newer, as required by Next.js 16
Install and start the development server:
bun install
bun run devTo use the project’s usual local port:
bun run dev -- --port 3100Then open http://localhost:3100.
Copy the example file and add server-side credentials locally:
cp .env.example .env.local| Variable | Purpose |
|---|---|
RESEND_API_KEY |
Server-only Resend credential used by the proposal API |
PROPOSAL_FROM_EMAIL |
Verified sender identity for notification and confirmation emails |
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
Public Cloudflare Turnstile widget key used on the final proposal step |
TURNSTILE_SECRET_KEY |
Server-only Turnstile key used by the proposal API for Siteverify validation |
GENIE_APP_KEY |
Server-only API Key (secret) from the Codezela Invoice Payments Genie application |
GENIE_API_BASE_URL |
https://api.geniebiz.lk for production |
PAYMENT_SESSION_SECRET |
Private random signing secret, at least 32 characters, kept stable across deployments |
GENIE_APPLICATION_ID |
Application ID from Codezela Invoice Payments, used to validate payment webhook ownership |
Never commit .env.local, API keys, or provider credentials. The committed .env.example contains names and safe placeholders only.
For local browser testing, place Cloudflare's published always-pass test pair in .env.development.local. This keeps production keys restricted to the live host while allowing the final proposal step to work predictably on localhost; production builds still read the real values from .env.local or the deployment environment.
Share /payment directly with invoice recipients. It is unlisted, excluded from the sitemap, and marked noindex through metadata and response headers. Crawlers may fetch the HTML to read noindex; API routes are blocked in robots.txt. This controls indexing, not access to the form.
The payer enters an invoice reference and the exact amount in LKR. The server validates the input and Turnstile challenge, creates a Genie hosted checkout with integer cents, and stores a signed, HttpOnly browser cookie. /payment/result and /api/payments/receipt query Genie again and match the transaction, amount, currency, and invoice reference before confirming a payment. Only CONFIRMED transactions receive a PDF receipt. Pending, authorized, failed, cancelled, mismatched, and unavailable responses do not issue a receipt.
There is no local invoice database or payment ledger. The amount is entered by the payer, so this page cannot look up an invoice balance or prevent a separate payment against the same invoice. Genie retains the gateway transaction record. The browser session expires after seven days; download the receipt while the session is available. The in-memory rate limit is per server instance, with Turnstile providing the shared bot check.
For Vercel, add production values from the local .env.local to the Production environment before deploying. Set GENIE_APPLICATION_ID, GENIE_APP_KEY, GENIE_API_BASE_URL, and PAYMENT_SESSION_SECRET, and retain the existing Resend and production Turnstile variables. Do not copy .env.development.local test keys into production. Keep all secret variables server-only, without a NEXT_PUBLIC_ prefix. The only public key is NEXT_PUBLIC_TURNSTILE_SITE_KEY. If creating a new signing secret, use openssl rand -hex 48; rotating it expires existing receipt sessions.
Every checkout registers https://codezela.com/api/payments/webhook with Genie. The signed webhook accepts Genie's nested event envelope and legacy flat format, queries Genie again, checks the application, amount, currency, invoice, and transaction reference, and sends a clean payment confirmation to info@codezela.com with sayuru@codezela.com CC'd. It uses the existing Resend key and sender. Email delivery failures return an error to allow webhook retries. Resend's transaction-specific idempotency key suppresses duplicate sends within its 24-hour retention window; there is no permanent local email ledger. No extra global webhook setup or scheduler is required. The return URL is https://codezela.com/payment/result, matching the registered live application domain, so the deployed return and webhook flow needs this version running on that domain.
The payment webhook is a server-to-server endpoint: browser challenges or hosting access rules must not block legitimate signed Genie requests. If Genie shows HTTP 403 in Webhooks > Logs, inspect the corresponding Cloudflare Security Event or Vercel firewall log and narrowly adjust the matching rule for POST /api/payments/webhook. Do not disable site-wide protection or remove webhook signature verification. Other handler diagnostics: 401 means invalid/missing signature, 400 invalid payload, 409 provider ownership/details mismatch, and 503 missing configuration or provider failure. Application logs record safe failure categories and accepted Resend email IDs, never credentials or full form payloads. A Resend acceptance ID is not proof of inbox delivery: check the email's delivery event in Resend.
After deployment, complete a controlled real payment and confirm the return page and downloaded receipt. API checkout creation and automated tests do not prove card authorization, settlement, or the deployed browser return flow.
Local development inherits the Genie configuration from .env.local, including live mode. Only the Cloudflare test pair is overridden locally. Use a separately provisioned UAT key and https://api.uat.geniebiz.lk if sandbox payments are required. Published Turnstile dummy responses are accepted by payment and proposal endpoints only in development on a loopback hostname, after a successful Siteverify response explicitly identifies the test key. Local proposal submissions still send real emails through the configured Resend account.
POST /api/proposals validates requests on the server, rejects oversized or invalid payloads, applies honeypot and same-site checks, rate-limits known IP addresses, validates a single-use Cloudflare Turnstile token, and sends two clean emails through Resend:
- An internal notification to
info@codezela.com, CCsayuru@codezela.com, with reply-to set to the submitter - A confirmation to the submitter, with reply-to set to
info@codezela.com
Email delivery requires a verified sender domain and valid production environment variables. A local build verifies the code path but does not prove live provider delivery.
The contact page and other proposal buttons share this same form and endpoint. There is no separate unprotected contact email handler.
Turnstile is explicitly rendered only on the final proposal step. Tokens are checked through Cloudflare Siteverify with the visitor IP when available, the proposal_submit action, the request hostname, an eight-second timeout, one safe retry, and a per-submission idempotency key. Expired, rejected, or consumed tokens are refreshed without discarding the visitor’s form details.
The site preserves the existing Google Analytics 4 property (G-NYCH8NBNH1) and Google Ads destination (AW-941011769) through one Google tag integration. Consent Mode v2 defaults optional storage to denied, and measurement scripts load only after a visitor accepts optional cookies.
- The consent preference is retained for 180 days and can be changed through Cookie Preferences in the footer.
- Rejecting optional cookies keeps Google measurement scripts unloaded on a fresh visit and removes known first-party Google measurement cookies when possible.
- Successful proposal delivery records a GA4
generate_leadevent without names, email addresses, phone numbers, project descriptions, submission references, or other form values. - GA4 enhanced measurement handles normal page loads and browser-history page changes; do not add duplicate manual page-view events unless the GA property is reconfigured accordingly.
Run the complete local code gate before handing off a change:
bun run lint
bun run typecheck
bun run test
bun run buildFor rendered changes, also test the affected route in production mode, exercise the interaction, check desktop and mobile layouts, review browser errors, and run Lighthouse against the production build.
src/
├── app/ App Router pages, metadata, sitemap, robots, and API
├── components/
│ ├── home/ Shared header, footer, homepage sections, and proposal UI
│ ├── pages/ Top-level and detail-page compositions
│ └── shared/ Reusable motion, carousel, metrics, FAQ, and SEO components
├── data/ Route content, FAQ profiles, industry visuals, and metrics
└── lib/ Email, validation, and structured-data helpers
public/
├── images/ Local, responsive website imagery
└── seo/ Favicons and social-sharing artwork
The site provides route-specific titles and descriptions, canonical URLs, Open Graph and X metadata, crawlable text, semantic headings, robots.txt, an XML sitemap, and JSON-LD for the relevant page type, breadcrumbs, projects, and visible FAQs. Legacy WordPress sitemap endpoints permanently redirect to the current /sitemap.xml, preserving compatibility with old submissions and crawler history.
FAQ structured data must always match the questions and answers visitors can read. Structured data helps machines understand a page, but it is not a promise of a rich result or a search ranking.
- Public media is stored locally; the website does not depend on the previous WordPress media library at runtime.
- Content images use
next/image, explicit aspect ratios, responsivesizes, lazy loading below the fold, and higher fetch priority only for genuine above-the-fold content. - Industry panel photography is stored as consistent 1280×720 WebP assets and delivered responsively by Next.js.
- The optimizer is restricted to local image paths, the site’s real responsive width buckets, and two approved quality levels to prevent wasteful transformations without disabling adaptive image delivery.
- Industry photography was sourced through Unsplash search and is governed by the Unsplash License. Review third-party asset rights before redistribution outside this project.
Before a production release:
- Run the full quality gate and rendered browser checks.
- Confirm all environment variables are configured on the intended deployment project.
- Verify the Resend sender domain and send one real end-to-end proposal test.
- Restrict the production Turnstile widget to
codezela.comandwww.codezela.com, then complete one real challenge and confirm a valid Siteverify event in Turnstile Analytics. - Check canonical URLs, sitemap, robots, redirects, and social images on the deployed origin.
- Accept and reject optional cookies in a clean browser session, then confirm GA4 Realtime/DebugView and Google Ads receive only the expected consented events.
- Run Lighthouse and link crawling against the deployed URL; local scores do not prove production CDN or third-party performance.
- Confirm no secrets, local reports, screenshots, generated audits, or unrelated files are staged.
This repository is Bun-only. Keep bun.lock as the sole package-manager lockfile and use bun run, bunx, and bun install for all dependency and script work. See AGENTS.md for the implementation contract.