# InsideKech — Flat-File PHP Travel Guide Platform

A complete bilingual (FR/EN) travel guide for Marrakech tourists.
No database, no framework — flat-file PHP with JSON content files.
Clean & modern UI inspired by Airbnb/Booking.com: functional, card-based, conversion-focused.

---

## Tech Stack

| Layer | Choice | Reason |
|---|---|---|
| Backend | PHP 8.1+, flat-file | No DB, zero ops overhead |
| CSS | Tailwind CSS (compiled) | Utility-first, fast iteration |
| Maps | Leaflet.js + OpenStreetMap | Free, no API key |
| Icons | Heroicons (SVG inline) | Clean, consistent |
| Fonts | Geist (body) + Fraunces (display headings) via Google Fonts | Modern + editorial contrast |
| i18n | URL-prefix based (`/fr/`, `/en/`) | SEO-friendly, clear |
| Flights | Client-side form → Skyscanner redirect | Zero maintenance |
| Images | 800×450 WebP, AI-generated via Fal AI | Consistent, fast |
| SEO | JSON-LD, hreflang, canonical, OG, XML sitemap | Full coverage |

---

## Design System

### Philosophy
Clean, trustworthy, conversion-focused. Every page element earns its place.
Primary goal per page type: **listing pages → click into detail**, **detail pages → book/reserve**, **articles → read + trust**.

### Color Palette (CSS variables in `assets/css/input.css`)

```css
:root {
  /* Brand */
  --ik-primary:       #E8603A;   /* Terracotta orange — CTAs, active states */
  --ik-primary-hover: #D4522D;
  --ik-primary-light: #FDF0EC;   /* Tinted backgrounds behind primary elements */

  /* Neutrals */
  --ik-bg:            #FFFFFF;
  --ik-bg-soft:       #F7F7F7;   /* Page background, card backgrounds */
  --ik-bg-muted:      #F0EFED;   /* Filter bars, input backgrounds */
  --ik-border:        #E5E4E0;
  --ik-border-strong: #C9C8C3;

  /* Text */
  --ik-text-primary:  #1A1917;
  --ik-text-secondary:#6B6860;
  --ik-text-muted:    #9C9A94;

  /* Semantic */
  --ik-success:       #16A34A;
  --ik-warning:       #D97706;
  --ik-star:          #F59E0B;   /* Star ratings */

  /* Shadows */
  --ik-shadow-sm:     0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05);
  --ik-shadow-md:     0 4px 12px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06);
  --ik-shadow-lg:     0 12px 32px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.06);
  --ik-shadow-hover:  0 8px 24px rgba(232,96,58,0.18);
}
```

### Typography

```css
/* Load in <head> via Google Fonts */
/* Fraunces: display headings only — page titles, hero text */
/* Geist: everything else */
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,300;0,600;0,700;1,300&family=Geist:wght@300;400;500;600&display=swap');

/* Scale */
--ik-font-display: 'Fraunces', Georgia, serif;
--ik-font-body:    'Geist', -apple-system, sans-serif;

/* Sizes */
h1.page-title   { font: 700 2.5rem/1.15 var(--ik-font-display); letter-spacing: -0.02em; }
h1.hero-title   { font: 600 3.25rem/1.1 var(--ik-font-display); letter-spacing: -0.03em; }
h2              { font: 600 1.5rem/1.3  var(--ik-font-body); }
h3              { font: 600 1.125rem/1.4 var(--ik-font-body); }
body            { font: 400 1rem/1.65   var(--ik-font-body); }
.text-sm        { font-size: 0.875rem; line-height: 1.5; }
.text-xs        { font-size: 0.75rem;  line-height: 1.4; }
```

### Spacing System
Base unit: 4px. All spacing uses multiples: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96px.
Sections on content pages: `padding-block: 64px` desktop, `padding-block: 40px` mobile.

### Border Radius
```
Cards:       16px  (rounded-2xl)
Buttons:     10px  (rounded-xl)
Inputs:      10px
Badges/tags: 999px (pill)
Images:      12px
Map:         16px
```

### Component Specs

#### Place Card (hotels, restaurants, activities)
```
┌──────────────────────────────┐
│ [Image 400×240, rounded-xl]  │  ← object-fit: cover, lazy loaded
│  ❤ Wishlist btn (top-right)  │  ← heart icon, appears on hover
├──────────────────────────────┤
│ ★★★★☆  Guéliz  · From €120   │  ← stars + neighborhood badge + price
│ Riad Yasmine                 │  ← h3, font-body 600
│ Piscine · Medina · Romantic  │  ← tag pills, text-muted, text-xs
│ Petit-déjeuner inclus        │  ← single highlight, text-sm
│                    [Voir →]  │  ← ghost link, color primary
└──────────────────────────────┘
Shadow: var(--ik-shadow-sm)
Hover: shadow changes to var(--ik-shadow-md), image scales to 1.03 (transition 300ms)
Entire card is a link (<a> wrapping the card)
```

#### Article Card
```
┌──────────────────────────────┐
│ [Image 400×220, rounded-xl]  │
│  [Category badge top-left]   │  ← e.g. "Conseils pratiques"
├──────────────────────────────┤
│ 5 min de lecture             │  ← text-xs text-muted
│ 7 Meilleures Cartes SIM Maroc│  ← h3
│ Restez connecté sans payer...│  ← excerpt, 2 lines max, text-sm
│ Lire l'article →             │  ← text-primary link
└──────────────────────────────┘
```

#### CTA Button Variants
```css
/* Primary — book / reserve / search */
.btn-primary {
  background: var(--ik-primary);
  color: #fff;
  padding: 12px 24px;
  border-radius: 10px;
  font: 600 0.9375rem var(--ik-font-body);
  transition: background 200ms, box-shadow 200ms;
}
.btn-primary:hover {
  background: var(--ik-primary-hover);
  box-shadow: var(--ik-shadow-hover);
}

/* Secondary — directions / website */
.btn-secondary {
  background: transparent;
  border: 1.5px solid var(--ik-border-strong);
  color: var(--ik-text-primary);
  padding: 11px 24px;
  border-radius: 10px;
}
.btn-secondary:hover { border-color: var(--ik-primary); color: var(--ik-primary); }

/* Ghost — inline links */
.btn-ghost { color: var(--ik-primary); font-weight: 500; }
.btn-ghost:hover { text-decoration: underline; }
```

#### Filter Bar (listing pages)
Horizontal scrollable pill filter row, sticky below header on mobile:
```
[Tous] [Médina] [Guéliz] [Palmeraie] | [Budget €] [€€] [€€€] | [Piscine] [Spa] [Vue]
```
Active pill: `background: var(--ik-primary); color: #fff;`
Inactive pill: `background: var(--ik-bg-muted); color: var(--ik-text-secondary);`
Filter interaction is client-side JS (no page reload), URL hash updated for shareability.

#### Star Rating Display
```html
<div class="rating" aria-label="4.2 sur 5">
  <!-- Filled stars via SVG mask/clip, fractional star support -->
  <span class="stars">★★★★☆</span>
  <span class="rating-score">4.2</span>
  <span class="rating-count">(128 avis)</span>
</div>
```

#### Opening Hours Widget (restaurants)
Shows today's hours with open/closed indicator:
```
🟢 Ouvert · Ferme à 23h30     ← if open now
🔴 Fermé · Ouvre demain à 19h ← if closed
```
Computed client-side from `opening_hours` JSON field + user's local time.

#### Map Component (Leaflet.js)
- Height: 320px desktop, 240px mobile
- Border-radius: 16px, overflow hidden
- Custom marker: SVG pin in `--ik-primary` color (not default blue Leaflet marker)
- Popup: place name + "Voir sur Google Maps" link
- Tiles: `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`
- No API key needed
- Loaded lazily — Leaflet CSS/JS only included on pages with a map

#### Language Switcher
Top-right in header. Clean pill toggle:
```
[FR | EN]   ← inactive lang is muted, active is primary color
```
On click: sets `ik_lang` cookie, redirects to alternate URL.

