Engineering case study · 2025–2026

Noura Lawyers — a trilingual UAE legal portal on the edge.

Public marketing site + admin CRM for a Dubai law firm. Cloudflare Pages + Workers + D1, trilingual (EN/AR/RU) with RTL-native Arabic typography, 49+ practice pages, and a lead pipeline that survives without an always-on backend.

Role
Solo build · design, eng, content systems
Duration
~9 months active
Stack
Static HTML · CF Pages · Workers · D1
Status
Live · iterating

The problem

UAE legal market has two persistent moats: domain authority on Arabic-keyword SERPs and same-business-day responsiveness on WhatsApp. Most firm sites in the segment treat the website as a brochure and the inbox as the CRM. That leaves money on the table on both sides: leads convert worse, and the website doesn't compound into an SEO asset.

Goal: build a single surface that doubles as a lead-magnet (49 practice-area pages targeting long-tail Arabic + English UAE legal queries) and a working pipeline (form capture → kanban → KYC → proposal → engagement) without committing to an always-on Postgres-and-Node stack the firm can't maintain.

Architecture

Everything that can be static, is. Everything that needs state, runs on Cloudflare's edge primitives. No origin server.

                INTERNET
                   │
        ┌──────────┴──────────┐
        ▼                     ▼
   noura-public            admin/*
   (static HTML)        (CF Access gated)
   Cloudflare Pages          │
        │                    ▼
        │         ┌──────────────────┐
        │         │  Worker handlers │
        │         │  /api/admin/*    │
        │         └────────┬─────────┘
        │                  │
        ▼                  ▼
   ┌─────────────┐  ┌──────────────┐
   │ Pages CDN   │  │   D1 (SQL)   │
   │ static .html│  │ leads,       │
   │ + assets    │  │ pipeline,    │
   └─────────────┘  │ audit,       │
                    │ research idx │
                    └──────┬───────┘
                           ▼
                    ┌──────────────┐
                    │   R2 + KV    │
                    │ KYC docs +   │
                    │ rate limits  │
                    └──────────────┘

Why static-first

Stack

Frontend

  • Vanilla HTML/CSS/JS
  • CSS custom properties (tokens)
  • Cormorant Garamond + Inter + Amiri
  • No build step

Edge

  • Cloudflare Pages
  • Workers (TS)
  • Cloudflare Access (admin auth)
  • Turnstile (form abuse)

State

  • D1 (leads, pipeline, audit)
  • R2 (KYC docs, signed URLs)
  • KV (rate limits, sessions)

Content ops

  • Python content generation
  • JSON-LD schema everywhere
  • RSS feed for insights
  • Sitemap + hreflang

Challenges worth talking about

RTL-native Arabic — not just dir="rtl"

The lazy approach swaps direction and calls it done. The Arabic legal audience reads that as sloppy. The fix: Amiri for body, Cinzel for display, line-height 1.75, logical CSS properties (padding-inline-start) instead of physical, Eastern Arabic numerals via Intl.NumberFormat('ar-AE'), and Hijri date toggle on regulator-watch pages. Icons that imply direction (arrows, chevrons) get mirrored via scaleX(-1); icons that don't (clocks, briefcases) stay.

Multi-language SEO without duplicate-content penalties

Three locales (EN, AR, RU) × 49 practice pages × 100+ insights = roughly 450 URLs that Google needs to recognize as language variants, not near-duplicates. Solution: per-locale canonical, full hreflang tag set on every page (including x-default), and locale-specific structured data with the correct inLanguage field. Verified via Search Console — zero language-confusion flags.

Lead capture that survives client moods

Lawyers don't reliably check three inboxes. Forms post to a Worker that writes to D1 and fans out to email + WhatsApp Business API + Slack simultaneously. If one channel fails, the others still alert. Every form has Turnstile + a honeypot field + KV-backed IP rate limit. Spam volume dropped from ~40/day to under 1/week.

Static SPA admin without React state hell

Admin is hand-written HTML + ES modules, not a framework. Each page (inbox, pipeline, cockpit, research) is independently loadable — a bug in pipeline can't crash inbox. State machine pattern (loading | empty | error | data) is enforced explicitly on every async view. No silent failures.

Before / after

SurfaceBeforeAfter
Mobile contact CTAs 3 overlapping floating buttons + bottom nav (5 contact entry points crowding the bottom of viewport) Bottom nav (Call / Chat / Brief) + 1 chat FAB. WhatsApp lives inside the nav, not as a redundant pill.
Newsletter popup Fired on first scroll, no exit-intent, no frequency cap, fired on the contact page (worst place to interrupt) Triggers on (70% scroll OR 60s OR exit-intent), 30-day dismiss cooldown, suppressed on contact / book / subscribe paths.
Admin URL hygiene .html suffixes leaking the static-export origin Clean URLs via _redirects 200-rewrites; deep links survive reload.
Lead pipeline view Silent empty cards on cockpit — couldn't tell broken from empty Explicit four-state machine (loading skeleton, illustrated empty + CTA, error card with retry, data view).

Numbers I'd defend

~640ms
Public site LCP, 3G throttle
49
Practice-area landing pages
3
Locales · EN · AR · RU
$0
Monthly infra at current traffic
<1/wk
Spam form submissions post-Turnstile
100%
AAA contrast on hero + nav

Metrics are measured, not aspirational. Lighthouse on the live site at 375×812 mobile.

What I'd do differently next time

  1. Declare the data layer on day one. Started with "static for now, add backend later." That deferred a hundred small decisions (form storage, audit logs, KYC retention) that all needed answering at the same time when admin shipped. Should have set up D1 + R2 + KV at the start, even unused.
  2. Split deploys earlier. Public site and admin shipping from the same Pages project meant a CSS conflict in admin once broke fonts on the homepage. Should have moved admin to admin. subdomain in month two, not month nine.
  3. Test the unsexy paths first. Wrote no tests for newsletter form, KYC upload, or pipeline drag-drop until something broke in production. Those three paths are 90% of the actual value. Should have had Playwright e2e on them from week one.
  4. Stop building admin features until a real lawyer uses one. Built kanban + filters + cockpit dashboards in vacuum. Half of them are unused. Should have shipped the inbox alone and waited for the firm to ask for the next thing.