# 2. Architecture technique et stratégie multisite

## Vue d'ensemble

Une seule application Laravel 12 sert **tous les sites** et le **back-office** :

```
                    ┌───────────────────────── Serveur (Nginx + PHP-FPM) ─────────────────────────┐
calculfacile.fr ─┐  │                                                                              │
potager-malin.fr ┼──┼─► routes/site.php  ── ResolveSite ─► TrackPageView ─► CachePublicPage ─► contrôleurs
www.… (alias)  ──┘  │      (sans session ni cookie)        (Host → Site)                      Site\*        │
                    │                                                                              │
admin.domaine.fr ───┼─► routes/admin.php ── web (session, CSRF) ─► auth ─► Gates ─► contrôleurs Admin\* │
                    │                                                                              │
                    │   Base de données unique : sites, site_domains, posts, pages, tools,         │
                    │   categories, ad_configs, ad_slots, page_view_stats, revenue_reports, users  │
                    └──────────────────────────────────────────────────────────────────────────────┘
```

### Pourquoi une base unique et une résolution par domaine ?

- **Ajout d'un site = une ligne en base** (via le back-office ou `php artisan platform:site`), sans déploiement.
- Code, sécurité et optimisations sont mutualisés ; chaque site garde son domaine, son thème, ses couleurs, sa typographie, ses modules, ses pages, ses catégories et ses réglages SEO/AdSense.
- Toutes les requêtes publiques passent par `$site->posts()`, `$site->tools()`… : l'isolation des contenus est garantie par construction (tests à l'appui).

## Résolution du site (`app/Support/SiteResolver.php`, `app/Http/Middleware/ResolveSite.php`)

1. Le domaine (`Host`) est recherché dans une table de correspondance mise en cache (domaines principaux + alias).
2. Alias marqué « redirection » → **301** vers le domaine principal (évite le contenu dupliqué, ex. `www.`).
3. Site « en pause » ou « archivé » → 503 ; site supprimé (corbeille) → 404.
4. Site « en développement » → consultable pour relecture mais **jamais indexable** (`X-Robots-Tag`, meta robots, `robots.txt: Disallow: /`, pas de sitemap).

## Modules, thèmes et outils