### Page Layout Grid
```css
/* Main content wrapper */
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }

/* Listing page grid */
.grid-places  { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
@media (max-width: 1024px) { .grid-places { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px)  { .grid-places { grid-template-columns: 1fr; } }

/* Detail page: content + sidebar */
.layout-detail {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 48px;
  align-items: start;
}
@media (max-width: 1024px) { .layout-detail { grid-template-columns: 1fr; } }
```

### Header
```
[InsideKech logo]   Hôtels  Restaurants  Activités  Vols  Conseils   [FR|EN]  [Menu☰]
```
- Height: 64px
- Background: `#fff` with `border-bottom: 1px solid var(--ik-border)` on scroll (JS adds class)
- Logo: wordmark "InsideKech" — "Inside" in Geist 600, "Kech" in Fraunces italic 600, color primary
- Nav links: Geist 500 0.9rem, color text-secondary, hover color text-primary
- Active nav item: color primary, font-weight 600
- Mobile: hamburger → full-screen slide-in menu

### Footer
3-column layout:
```
InsideKech                  Explorer               Informations
Guide de Marrakech          Hôtels                 À propos
© 2026                      Restaurants            Contact
[Instagram][Facebook]       Activités              Mentions légales
                            Vols                   Politique de confidentialité
                            Conseils
```
Background: `var(--ik-text-primary)` (dark), text white/muted.

---

## Project Structure

```
/
├── index.php                      # Front controller
├── .htaccess                      # URL rewriting + 301 redirects + security headers
├── robots.txt
├── sitemap.php                    # Dynamic XML sitemap (all langs, all types)
├── package.json
├── tailwind.config.js
│
├── assets/
│   ├── css/
│   │   ├── input.css              # Tailwind + CSS variables + custom components
│   │   └── output.css             # Compiled
│   ├── js/
│   │   └── main.js                # Lang switcher, map, flight form, filters, menu, hours
│   └── images/
│       ├── hotels/                # {slug}.webp 800×450
│       ├── restaurants/           # {slug}.webp 800×450
│       ├── activities/            # {slug}.webp 800×450
│       ├── articles/              # {slug}.webp 800×450
│       └── og/                    # Open Graph 1200×630 per page
│
├── data/
│   ├── config.json                # Site settings, nav, featured items, affiliate IDs
│   ├── redirects.json             # Old URL → new URL mapping (301s)
│   ├── translations/
│   │   ├── fr.json                # All UI strings in French
│   │   └── en.json                # All UI strings in English
│   ├── hotels/                    # {slug}.json per hotel
│   ├── restaurants/               # {slug}.json per restaurant
│   ├── activities/                # {slug}.json per activity
│   └── articles/                  # {slug}.json per article/tip
│
├── lib/
│   ├── helpers.php                # h(), t(), langUrl(), placeCard(), articleCard(), stars()
│   ├── DataLoader.php             # JSON loading, discovery, filtering, sorting, related
│   ├── Router.php                 # Bilingual URL matching, redirect handling
│   ├── SeoMeta.php                # Title, meta, OG, hreflang, JSON-LD per type
│   └── I18n.php                   # Lang detection, translation loader, t() backend
│
└── templates/
    ├── partials/
    │   ├── head.php               # <head> — all SEO, fonts, CSS
    │   ├── header.php             # Nav + lang switcher + mobile menu
    │   └── footer.php             # 3-col dark footer
    ├── home.php                   # Hero + featured sections + latest articles
    ├── hotel.php                  # Hotel detail: gallery, info, map, FAQ, related
    ├── restaurant.php             # Restaurant detail: info, hours widget, map, related
    ├── activity.php               # Activity detail: info, booking, map, related
    ├── article.php                # Article: rich content, sections, tips, FAQ
    ├── hotels-list.php            # Hotels listing + filter bar
    ├── restaurants-list.php       # Restaurants listing + filter bar
    ├── activities-list.php        # Activities listing + filter bar
    ├── articles-list.php          # Articles listing
    ├── vols.php                   # Flight search form + Skyscanner redirect
    ├── about.php
    ├── contact.php
    └── 404.php                    # Custom 404 with search + popular links
```

---

## URL Structure

### New URL Structure (canonical)

```
# Homepage redirects
/                         → 301 → /fr/
/fr/                      ← French homepage (canonical)
/en/                      ← English homepage (canonical)

# Listings
/fr/hotels/               /en/hotels/
/fr/restaurants/          /en/restaurants/
/fr/activites/            /en/activities/
/fr/vols/                 /en/flights/
/fr/conseils/             /en/tips/

# Detail pages
/fr/hotels/{slug}/        /en/hotels/{slug}/
/fr/restaurants/{slug}/   /en/restaurants/{slug}/
/fr/activites/{slug}/     /en/activities/{slug}/
/fr/conseils/{slug}/      /en/tips/{slug}/

# Static
/fr/a-propos/             /en/about/
/fr/contact/              /en/contact/
/fr/mentions-legales/     /en/legal/
/fr/confidentialite/      /en/privacy/
```

### 301 Redirect Map (Old WordPress URLs → New URLs)

All old URLs are permanently redirected. SEO equity is fully preserved.
Redirects are handled in `.htaccess` (fastest, zero PHP overhead).

#### Old category pages → new listing pages
```apache
Redirect 301 /hotels-et-hebergements/          /fr/hotels/
Redirect 301 /restaurants-et-gastronomie/      /fr/restaurants/
Redirect 301 /activites-et-experiences/        /fr/activites/
Redirect 301 /sejours-circuits/                /fr/conseils/
Redirect 301 /destinations/                    /fr/conseils/
Redirect 301 /vols/                            /fr/vols/
Redirect 301 /transport/                       /fr/conseils/
```

#### Old article URLs → new detail pages
These old slugs map to the correct new content type + lang prefix.
Stored in `data/redirects.json` and processed by `Router.php` as a fallback
for any old URL not caught by `.htaccess` static rules.

**Hotels (old → new):**
```
/riad-marrakech/                        → /fr/hotels/riad-marrakech/
/hotel-all-inclusive-marrakech/         → /fr/hotels/hotel-all-inclusive-marrakech/
/club-hotel-a-marrakech/                → /fr/hotels/club-hotel-a-marrakech/
/kasbah-hotel-marrakech/                → /fr/hotels/kasbah-hotel-marrakech/
/la-palmeraie-hotel-marrakech/          → /fr/hotels/la-palmeraie-hotel-marrakech/
/location-villa-marrakech/              → /fr/hotels/location-villa-marrakech/
/hotel-les-idrissides-marrakech/        → /fr/hotels/hotel-les-idrissides-marrakech/
/hotel-les-dunes-dor-agadir/            → /fr/hotels/hotel-les-dunes-dor-agadir/
/marrakech-hotel-aqua-fun/              → /fr/hotels/marrakech-hotel-aqua-fun/
```

**Restaurants (old → new):**
```
/koya-marrakech/                        → /fr/restaurants/koya-marrakech/
/rooftop-marrakech/                     → /fr/restaurants/rooftop-marrakech/
/restaurant-festif-marrakech/           → /fr/restaurants/restaurant-festif-marrakech/
/restaurant-ambiance-marrakech/         → /fr/restaurants/restaurant-ambiance-marrakech/
/marrakech-cappuccino/                  → /fr/restaurants/marrakech-cappuccino/
```

**Activities (old → new):**
```
/vol-en-montgolfiere-marrakech/         → /fr/activites/vol-en-montgolfiere-marrakech/
/excursions-marrakech/                  → /fr/activites/excursions-marrakech/
/excursion-agadir/                      → /fr/activites/excursion-agadir/
/piscine-sur-marrakech/                 → /fr/activites/piscine-sur-marrakech/
/piscine-desert-marrakech/              → /fr/activites/piscine-desert-marrakech/
/buggy-marrakech/                       → /fr/activites/buggy-marrakech/
/hammam-targa/                          → /fr/activites/hammam-targa/
```

