# Spécifications techniques — état actuel de l'implémentation

Documente les décisions d'architecture prises pendant la construction, au fil de l'eau.
Complète les contrats de données (`client-json-explication.md`,
`client-dossier-arborescence.md`) sans les dupliquer — ce document décrit le code Laravel
lui-même : routes, contrôleurs, vues, assets.

**Statut** : reflète ce qui est réellement construit à ce jour (sandbox uniquement). À tenir à
jour à chaque évolution significative plutôt que de laisser dériver.

## Stack

Laravel 13 / PHP 8.4 / SQLite / Pest / Tailwind v4 / Vite — pour l'application elle-même
(admin `/gestion` à venir). **Les templates clients n'utilisent pas ce pipeline** : ils
embarquent leurs propres assets pré-compilés (Bootstrap, AOS, GLightbox, Swiper) hérités de
l'ancien système, servis tels quels depuis `public/`.

## Zéro base de données pour le contenu client

Chaque client est un fichier JSON dans `storage/app/tenants/{slug}.json` (publié) et
`{slug}.brouillon.json` (édition), lus directement via `Illuminate\Support\Facades\File` —
volontairement **pas** via `Storage::disk('local')`, car ce disque pointe par défaut sur
`storage/app/private` en Laravel 11+, pas `storage/app`.

## Routage sandbox

```
GET /sandbox/{slug}                      → SandboxController::show            (accueil)
GET /sandbox/{slug}/mentions-legales     → SandboxController::mentionsLegales
GET /sandbox/{slug}/{prestationSlug}     → SandboxController::prestation
```

`{slug}` et `{prestationSlug}` sont contraints à `[a-z0-9\-]+` (protection contre la
traversée de répertoire, puisque le slug sert directement à construire un chemin fichier).

**Choix de structure d'URL pour les prestations** : à plat (`/sandbox/{slug}/{prestationSlug}`),
pas `/sandbox/{slug}/prestations/{slug}`. Ce n'est pas arbitraire — l'ancien système route déjà
les prestations à la racine du domaine réel (`jerome-loze.fr/ferronnier-d-art-occitanie-aude`,
voir le `.htaccess` de `ancien-code/sandbox/jloze/`), pour préserver la continuité SEO
(`cdc-fonctionnel.md`, section 0). Garder la même forme en sandbox qu'en production évite une
divergence entre les deux mécanismes de lecture. La route `mentions-legales` doit être
déclarée **avant** la route catch-all `{prestationSlug}` pour que Laravel la fasse gagner (les
routes sont testées dans leur ordre de déclaration).

Le contrôleur ne code jamais le nom du template en dur : le nom de vue est construit depuis
`$client['template']` (ex. `tenant-templates.template-2-0.accueil`).

`X-Robots-Tag: noindex` est forcé sur toutes les réponses sandbox, sans exception, quelle que
soit la valeur du champ `publie` du client — conforme à `cdc-fonctionnel.md`.

## Vues — layout, partials, View Composer

```
resources/views/tenant-templates/template-2-0/
├── layout.blade.php       ← shell partagé (head, menu, footer, scripts)
├── accueil.blade.php      ← @extends(layout), une section par bloc de page
├── mentions-legales.blade.php
├── prestation.blade.php
└── partials/
    ├── menu.blade.php, footer.blade.php, cta-button.blade.php, ...
    ├── email-display.blade.php, email-link.blade.php   (anti-scraping, voir plus bas)
    └── prestation/
        ├── info-bar.blade.php, indications.blade.php, cab.blade.php, declencheur.blade.php
```

Chaque page (`accueil`, `mentions-legales`, `prestation`) hérite de `layout.blade.php` via
`@extends`/`@section`. Point d'attention Blade à connaître : **le layout parent s'exécute
après le contenu de la page enfant** (le mécanisme `@extends` reporte le rendu du parent à la
toute fin). Une variable calculée dans le layout n'est donc pas visible dans le
`@section('content')` de l'enfant.

Solution retenue : un **View Composer** (`App\Providers\AppServiceProvider::boot()`), enregistré
sur le pattern `tenant-templates.template-2-0.*`, qui injecte avant le rendu de *chaque* vue
(layout, pages, partials) :

- `$tpl` — URL de base des assets partagés du template
- `$accueilUrl` — URL de la page d'accueil du client (route nommée `sandbox.accueil`)
- `$ctaLien` / `$ctaLabel` / `$ctaIcone` — CTA résolu (priorité à `lien_reservation` sur `cta`)
- `$ctaIsMailto` / `$ctaEmailB64` — pour l'anti-scraping du CTA (voir plus bas)

Le menu et le footer sont partagés entre plusieurs pages : leurs ancres internes
(`#hero`, `#contact`...) sont donc préfixées par `$accueilUrl` plutôt que d'être des ancres
locales, sans quoi elles ne fonctionneraient pas depuis `mentions-legales` ou une page
`prestation`.

`<main>` et `<body>` acceptent une classe additionnelle par page
(`@section('main-class', ...)`, `@section('body-class', ...)`) — nécessaire car une partie du
CSS hérité de l'ancien système est scopée par ces classes (ex. `.prestation .info-item`).

## Assets — deux zones distinctes

- `public/tenant-templates/{template}/assets/` — assets **partagés par template**, pas par
  client (Bootstrap, AOS, GLightbox, Swiper, `main.css`, `main.js`, pictos décoratifs). En
  miroir de `resources/views/tenant-templates/{template}/`.
- `public/tenants/{slug}/assets/` — assets **propres à chaque client** (photos), voir
  `client-dossier-arborescence.md`. Zone non encore utilisée en pratique (aucun client n'a de
  photo à ce jour).

Bibliothèques volontairement pas copiées : Isotope, imagesLoaded, php-email-form. Le
`main.js` hérité ne les invoque que si les éléments DOM correspondants existent
(portfolio, formulaire de contact) — absents à ce stade, aucune régression à les omettre.

## Anti-scraping email

L'e-mail du client ne doit jamais apparaître en clair dans le HTML source. Deux partials
génériques (`email-display.blade.php` pour un simple affichage, `email-link.blade.php` pour un
lien cliquable + bouton copier) encodent l'adresse en base64 côté serveur et la décodent via
`atob()` côté navigateur au chargement (affichage) ou au clic (lien). Le CTA
(`cta-button.blade.php`) applique le même principe quand `$ctaLien` commence par `mailto:`
(détecté une fois dans le View Composer, `$ctaIsMailto`/`$ctaEmailB64`).

## Hors scope à ce jour

Résolution par domaine réel, action "Publier", `/gestion` (admin), upload d'images,
portfolio, `pages_liees`, schema.org (JSON-LD + microdonnées), sitemap/robots.txt
dynamiques, `llm.txt`/`llm-full.txt`.