| Élément | Où | Comment l'étendre |
|---|---|---|
| Modules (`blog`, `tools`, `search`, `rss`) | `config/platform.php` → `modules` | Activés par site ; une route d'un module désactivé renvoie 404 |
| Thèmes | `resources/views/themes/{thème}` | Ne surcharger que les vues nécessaires : le reste retombe sur `themes/default` (ex. `utility` ne redéfinit que l'accueil) |
| Identité visuelle | `sites.branding` (JSON) | Couleurs injectées en variables CSS (`--brand`, `--accent`…) utilisées par Tailwind (`bg-brand`, `text-brand`) : **un seul CSS compilé** pour tous les sites |
| Outils interactifs | `resources/views/tools/{clé}.blade.php` + `resources/js/tools/{clé}.js` | Déclarer la clé dans `config/platform.tools` ; le JS est chargé à la demande (code-splitting Vite) |
| Emplacements publicitaires | `config/platform.ad_placements` | Ajouter la clé puis `{{ $ads->slot('cle') }}` dans un thème |

## Modèle de données

- `sites` : identité, statut (développement/publié/pause/archivé), statut SEO, statut de monétisation, `branding`, `seo`, `modules` (JSON), suppression douce.
- `site_domains` : alias (redirigés ou servis tels quels, ex. `*.localhost` en local).
- `categories`, `posts`, `pages`, `tools` : contenus par site, en **Markdown** (HTML brut ignoré → pas d'injection de script), slugs uniques par site, champs SEO, FAQ structurée.
- `ad_configs` (1 par site) et `ad_slots` (1 par emplacement) : configuration AdSense.
- `page_view_stats` : compteur anonyme agrégé (site, jour, chemin → vues).
- `revenue_reports` : données AdSense quotidiennes (import CSV officiel ou saisie).
- `users` + `site_user` : rôles et affectation aux sites.

## Rôles et autorisations (`app/Enums/Role.php`, `AppServiceProvider::defineGates`)

| Rôle | Périmètre |
|---|---|
| Super administrateur | Tout : utilisateurs, paramètres plateforme, scripts CMP |
| Administrateur | Tous les sites : création, contenus, publicité, statistiques, revenus |
| Éditeur | Sites assignés : contenus et publication, statistiques |
| Auteur | Sites assignés : ses propres articles, envoyés en relecture (ne peut pas publier) |

Un compte désactivé perd immédiatement tous ses droits (`Gate::before`).

## Performances

- **Pages publiques sans session ni cookie** → cache pleine page (`CachePublicPage`), clé = site + version + chemin. La version du site est incrémentée automatiquement à chaque modification de contenu : pas de cache périmé.
- **JS public minimal** : ~2,6 Ko + 0,4 à 1 Ko par outil, chargé uniquement sur la page de l'outil. Le back-office (Alpine.js) a son propre bundle.
- **Aucune police web** : piles de polices système (zéro requête, pas de décalage de mise en page).
- Images : `loading="lazy"`, dimensions enregistrées à l'envoi (anti-CLS), hauteur minimale réservée aux blocs publicitaires.
- Statistiques écrites **après l'envoi de la réponse** (`terminate`).
- En production : Redis pour le cache, `php artisan optimize`, OPcache, HTTP/2, compression Brotli/Gzip, CDN (voir doc 04).

## Sécurité

- Back-office servi uniquement sur son domaine (`PLATFORM_ADMIN_DOMAIN`), en `noindex`, CSRF, limitation des tentatives de connexion (5/min par e-mail+IP), régénération de session, mots de passe de 12 caractères minimum.
- Autorisations vérifiées dans chaque contrôleur (Gates) et sur les routes ; ressources imbriquées liées à leur site (`scopeBindings`).
- Contenus en Markdown avec `html_input: strip` et liens dangereux supprimés ; échappement Blade partout ailleurs.
- En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, HSTS en HTTPS.
- Validation stricte : domaines, couleurs (hexadécimal), identifiants AdSense, lignes ads.txt, fichiers (pas de SVG).
- Le seul HTML libre (script de CMP tierce) est réservé aux super administrateurs.

## Ajouter un nouveau projet

1. Back-office → **Sites → Nouveau site** (ou `php artisan platform:site slug domaine.fr "Nom" --theme=default`).
2. Les pages obligatoires (à propos, contact, confidentialité, mentions légales) sont générées **en brouillon** : à personnaliser.
3. Choisir thème, couleurs, typographie et modules ; ajouter l'alias `www.` (redirigé).
4. Produire le contenu en statut « développement » (non indexé), puis passer en « publié ».
5. Pointer le DNS du domaine vers le serveur et ajouter le domaine au certificat TLS.

## Modules « livescore » et « games »

### Résultats sportifs (`app/Services/LiveScore`)

```
ScoreProvider (interface)
 ├─ FootballDataProvider  API football-data.org v4 (jeton FOOTBALL_DATA_TOKEN)
 └─ DemoProvider          matchs fictifs déterministes, refusés en production
LiveScore                 choix du fournisseur par site, cache (30 s en direct, 5 min par jour,
                          30 min pour les classements), repli sur la dernière réponse valide
```

- Réglages par site (`sites.settings.livescore`) : fournisseur, compétitions, fuseau horaire, indexation des pages de match.
- Pages : `/` ou `/resultats/{date}` (tableau du jour), `/match/{id}`, `/competition/{code}` (classement + calendrier), `/resultats/live.json` (flux de mise à jour).
- Le navigateur interroge le flux JSON toutes les 30 s (uniquement si l'onglet est visible) et met à jour les scores **sans recharger la page ni les annonces**.
- Le cache pleine page accepte une durée par réponse (`X-Cache-TTL`) : 30 s pour les pages en direct.
- Ajouter un fournisseur : implémenter `ScoreProvider` (normalisation vers le format décrit dans l'interface) et l'ajouter dans `config/platform.php` et `LiveScore::provider()`.

### Jeux (`resources/views/games`, `resources/js/games`)

- Les jeux réutilisent le modèle `Tool` avec `kind = game` (même gestion éditoriale : explication, FAQ, SEO) et l'URL `/jeux/{slug}`.
- Ajouter un jeu : `resources/views/games/{clé}.blade.php` (balisage avec `data-game="{clé}"`) + `resources/js/games/{clé}.js` (export par défaut `function (root)`), puis déclarer la clé dans `config/platform.games`.
- Les records sont stockés dans le navigateur du visiteur (`localStorage`), rien n'est envoyé au serveur.

## Multilingue

- **URL** : la langue du site (`sites.locale`) est servie sans préfixe ; chaque langue supplémentaire activée (Réglages du site → Langues) est servie sous `/{code}/` avec des segments traduits (`config/platform.php` → `segments`), par exemple `/outils/tva` → `/en/tools/tva` → `/es/herramientas/tva`. Les routes sont enregistrées une fois par langue ; `ResolveSite` applique la langue et renvoie 404 pour une langue non activée.
- **Publication progressive** : une langue n'est exposée que lorsqu'au moins un contenu y est traduit (`Localization::activeLocales`). Exceptions : les modules `icons` et `livescore`, dont l'interface est entièrement traduite dans `lang/`.
- **Contenus** : le trait `Translatable` (articles, pages, outils, catégories) lit les champs traduits dans la table `translations` (polymorphe, un enregistrement par langue, statut `machine` ou `reviewed`, empreinte de la source pour détecter les traductions obsolètes).
- **Traduction automatique** : `TRANSLATION_PROVIDER=deepl` (+ `DEEPL_API_KEY`) ou `libretranslate` (+ `LIBRETRANSLATE_URL`). Le Markdown est découpé en segments pour préserver titres, listes, tableaux et code. Commande `php artisan platform:translate {slug}` ou bouton dans le back-office (onglet Traductions) ; option « traduire automatiquement à l'enregistrement ». Les traitements passent par la file d'attente (`php artisan queue:work`). Une traduction relue par un humain n'est jamais écrasée.
- **SEO et AdSense** : balises `hreflang` + `x-default`, `og:locale`, sitemap multilingue. Par défaut, une traduction automatique non relue est `noindex`, exclue du sitemap et sans annonce, avec un bandeau informant le lecteur ; l'option « indexer les traductions automatiques » lève cette protection site par site.
- **API externes** : correction orthographique via LanguageTool (`LANGUAGETOOL_URL`, à auto-héberger en production : l'API publique est limitée), taux de change BCE via Frankfurter (cache 6 h, repli sur la dernière valeur). `HTTP_CA_BUNDLE` sert uniquement aux installations PHP sans certificats racine (WAMP).