**Articles / Tips (old → new):**
```
/carte-sim-maroc/                       → /fr/conseils/carte-sim-maroc/
/ramadan-maroc/                         → /fr/conseils/ramadan-maroc/
/peut-on-se-baigner-a-marrakech-en-avril/ → /fr/conseils/peut-on-se-baigner-a-marrakech-en-avril/
/tarifa-tanger/                         → /fr/conseils/tarifa-tanger/
/bateau-marseille-tanger/               → /fr/conseils/bateau-marseille-tanger/
/road-trip-maroc/                       → /fr/conseils/road-trip-maroc/
/circuit-maroc-voyage/                  → /fr/conseils/circuit-maroc-voyage/
/voyage-a-marrakech-tout-compris/       → /fr/conseils/voyage-a-marrakech-tout-compris/
/voyage-marrakech-tout-compris/         → /fr/conseils/voyage-marrakech-tout-compris/
/agadir-voyage-tout-compris/            → /fr/conseils/agadir-voyage-tout-compris/
/maroc-nord/                            → /fr/conseils/maroc-nord/
/vallee-de-lourika/                     → /fr/conseils/vallee-de-lourika/
/vol-nantes-agadir/                     → /fr/vols/
```

**WordPress pagination / category pages:**
```apache
# Catch old /page/2/ pattern
RedirectMatch 301 ^/hotels-et-hebergements/page/(.*)$ /fr/hotels/
RedirectMatch 301 ^/([^/]+)/page/(.*)$                /fr/conseils/$1/
```

**`data/redirects.json`** (used by Router.php for dynamic redirect handling):
```json
{
  "/riad-marrakech/":                       "/fr/hotels/riad-marrakech/",
  "/koya-marrakech/":                       "/fr/restaurants/koya-marrakech/",
  "/vol-en-montgolfiere-marrakech/":        "/fr/activites/vol-en-montgolfiere-marrakech/",
  "/carte-sim-maroc/":                      "/fr/conseils/carte-sim-maroc/",
  "...":                                    "... (all 30 old URLs listed)"
}
```

Router.php redirect logic (runs before any other routing):
```php
// In Router.php::resolve()
$redirects = DataLoader::getRedirects(); // loads data/redirects.json, cached in static var
$currentPath = '/' . trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') . '/';
if (isset($redirects[$currentPath])) {
    header('Location: ' . $redirects[$currentPath], true, 301);
    exit;
}
```

---

## .htaccess

```apache
Options -Indexes
RewriteEngine On

# --- 301 REDIRECTS: Old WordPress URLs → New structure ---

# Old category pages
Redirect 301 /hotels-et-hebergements/         /fr/hotels/
Redirect 301 /restaurants-et-gastronomie/     /fr/restaurants/
Redirect 301 /activites-et-experiences/       /fr/activites/
Redirect 301 /sejours-circuits/               /fr/conseils/
Redirect 301 /destinations/                   /fr/conseils/
Redirect 301 /vols/                           /fr/vols/
Redirect 301 /transport/                      /fr/conseils/

# Old article URLs (hotels)
Redirect 301 /riad-marrakech/                 /fr/hotels/riad-marrakech/
Redirect 301 /hotel-all-inclusive-marrakech/  /fr/hotels/hotel-all-inclusive-marrakech/
Redirect 301 /club-hotel-a-marrakech/         /fr/hotels/club-hotel-a-marrakech/
Redirect 301 /kasbah-hotel-marrakech/         /fr/hotels/kasbah-hotel-marrakech/
Redirect 301 /la-palmeraie-hotel-marrakech/   /fr/hotels/la-palmeraie-hotel-marrakech/
Redirect 301 /location-villa-marrakech/       /fr/hotels/location-villa-marrakech/
Redirect 301 /hotel-les-idrissides-marrakech/ /fr/hotels/hotel-les-idrissides-marrakech/
Redirect 301 /hotel-les-dunes-dor-agadir/     /fr/hotels/hotel-les-dunes-dor-agadir/
Redirect 301 /marrakech-hotel-aqua-fun/       /fr/hotels/marrakech-hotel-aqua-fun/

# Old article URLs (restaurants)
Redirect 301 /koya-marrakech/                 /fr/restaurants/koya-marrakech/
Redirect 301 /rooftop-marrakech/              /fr/restaurants/rooftop-marrakech/
Redirect 301 /restaurant-festif-marrakech/    /fr/restaurants/restaurant-festif-marrakech/
Redirect 301 /restaurant-ambiance-marrakech/  /fr/restaurants/restaurant-ambiance-marrakech/
Redirect 301 /marrakech-cappuccino/           /fr/restaurants/marrakech-cappuccino/

# Old article URLs (activities)
Redirect 301 /vol-en-montgolfiere-marrakech/  /fr/activites/vol-en-montgolfiere-marrakech/
Redirect 301 /excursions-marrakech/           /fr/activites/excursions-marrakech/
Redirect 301 /excursion-agadir/               /fr/activites/excursion-agadir/
Redirect 301 /piscine-sur-marrakech/          /fr/activites/piscine-sur-marrakech/
Redirect 301 /piscine-desert-marrakech/       /fr/activites/piscine-desert-marrakech/
Redirect 301 /buggy-marrakech/                /fr/activites/buggy-marrakech/
Redirect 301 /hammam-targa/                   /fr/activites/hammam-targa/

# Old article URLs (articles/tips)
Redirect 301 /carte-sim-maroc/                /fr/conseils/carte-sim-maroc/
Redirect 301 /ramadan-maroc/                  /fr/conseils/ramadan-maroc/
Redirect 301 /peut-on-se-baigner-a-marrakech-en-avril/ /fr/conseils/peut-on-se-baigner-a-marrakech-en-avril/
Redirect 301 /tarifa-tanger/                  /fr/conseils/tarifa-tanger/
Redirect 301 /bateau-marseille-tanger/        /fr/conseils/bateau-marseille-tanger/
Redirect 301 /road-trip-maroc/                /fr/conseils/road-trip-maroc/
Redirect 301 /circuit-maroc-voyage/           /fr/conseils/circuit-maroc-voyage/
Redirect 301 /voyage-a-marrakech-tout-compris/ /fr/conseils/voyage-a-marrakech-tout-compris/
Redirect 301 /voyage-marrakech-tout-compris/  /fr/conseils/voyage-marrakech-tout-compris/
Redirect 301 /agadir-voyage-tout-compris/     /fr/conseils/agadir-voyage-tout-compris/
Redirect 301 /maroc-nord/                     /fr/conseils/maroc-nord/
Redirect 301 /vallee-de-lourika/              /fr/conseils/vallee-de-lourika/
Redirect 301 /vol-nantes-agadir/              /fr/vols/

# Old pagination
RedirectMatch 301 ^/hotels-et-hebergements/page/(.+)/$   /fr/hotels/
RedirectMatch 301 ^/activites-et-experiences/page/(.+)/$ /fr/activites/
RedirectMatch 301 ^/([^/]+)/page/(.+)/$                  /fr/conseils/$1/

# Bare domain → default lang
RewriteRule ^$ /fr/ [R=301,L]

# --- ROUTING: all /fr/* and /en/* → index.php ---
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(fr|en)/(.*)$ index.php [L,QSA]

# --- SECURITY ---
# Block sensitive directories
RewriteRule ^(data|lib|templates|generate-images\.php)(/|$) - [F,L]

# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' unpkg.com fonts.googleapis.com; style-src 'self' 'unsafe-inline' unpkg.com fonts.googleapis.com fonts.gstatic.com; font-src fonts.gstatic.com; img-src 'self' data: *.openstreetmap.org tile.openstreetmap.org *.tile.openstreetmap.org; connect-src 'self'; frame-ancestors 'none';"

# Cache static assets
<FilesMatch "\.(webp|jpg|png|svg|woff2)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "output\.css$">
  Header set Cache-Control "public, max-age=604800"
</FilesMatch>
```

