Case study · 2026
Agadir Morocco Tours — Multilingual Tour Booking Platform
Four-language tour site (incl. Arabic RTL) with an owner dashboard, availability calendar and WhatsApp booking requests.
- Next.js 16
- TypeScript
- PostgreSQL
- Prisma 7
- Zod
- Tailwind CSS v4
- sharp
- Cloudinary
- Vitest

Context
A small operator selling excursions, day trips and airport transfers. Customers are international; the owner confirms bookings personally on WhatsApp.
Constraints
- English, French, Arabic (right-to-left) and Spanish
- One non-technical owner manages all content
- No online payment — a deliberate business choice the UI must state honestly
- Minimal dependencies for long-term maintainability
System design
Visitor (/en /fr /ar /es) Owner (/admin)
│ │
┌───────▼─────────────────────┐ ┌──────────────▼──────────────┐
│ Public site (SSG/ISR) │ │ Dashboard (Server Actions) │
│ activities · pages · FAQ │ │ activities · translations │
│ reviews · availability │ │ photos · slots · reviews │
└───────┬─────────────────────┘ └──────────────┬──────────────┘
│ booking panel │ requireAdmin()
│ date → time → party size │ DB sessions, scrypt
▼ ▼
WhatsApp deep link ┌────────────────────────────────┐
(pre-filled, visitor's │ PostgreSQL (Prisma 7) │
language + reply language) │ Activity ─< ActivityTranslation│
│ │ Activity ─< AvailabilitySlot │
▼ │ Review, Page, FaqItem (+ i18n) │
Owner confirms by reply └────────────────────────────────┘
Uploads → sharp (decode, WebP,
strip EXIF, blur) → Cloudinary / S3
Key decisions
- Translation rows, not JSON blobs. Facts (price, duration) live on the parent; text lives in
*Translationrows keyed by locale. Adding a language needs no migration. - Incomplete translations are hidden, not faked. An activity missing Arabic simply isn't listed on the Arabic site — no English text under
<html lang="ar">. - Localized slugs, Arabic kept in Arabic script; a slug from another language redirects to the right one.
- Structural RTL with CSS logical properties and a dedicated Arabic type pairing.
- WhatsApp instead of payments — matches how the owner already works; the UI says "request", never "booked".
Security
- Opaque 256-bit session tokens, only SHA-256 stored; scrypt password hashing
- Rate-limited sign-in with constant-time checks against account enumeration
- Uploads decoded and re-encoded (EXIF/GPS stripped), never trusted by MIME type
- No stored HTML rendered — rich text parsed to a typed tree; CSP and security headers
Results
- Live at agadirmoroccotours.com in four languages
- Owner runs content, availability and reviews without developer help
- Vitest + Testing Library coverage on the booking flow
More projects
2025–26Case studyDubai Haus — Real-Estate Platform, SEO & Growth
Bilingual property platform built from scratch and grown through SEO — 2,200+ synced projects and 6.8× more organic clicks in 12 months.
- Next.js
- PostgreSQL
- Prisma
- Redis
- +8
Read case study
2026Case studySaadiyat Marina — Bilingual Property Publication
German-first Next.js publication on Abu Dhabi's Saadiyat Island — 54 URLs with a complete hreflang cluster, built in 7 working days.
- Next.js 15
- TypeScript
- JSON-LD
- hreflang
- +3
Read case study