---

## Routing (Router.php)

```php
// Language detection order:
// 1. First URL segment (/fr/ or /en/)
// 2. Cookie ik_lang
// 3. Accept-Language header
// 4. Config default ('fr')

// Route table (matched after stripping lang prefix)
$routes = [
    ''                              => 'home',
    'hotels'                        => 'hotels-list',
    'hotels/{slug}'                 => 'hotel',
    'restaurants'                   => 'restaurants-list',
    'restaurants/{slug}'            => 'restaurant',
    'activites|activities'          => 'activities-list',
    'activites/{slug}|activities/{slug}' => 'activity',
    'vols|flights'                  => 'vols',
    'conseils|tips'                 => 'articles-list',
    'conseils/{slug}|tips/{slug}'   => 'article',
    'a-propos|about'                => 'about',
    'contact'                       => 'contact',
    'mentions-legales|legal'        => 'legal',
    'confidentialite|privacy'       => 'privacy',
];

// Slug sanitization (before any file operation)
$slug = preg_replace('/[^a-z0-9-]/', '', strtolower($rawSlug));
```

---

## i18n (I18n.php)

Language-aware URL builder used in every template:
```php
// Generate URL in current or specified language
langUrl('hotels')                         // → /fr/hotels/ or /en/hotels/
langUrl('hotels', 'riad-yasmine')         // → /fr/hotels/riad-yasmine/
langUrl('hotels', 'riad-yasmine', 'en')   // → /en/hotels/riad-yasmine/ (for hreflang)

// Translation lookup
t('nav.hotels')     // → "Hôtels" (fr) or "Hotels" (en)
t('btn.book_now')   // → "Réserver" (fr) or "Book now" (en)
```

`data/translations/fr.json` (partial):
```json
{
  "nav.hotels":              "Hôtels",
  "nav.restaurants":         "Restaurants",
  "nav.activities":          "Activités",
  "nav.flights":             "Vols",
  "nav.tips":                "Conseils",
  "btn.book_now":            "Réserver",
  "btn.see_on_map":          "Voir sur la carte",
  "btn.get_directions":      "Itinéraire",
  "btn.visit_website":       "Visiter le site",
  "btn.search_flights":      "Rechercher des vols",
  "btn.read_article":        "Lire l'article",
  "btn.see_all":             "Voir tout",
  "label.from_price":        "À partir de",
  "label.per_night":         "/ nuit",
  "label.read_time":         "min de lecture",
  "label.open_now":          "Ouvert maintenant",
  "label.closed":            "Fermé",
  "label.closes_at":         "Ferme à",
  "label.opens_at":          "Ouvre à",
  "label.best_season":       "Meilleure saison",
  "label.duration":          "Durée",
  "label.difficulty":        "Difficulté",
  "label.included":          "Inclus",
  "label.not_included":      "Non inclus",
  "label.neighborhood":      "Quartier",
  "label.address":           "Adresse",
  "label.phone":             "Téléphone",
  "label.stars":             "Étoiles",
  "flights.from_city":       "Ville de départ",
  "flights.departure":       "Aller",
  "flights.return":          "Retour",
  "flights.passengers":      "Passagers",
  "flights.class":           "Classe",
  "flights.disclaimer":      "Résultats fournis par Skyscanner. Cliquer ouvrira leur site.",
  "flights.one_way":         "Aller simple",
  "flights.round_trip":      "Aller-retour",
  "home.hero.title":         "Votre guide de voyage à Marrakech",
  "home.hero.subtitle":      "Hôtels, restaurants, activités et vols — tout pour un séjour parfait",
  "home.hero.cta":           "Explorer Marrakech",
  "home.featured.hotels":    "Hébergements recommandés",
  "home.featured.restaurants": "Tables incontournables",
  "home.featured.activities":  "Expériences à vivre",
  "home.latest_articles":    "Conseils et informations",
  "404.title":               "Page introuvable",
  "404.message":             "Cette page n'existe pas ou a été déplacée.",
  "404.cta":                 "Retour à l'accueil"
}
```

---

## Data Schemas

### Hotel JSON (`data/hotels/{slug}.json`)

```json
{
  "slug": "riad-yasmine",
  "type": "hotel",
  "subtype": "riad",
  "published": true,
  "published_date": "2026-06-01",
  "modified_date": "2026-06-01",

  "fr": {
    "name": "Riad Yasmine",
    "seo_title": "Riad Yasmine Marrakech — Avis & Réservation 2026 | InsideKech",
    "meta_description": "Découvrez le Riad Yasmine, havre de paix en pleine médina. Piscine iconique, terrasse panoramique, petit-déjeuner marocain. Réservez au meilleur prix.",
    "description": "<p>Le Riad Yasmine est l'un des riads les plus photographiés de Marrakech, connu pour sa piscine à carreaux verts...</p>",
    "highlights": ["Piscine à carreaux verts iconique", "Terrasse panoramique", "Petit-déjeuner marocain inclus"],
    "practical_tip": "Réservez 2 mois à l'avance en haute saison (mars–avril, octobre–novembre).",
    "neighborhood_desc": "Niché dans la médina, à 5 min à pied de Jemaa el-Fna."
  },
  "en": {
    "name": "Riad Yasmine",
    "seo_title": "Riad Yasmine Marrakech — Reviews & Booking 2026 | InsideKech",
    "meta_description": "Discover Riad Yasmine, a peaceful haven in Marrakech medina. Iconic tiled pool, panoramic terrace, Moroccan breakfast. Book at the best price.",
    "description": "<p>Riad Yasmine is one of the most photographed riads in Marrakech, famous for its green-tiled pool...</p>",
    "highlights": ["Iconic green-tiled pool", "Panoramic rooftop terrace", "Moroccan breakfast included"],
    "practical_tip": "Book 2 months ahead in high season (March–April, October–November).",
    "neighborhood_desc": "Nestled in the medina, 5 min walk from Jemaa el-Fna square."
  },

  "image": "/assets/images/hotels/riad-yasmine.webp",
  "image_alt_fr": "Piscine du Riad Yasmine avec carreaux verts, Marrakech",
  "image_alt_en": "Riad Yasmine pool with green tiles, Marrakech",
  "gallery": [
    "/assets/images/hotels/riad-yasmine-pool.webp",
    "/assets/images/hotels/riad-yasmine-room.webp",
    "/assets/images/hotels/riad-yasmine-terrace.webp"
  ],

  "location": {
    "address": "18 Derb Sidi Ali Tair, Médina, Marrakech 40000",
    "neighborhood": "medina",
    "lat": 31.6258,
    "lng": -7.9892,
    "google_maps_url": "https://maps.google.com/?q=31.6258,-7.9892"
  },

  "contact": {
    "phone": "+212 524 123 456",
    "email": "contact@riad-yasmine.com",
    "website": "https://riad-yasmine.com"
  },

  "details": {
    "stars": 4,
    "price_range": "€€€",
    "price_from_eur": 120,
    "amenities": {
      "has_pool": true,
      "has_wifi": true,
      "has_parking": false,
      "has_restaurant": true,
      "has_spa": false,
      "has_air_conditioning": true,
      "breakfast_included": true
    },
    "best_for": ["couples", "honeymoon", "solo"],
    "seasons_best": ["oct", "nov", "mar", "apr"]
  },

  "booking": {
    "booking_com_url": "https://www.booking.com/hotel/ma/riad-yasmine.html?aid=XXXXXXX",
    "affiliate": true
  },

  "tags": ["riad", "piscine", "medina", "romantique"],
  "related": ["kasbah-hotel-marrakech", "hotel-all-inclusive-marrakech"],
  "schema_type": "LodgingBusiness",
  "rating": { "count": 0, "sum": 0, "average": 0 }
}
```

### Restaurant JSON (`data/restaurants/{slug}.json`)

```json
{
  "slug": "koya-marrakech",
  "type": "restaurant",
  "published": true,
  "published_date": "2026-06-01",
  "modified_date": "2026-06-01",

  "fr": {
    "name": "Koya Marrakech",
    "seo_title": "Koya Marrakech — Restaurant Japonais Fusion, Avis & Réservation | InsideKech",
    "meta_description": "Koya Marrakech : restaurant fusion alliant cuisine japonaise et saveurs marocaines. Cadre luxueux, cocktails créatifs. Réservation indispensable.",
    "description": "<p>Au cœur du Guéliz, Koya propose une expérience culinaire unique...</p>",
    "highlights": ["Cuisine fusion Japon–Maroc", "Cocktails signature", "Ambiance intime"],
    "must_try": "Le tataki de bœuf aux épices marocaines est incontournable.",
    "reservation_tip": "Réservez impérativement le jeudi, vendredi et samedi — complet dès 20h."
  },
  "en": {
    "name": "Koya Marrakech",
    "seo_title": "Koya Marrakech — Japanese Fusion Restaurant, Reviews & Booking | InsideKech",
    "meta_description": "Koya Marrakech: fusion restaurant blending Japanese cuisine with Moroccan flavours. Luxurious setting, creative cocktails. Reservation essential.",
    "description": "<p>In the heart of Guéliz, Koya offers a unique culinary experience...</p>",
    "highlights": ["Japan–Morocco fusion cuisine", "Signature cocktails", "Intimate atmosphere"],
    "must_try": "The beef tataki with Moroccan spices is unmissable.",
    "reservation_tip": "Book ahead for Thursday, Friday and Saturday — fully booked by 8pm."
  },

  "image": "/assets/images/restaurants/koya-marrakech.webp",
  "image_alt_fr": "Salle du restaurant Koya Marrakech, décor luxueux",
  "image_alt_en": "Interior of Koya restaurant Marrakech, luxury decor",

  "location": {
    "address": "Rue de la Liberté, Guéliz, Marrakech 40000",
    "neighborhood": "gueliz",
    "lat": 31.6340,
    "lng": -8.0089,
    "google_maps_url": "https://maps.google.com/?q=31.6340,-8.0089"
  },

  "contact": {
    "phone": "+212 524 456 789",
    "website": "https://koya-marrakech.com",
    "instagram": "https://instagram.com/koya_marrakech",
    "reservation_url": "https://koya-marrakech.com/reservation"
  },

  "details": {
    "cuisine": ["fusion", "japonaise", "marocaine"],
    "price_range": "€€€",
    "avg_price_eur": 45,
    "opening_hours": {
      "mon": null,
      "tue": "19:00-23:30",
      "wed": "19:00-23:30",
      "thu": "19:00-00:00",
      "fri": "19:00-00:00",
      "sat": "12:00-15:00,19:00-00:00",
      "sun": "12:00-15:00,19:00-23:30"
    },
    "accepts_reservations": true,
    "dress_code": "smart casual",
    "best_for": ["couples", "business", "special_occasion"],
    "has_vegetarian": true,
    "has_alcohol": true,
    "has_terrace": false
  },

  "tags": ["fusion", "gastronomique", "gueliz", "romantique"],
  "related": ["restaurant-ambiance-marrakech", "rooftop-marrakech"],
  "schema_type": "Restaurant",
  "rating": { "count": 0, "sum": 0, "average": 0 }
}
```

### Activity JSON (`data/activities/{slug}.json`)

```json
{
  "slug": "vol-en-montgolfiere-marrakech",
  "type": "activity",
  "subtype": "excursion",
  "published": true,
  "published_date": "2026-06-01",
  "modified_date": "2026-06-01",

  "fr": {
    "name": "Vol en Montgolfière à Marrakech",
    "seo_title": "Vol en Montgolfière Marrakech 2026 — Prix & Réservation | InsideKech",
    "meta_description": "Survolez Marrakech en montgolfière au lever du soleil. Palmeraie, Atlas, médina depuis les airs. Champagne inclus. À partir de 160 €.",
    "description": "<p>Le vol en montgolfière est sans doute l'expérience la plus magique de Marrakech...</p>",
    "highlights": ["Lever du soleil sur l'Atlas", "Survol de la Palmeraie", "Champagne à l'atterrissage"],
    "what_to_bring": "Vêtements chauds (frais à l'aube), chaussures fermées, appareil photo.",
    "booking_tip": "Réservez 48h à l'avance. Annulation gratuite si météo défavorable."
  },
  "en": {
    "name": "Hot Air Balloon Ride in Marrakech",
    "seo_title": "Hot Air Balloon Marrakech 2026 — Prices & Booking | InsideKech",
    "meta_description": "Soar over Marrakech by balloon at sunrise. Palmeraie, Atlas, medina from above. Champagne included. From €160.",
    "description": "<p>A hot air balloon ride is perhaps the most magical experience in Marrakech...</p>",
    "highlights": ["Sunrise over the Atlas Mountains", "Flight over the Palmeraie", "Champagne on landing"],
    "what_to_bring": "Warm layers (cool at dawn), closed-toe shoes, camera.",
    "booking_tip": "Book 48h ahead. Free cancellation in bad weather."
  },

  "image": "/assets/images/activities/vol-en-montgolfiere-marrakech.webp",
  "image_alt_fr": "Montgolfière au-dessus de Marrakech au lever du soleil, Atlas en arrière-plan",
  "image_alt_en": "Hot air balloon over Marrakech at sunrise, Atlas Mountains in background",

  "location": {
    "meeting_point_fr": "Votre hôtel (navette incluse) ou La Palmeraie",
    "meeting_point_en": "Your hotel (shuttle included) or La Palmeraie",
    "neighborhood": "palmeraie",
    "lat": 31.6600,
    "lng": -7.9500,
    "google_maps_url": "https://maps.google.com/?q=31.6600,-7.9500"
  },

  "details": {
    "duration_hours": 3.5,
    "difficulty": "easy",
    "min_age": 6,
    "max_participants": 8,
    "price_from_eur": 160,
    "price_range": "€€€",
    "includes_fr": ["Navette hôtel A/R", "Vol 1 heure", "Champagne", "Certificat de vol"],
    "includes_en": ["Hotel shuttle", "1-hour flight", "Champagne", "Flight certificate"],
    "excludes_fr": ["Assurance annulation"],
    "excludes_en": ["Cancellation insurance"],
    "departure_time": "05:30",
    "languages_available": ["fr", "en", "es"],
    "best_for": ["couples", "family", "solo"],
    "seasons_available": ["jan","feb","mar","apr","may","oct","nov","dec"],
    "seasons_note_fr": "Suspendu juillet–août (vents chauds)",
    "seasons_note_en": "Suspended July–August (heat winds)",
    "booking_required": true
  },

  "booking": {
    "getyourguide_url": "https://www.getyourguide.com/marrakech-l175/?partner_id=XXXXX",
    "viator_url": "https://www.viator.com/tours/Marrakech/XXXXXXX",
    "affiliate": true
  },

  "tags": ["montgolfiere", "excursion", "romantique", "palmeraie", "famille"],
  "related": ["excursions-marrakech", "buggy-marrakech"],
  "schema_type": "TouristAttraction",
  "rating": { "count": 0, "sum": 0, "average": 0 }
}
```

### Article JSON (`data/articles/{slug}.json`)

```json
{
  "slug": "carte-sim-maroc",
  "type": "article",
  "subtype": "tips",
  "published": true,
  "published_date": "2026-06-01",
  "modified_date": "2026-06-01",
  "author": "InsideKech",
  "read_time_minutes": 5,
  "image": "/assets/images/articles/carte-sim-maroc.webp",

  "fr": {
    "title": "7 Meilleures Cartes SIM Maroc pour Voyageurs 2026",
    "seo_title": "Carte SIM Maroc 2026 : Laquelle Choisir ? Guide Complet | InsideKech",
    "meta_description": "Quelle carte SIM choisir pour le Maroc en 2026 ? Maroc Telecom, Orange, Inwi — comparatif prix, couverture et data. Conseils d'achat à l'aéroport.",
    "image_alt": "Cartes SIM marocaines pour voyageurs — Maroc Telecom, Orange, Inwi",
    "intro": "<p>Rester connecté au Maroc sans exploser sa facture roaming, c'est possible avec la bonne carte SIM locale...</p>",
    "sections": [
      {
        "title": "Maroc Telecom — Meilleure couverture nationale",
        "content": "<p>Maroc Telecom couvre 98% du territoire marocain, y compris les zones rurales et désertiques...</p>",
        "pros_fr": ["Couverture 4G excellente partout", "Réseau le plus fiable"],
        "cons_fr": ["Prix légèrement supérieur aux concurrents"],
        "price_info": "À partir de 30 MAD pour 5 Go (≈ 3 €)",
        "affiliate_url": ""
      }
    ],
    "key_tips": [
      "Achetez votre SIM à l'aéroport dès l'arrivée — boutiques ouvertes 24h/24",
      "Apportez votre passeport (obligatoire pour l'enregistrement)",
      "Vérifiez que votre téléphone est déverrouillé avant de partir"
    ],
    "faq": [
      {
        "question": "Peut-on acheter une SIM sans passeport au Maroc ?",
        "answer": "Non. La loi marocaine exige la présentation d'un passeport valide pour tout enregistrement de carte SIM."
      }
    ],
    "conclusion": "<p>Notre recommandation : Maroc Telecom pour la couverture, Inwi pour le meilleur rapport qualité-prix en ville.</p>"
  },
  "en": {
    "title": "7 Best SIM Cards for Morocco Travelers 2026",
    "seo_title": "Morocco SIM Card 2026: Which One? Complete Guide | InsideKech",
    "meta_description": "Which SIM card for Morocco in 2026? Maroc Telecom, Orange, Inwi — price comparison, coverage and data. Airport buying tips included.",
    "image_alt": "Moroccan SIM cards for travelers — Maroc Telecom, Orange, Inwi",
    "intro": "<p>Staying connected in Morocco without a huge roaming bill is easy with the right local SIM card...</p>",
    "sections": [
      {
        "title": "Maroc Telecom — Best National Coverage",
        "content": "<p>Maroc Telecom covers 98% of Morocco including rural and desert areas...</p>",
        "pros_en": ["Excellent 4G coverage everywhere", "Most reliable network"],
        "cons_en": ["Slightly more expensive than competitors"],
        "price_info": "From 30 MAD for 5GB (≈ €3)",
        "affiliate_url": ""
      }
    ],
    "key_tips": [
      "Buy your SIM at the airport on arrival — shops open 24/7",
      "Bring your passport (required by law for registration)",
      "Make sure your phone is unlocked before traveling"
    ],
    "faq": [
      {
        "question": "Can you buy a SIM card without a passport in Morocco?",
        "answer": "No. Moroccan law requires a valid passport for any SIM card registration."
      }
    ],
    "conclusion": "<p>Our recommendation: Maroc Telecom for coverage, Inwi for best value in cities.</p>"
  },

  "tags": ["pratique", "telecom", "connectivite", "aeroport"],
  "related": ["ramadan-maroc", "road-trip-maroc"]
}
```

---

## Template Layouts

### home.php

```
┌─────────────────────────────────────────────────┐
│  HERO (full-width, 560px height)                │
│  Background: Marrakech photo with dark overlay  │
│  [Fraunces] "Votre guide de voyage              │
│              à Marrakech"                       │
│  [Geist] subtitle + [Explorer →] btn-primary   │
│  Search bar: [Hotels ▼][Restaurants ▼][→]      │
├─────────────────────────────────────────────────┤
│  FEATURED HOTELS                                │
│  "Hébergements recommandés"  [Voir tout →]      │
│  [Card][Card][Card]  (3-col grid)               │
├─────────────────────────────────────────────────┤
│  FEATURED RESTAURANTS                           │
│  "Tables incontournables"    [Voir tout →]      │
│  [Card][Card][Card]                             │
├─────────────────────────────────────────────────┤
│  FEATURED ACTIVITIES                            │
│  "Expériences à vivre"       [Voir tout →]      │
│  [Card][Card][Card]                             │
├─────────────────────────────────────────────────┤
│  FLIGHT SEARCH TEASER                           │
│  Terracotta bg section                          │
│  "Trouver votre vol" + mini form → /fr/vols/    │
├─────────────────────────────────────────────────┤
│  LATEST ARTICLES                                │
│  "Conseils et informations"  [Voir tout →]      │
│  [ArticleCard][ArticleCard][ArticleCard]        │
├─────────────────────────────────────────────────┤
│  FOOTER                                         │
└─────────────────────────────────────────────────┘
```

### hotel.php / restaurant.php / activity.php (Place Detail)

```
┌─────────────────────────────────────────────────┐
│  Breadcrumb: Accueil > Hôtels > Riad Yasmine   │
├─────────────────────────────────────────────────┤
│  Hero image (full-width, 480px, rounded-2xl)    │
│  [Gallery thumbnails row below if gallery ≥ 3] │
├────────────────────────┬────────────────────────┤
│  MAIN CONTENT (left)   │  STICKY SIDEBAR (right)│
│                        │  ┌──────────────────┐  │
│  h1: Place name        │  │ À partir de 120€ │  │
│  ★★★★☆ 4.2 (128 avis) │  │ / nuit           │  │
│                        │  │ [Réserver]       │  │  ← btn-primary, full-width
│  Description (HTML)    │  │ [Voir sur Maps]  │  │  ← btn-secondary
│                        │  ├──────────────────┤  │
│  ✓ Highlights list     │  │ Adresse:  ...    │  │
│                        │  │ Tél:      ...    │  │
│  Practical tip         │  │ Horaires: ...    │  │
│  (info box, bg-soft)   │  │ Site web: ...    │  │
│                        │  └──────────────────┘  │
├────────────────────────┴────────────────────────┤
│  MAP (Leaflet, 320px, rounded-2xl)              │
│  Custom terracotta pin marker                   │
│  [Voir sur Google Maps →]                       │
├─────────────────────────────────────────────────┤
│  Includes / Excludes (activities only)          │
├─────────────────────────────────────────────────┤
│  FAQ Accordion                                  │
│  (smooth height animation, JS-powered)          │
├─────────────────────────────────────────────────┤
│  RELATED PLACES                                 │
│  [Card][Card][Card]                             │
└─────────────────────────────────────────────────┘
```

Sidebar becomes bottom bar on mobile (position: sticky bottom: 0, full-width).

### hotels-list.php / restaurants-list.php / activities-list.php

```
┌─────────────────────────────────────────────────┐
│  Page title (Fraunces) + count "24 hôtels"      │
├─────────────────────────────────────────────────┤
│  FILTER BAR (sticky, bg-white, border-bottom)   │
│  [Tous] [Médina] [Guéliz] [Palmeraie]          │  ← neighborhood pills
│  [€] [€€] [€€€]                                │  ← price pills
│  [Piscine] [Spa] [Parking] [Breakfast]         │  ← amenity pills (hotels)
│  Active count: "14 résultats"                   │
├─────────────────────────────────────────────────┤
│  GRID (3-col desktop, 2-col tablet, 1-col mob)  │
│  [PlaceCard][PlaceCard][PlaceCard]              │
│  [PlaceCard][PlaceCard][PlaceCard]              │
│  ...                                            │
├─────────────────────────────────────────────────┤
│  No results state (if filters = 0 matches):     │
│  "Aucun résultat pour ces filtres"              │
│  [Réinitialiser les filtres]                    │
└─────────────────────────────────────────────────┘
```

Filters are applied **client-side** (JS, no reload). All items rendered in HTML, hidden/shown via `data-*` attributes. URL hash updated for shareability: `#medina+piscine`.

### vols.php (Flight Search)

```
┌─────────────────────────────────────────────────┐
│  HERO: "Trouvez votre vol vers Marrakech"       │
│  Subtitle: "RAK — Aéroport Menara"              │
├─────────────────────────────────────────────────┤
│  FLIGHT SEARCH FORM (white card, shadow-lg)     │
│  ┌─────────────────────────────────────────┐    │
│  │ [○ Aller-retour  ○ Aller simple]        │    │
│  ├──────────────┬──────────────────────────┤    │
│  │ Départ       │ Destination              │    │
│  │ [CDG Paris ▼]│ [RAK Marrakech — fixed]  │    │
│  ├──────────────┼──────────────────────────┤    │
│  │ Aller        │ Retour                   │    │
│  │ [📅 date]   │ [📅 date]               │    │
│  ├──────────────┴──────────────────────────┤    │
│  │ Passagers [1 adulte ▼]  Classe [Éco ▼] │    │
│  ├─────────────────────────────────────────┤    │
│  │ [🔍 Rechercher des vols] btn-primary   │    │
│  └─────────────────────────────────────────┘    │
│                                                 │
│  ℹ️ "Les résultats sont fournis par Skyscanner. │
│     Cliquer ouvrira leur site pour finaliser."  │
├─────────────────────────────────────────────────┤
│  POPULAR DEPARTURE CITIES                       │
│  [Paris] [Lyon] [Marseille] [Nantes] [Genève]  │
│  [Bruxelles] [Amsterdam] [Londres] [Madrid]    │
│  (pills, clicking pre-fills the form)           │
├─────────────────────────────────────────────────┤
│  INFO SECTION                                   │
│  "Infos pratiques sur l'aéroport de Marrakech" │
│  Transfert, visa, saison... (article content)  │
└─────────────────────────────────────────────────┘
```

**Skyscanner URL builder (main.js):**
```javascript
function buildSkyscannerUrl({ from, outDate, inDate, adults, cabin, locale }) {
  // from: IATA code (e.g. "CDG"), always to RAK
  const base = `https://www.skyscanner.fr/transport/vols/${from.toLowerCase()}/rak/`;
  const d1 = outDate.replace(/-/g, '');
  const d2 = inDate ? inDate.replace(/-/g, '') : null;
  const path = d2 ? `${d1}/${d2}/` : `${d1}/`;
  const params = new URLSearchParams({
    adults,
    cabinclass: cabin,   // economy | business | first
    currency: 'EUR',
    locale: locale || 'fr-FR',
    market: 'FR'
  });
  return `${base}${path}?${params}`;
}

flightForm.addEventListener('submit', (e) => {
  e.preventDefault();
  const url = buildSkyscannerUrl({ ... });
  window.open(url, '_blank', 'noopener');
});
```

### article.php

```
┌─────────────────────────────────────────────────┐
│  Breadcrumb + "5 min de lecture"                │
├─────────────────────────────────────────────────┤
│  H1 title (Fraunces)                            │
│  Hero image (full-width, 480px)                 │
├────────────────────────┬────────────────────────┤
│  ARTICLE CONTENT       │  TABLE OF CONTENTS      │
│  (left, 65%)           │  (right, 35%, sticky)  │
│                        │  Auto-generated from   │
│  Intro paragraph       │  h2/h3 headings        │
│                        │                        │
│  Section 1             │  Quick info box:       │
│    h2                  │  • Durée lecture        │
│    content             │  • Publié le            │
│    pros/cons table     │  • Mis à jour           │
│                        │                        │
│  Section 2...          │  [Share: 𝕏 FB WA]     │
│                        │                        │
│  Key Tips box          │                        │
│  (bg-primary-light,    │                        │
│   terracotta border)   │                        │
│                        │                        │
│  FAQ Accordion         │                        │
│                        │                        │
│  Conclusion            │                        │
├────────────────────────┴────────────────────────┤
│  RELATED ARTICLES [Card][Card][Card]            │
└─────────────────────────────────────────────────┘
```

### 404.php
Custom page with popular links:
- "Cette page n'existe pas" + illustration
- Links to: Hôtels, Restaurants, Activités, Vols, Conseils
- Brief message noting old links may have moved (references redirect)

---

## SeoMeta.php — JSON-LD Schemas

| Template | Schema(s) |
|---|---|
| `home.php` | `WebSite` + `TouristDestination` (Marrakech) |
| `hotel.php` | `LodgingBusiness` (name, address, geo, priceRange, amenityFeature, aggregateRating) |
| `restaurant.php` | `Restaurant` (servesCuisine, openingHoursSpecification, priceRange, aggregateRating) |
| `activity.php` | `TouristAttraction` (geo, offers with price, aggregateRating) |
| `article.php` | `Article` (author, datePublished, dateModified, image) |
| All listing pages | `ItemList` + `BreadcrumbList` |
| `vols.php` | `WebPage` + `BreadcrumbList` |

**hreflang on every page:**
```html
<link rel="alternate" hreflang="fr"      href="https://www.insidekech.com/fr/hotels/riad-yasmine/"/>
<link rel="alternate" hreflang="en"      href="https://www.insidekech.com/en/hotels/riad-yasmine/"/>
<link rel="alternate" hreflang="x-default" href="https://www.insidekech.com/fr/hotels/riad-yasmine/"/>
```

---

## sitemap.php

Auto-discovers all JSON files. Outputs two `<url>` entries per item (FR + EN) with `xhtml:link` alternates.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">

  <!-- Static pages (2 each) -->
  <url>
    <loc>https://www.insidekech.com/fr/</loc>
    <xhtml:link rel="alternate" hreflang="fr" href="https://www.insidekech.com/fr/"/>
    <xhtml:link rel="alternate" hreflang="en" href="https://www.insidekech.com/en/"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://www.insidekech.com/fr/"/>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>

  <!-- Dynamic (all hotels × 2, restaurants × 2, activities × 2, articles × 2) -->
  <url>
    <loc>https://www.insidekech.com/fr/hotels/riad-yasmine/</loc>
    <lastmod>2026-06-01</lastmod>
    <xhtml:link rel="alternate" hreflang="fr" href="https://www.insidekech.com/fr/hotels/riad-yasmine/"/>
    <xhtml:link rel="alternate" hreflang="en" href="https://www.insidekech.com/en/hotels/riad-yasmine/"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://www.insidekech.com/fr/hotels/riad-yasmine/"/>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
  ...
</urlset>
```

---

## Security

| Threat | Mitigation |
|---|---|
| XSS | All output via `h()` = `htmlspecialchars(ENT_QUOTES, 'UTF-8')`. Authored HTML from site-controlled JSON only rendered raw. |
| CSRF | Token on contact form. Flight form is GET/redirect — no CSRF needed. |
| Path traversal | Slugs: `preg_replace('/[^a-z0-9-]/', '', strtolower($slug))` before any file op. |
| Open redirect | `validateRedirect()` verifies same-origin before any PHP redirect. |
| Session | `strict_mode`, `httponly`, `SameSite=Lax`, `secure` on HTTPS. |
| Directory listing | `Options -Indexes` + block `data/`, `lib/`, `templates/` in `.htaccess`. |
| HTTP headers | CSP, HSTS 1yr, X-Content-Type-Options, X-Frame-Options, Referrer-Policy. |
| API keys | `FAL_API_KEY` as env var only. `generate-images.php` blocked via `.htaccess`. |

---

## config.json

```json
{
  "site_name":    "InsideKech",
  "site_url":     "https://www.insidekech.com",
  "default_lang": "fr",
  "supported_langs": ["fr", "en"],

  "destination": {
    "city_fr": "Marrakech",
    "city_en": "Marrakech",
    "iata":    "RAK",
    "lat":     31.6295,
    "lng":    -7.9811
  },

  "nav": {
    "fr": [
      { "label": "Hôtels",       "url": "/fr/hotels/"      },
      { "label": "Restaurants",  "url": "/fr/restaurants/" },
      { "label": "Activités",    "url": "/fr/activites/"   },
      { "label": "Vols",         "url": "/fr/vols/"        },
      { "label": "Conseils",     "url": "/fr/conseils/"    }
    ],
    "en": [
      { "label": "Hotels",       "url": "/en/hotels/"      },
      { "label": "Restaurants",  "url": "/en/restaurants/" },
      { "label": "Activities",   "url": "/en/activities/"  },
      { "label": "Flights",      "url": "/en/flights/"     },
      { "label": "Tips",         "url": "/en/tips/"        }
    ]
  },

  "featured": {
    "hotels":      ["riad-yasmine", "marrakech-hotel-aqua-fun", "kasbah-hotel-marrakech"],
    "restaurants": ["koya-marrakech", "rooftop-marrakech", "restaurant-festif-marrakech"],
    "activities":  ["vol-en-montgolfiere-marrakech", "hammam-targa", "buggy-marrakech"]
  },

  "affiliate": {
    "booking_aid":       "YOUR_BOOKING_AID",
    "getyourguide_pid":  "YOUR_GYG_PID",
    "viator_pid":        "YOUR_VIATOR_PID",
    "skyscanner_locale": "fr-FR"
  },

  "social": {
    "instagram": "https://instagram.com/insidekech",
    "facebook":  "https://facebook.com/insidekech",
    "pinterest": "https://pinterest.com/insidekech"
  }
}
```

---

## main.js — Module Overview

```javascript
// 1. Language switcher
//    - setLangCookie(lang): sets ik_lang cookie, 1yr
//    - Reads data-alt-url from <html> tag for same-page redirect

// 2. Header scroll behavior
//    - Adds .scrolled class to <header> on scroll > 10px
//    - .scrolled adds box-shadow and shrinks height slightly (CSS transition)

// 3. Mobile menu
//    - Hamburger toggles full-screen overlay nav
//    - Locks body scroll when open

// 4. Leaflet map (lazy init)
//    - Only runs if #place-map element exists
//    - Custom SVG marker in --ik-primary color
//    - Popup with place name + Google Maps link

// 5. Opening hours widget
//    - Reads data-hours JSON from element
//    - Computes open/closed based on current local time
//    - Updates DOM with colored indicator

// 6. Client-side listing filter
//    - Reads data-neighborhood, data-price, data-amenities from each card
//    - Pill click toggles filter state
//    - Shows/hides cards, updates count
//    - Updates URL hash: #medina+piscine

// 7. Flight search form
//    - Validates required fields
//    - Builds Skyscanner URL
//    - window.open(url, '_blank', 'noopener')

// 8. FAQ accordion
//    - Smooth max-height animation via CSS transition
//    - Keyboard accessible (Enter/Space on trigger)

// 9. Image lazy loading
//    - Native loading="lazy" on all images
//    - IntersectionObserver fade-in on enter

// 10. Social share
//     - Facebook, WhatsApp, X (Twitter), Pinterest, copy link
//     - Reads og:url and og:title from <head>
```

---

## Image Generation Script

Same as TodayFoodsMenu — CLI-only PHP script, blocked from web access.

```bash
FAL_API_KEY=your_key php generate-images.php --type=hotels
FAL_API_KEY=your_key php generate-images.php --type=restaurants
FAL_API_KEY=your_key php generate-images.php --type=activities
FAL_API_KEY=your_key php generate-images.php --type=articles
FAL_API_KEY=your_key php generate-images.php --type=all
```

Prompts by type:
- **hotels**: `"{name}, luxury hotel interior or courtyard in Marrakech Morocco, warm golden light, travel photography, editorial, 16:9"`
- **restaurants**: `"{name}, upscale restaurant in Marrakech Morocco, elegant setting, warm candlelight, food photography, 16:9"`
- **activities**: `"{name} in Marrakech Morocco, vibrant colors, travel photography, golden hour, 16:9"`
- **articles**: `"Travel in Morocco, {title topic}, editorial travel photography, warm light, 16:9"`

---

## Tailwind Config

```javascript
// tailwind.config.js
module.exports = {
  content: ['./templates/**/*.php', './assets/js/*.js'],
  theme: {
    extend: {
      colors: {
        'ik-primary':       '#E8603A',
        'ik-primary-hover': '#D4522D',
        'ik-primary-light': '#FDF0EC',
        'ik-bg-soft':       '#F7F7F7',
        'ik-bg-muted':      '#F0EFED',
        'ik-border':        '#E5E4E0',
        'ik-text':          '#1A1917',
        'ik-text-secondary':'#6B6860',
        'ik-star':          '#F59E0B',
      },
      fontFamily: {
        display: ['Fraunces', 'Georgia', 'serif'],
        body:    ['Geist', '-apple-system', 'sans-serif'],
      },
      borderRadius: {
        'xl':  '12px',
        '2xl': '16px',
        '3xl': '24px',
      },
      boxShadow: {
        'card':      '0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05)',
        'card-hover':'0 4px 12px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06)',
        'cta-hover': '0 8px 24px rgba(232,96,58,0.18)',
      }
    }
  },
  plugins: []
}
```

---

## Dependencies

| Dependency | Version | How |
|---|---|---|
| PHP | 8.1+ | Server |
| Node.js | 18+ | Tailwind build only |
| Tailwind CSS | 3.x | npm |
| Leaflet.js | 1.9.4 | unpkg CDN, no npm |
| Heroicons | inline SVG | copy-paste, no npm |
| Geist font | latest | Google Fonts CDN |
| Fraunces font | latest | Google Fonts CDN |

No composer. No database. No framework.

---

## Content to Migrate from Current InsideKech

All 30 existing articles must be converted to the new JSON format and placed in the correct type folder:

| Old slug | New type | New folder |
|---|---|---|
| riad-marrakech | hotel | data/hotels/ |
| hotel-all-inclusive-marrakech | hotel | data/hotels/ |
| club-hotel-a-marrakech | hotel | data/hotels/ |
| kasbah-hotel-marrakech | hotel | data/hotels/ |
| la-palmeraie-hotel-marrakech | hotel | data/hotels/ |
| location-villa-marrakech | hotel | data/hotels/ |
| hotel-les-idrissides-marrakech | hotel | data/hotels/ |
| hotel-les-dunes-dor-agadir | hotel | data/hotels/ |
| marrakech-hotel-aqua-fun | hotel | data/hotels/ |
| koya-marrakech | restaurant | data/restaurants/ |
| rooftop-marrakech | restaurant | data/restaurants/ |
| restaurant-festif-marrakech | restaurant | data/restaurants/ |
| restaurant-ambiance-marrakech | restaurant | data/restaurants/ |
| marrakech-cappuccino | restaurant | data/restaurants/ |
| vol-en-montgolfiere-marrakech | activity | data/activities/ |
| excursions-marrakech | activity | data/activities/ |
| excursion-agadir | activity | data/activities/ |
| piscine-sur-marrakech | activity | data/activities/ |
| piscine-desert-marrakech | activity | data/activities/ |
| buggy-marrakech | activity | data/activities/ |
| hammam-targa | activity | data/activities/ |
| carte-sim-maroc | article | data/articles/ |
| ramadan-maroc | article | data/articles/ |
| peut-on-se-baigner-a-marrakech-en-avril | article | data/articles/ |
| tarifa-tanger | article | data/articles/ |
| bateau-marseille-tanger | article | data/articles/ |
| road-trip-maroc | article | data/articles/ |
| circuit-maroc-voyage | article | data/articles/ |
| voyage-a-marrakech-tout-compris | article | data/articles/ |
| voyage-marrakech-tout-compris | article | data/articles/ |
| agadir-voyage-tout-compris | article | data/articles/ |
| maroc-nord | article | data/articles/ |
| vallee-de-lourika | article | data/articles/ |
| vol-nantes-agadir | article | data/articles/ |
