--- stepsCompleted: [1, 2, 3, 4] inputDocuments: - docs/prd-gamification.md - docs/planning-artifacts/ux-design-specification.md - docs/brainstorming-gamification-2026-01-26.md workflowType: 'architecture' project_name: 'skycel' user_name: 'Célian' date: '2026-02-01' --- # Architecture Decision Document _This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._ ## Project Context Analysis ### Requirements Overview **Functional Requirements:** 14 FRs couvrant : double entrée visiteur (FR1), transitions animées seamless (FR2), narrateur-guide contextuel (FR3), carte interactive Konva.js (FR4), arbre de compétences vis.js (FR5), compétences cliquables → projets (FR6), dialogues PNJ typewriter (FR7), barre de progression globale (FR8), chemins narratifs multiples 4-8 parcours (FR9), challenge/puzzle avant contact (FR10), easter eggs cachés (FR11), sauvegarde LocalStorage (FR12), bilingue FR/EN avec détection URL (FR13), contact comme récompense narrative (FR14). Architecturalement, ces FRs dessinent un système à **forte interactivité côté client** avec un **backend API relativement simple** (CRUD + contact + progression). La complexité réside dans l'orchestration frontend : état de progression, navigation narrative adaptative, et composants lourds en lazy-loading. **Non-Functional Requirements:** - **NFR1** : Bundle JS ≤ 170kb gzip (Nuxt + Konva + vis.js) avec lazy-loading - **NFR2** : LCP < 2.5s sur 3G - **NFR3** : Responsive avec expérience mobile adaptée (carte simplifiée) - **NFR4** : Navigateurs modernes (Chrome, Firefox, Safari, Edge — 2 dernières versions) - **NFR5** : URLs SEO-friendly, contenu accessible aux crawlers (SSR) - **NFR6** : Respect `prefers-reduced-motion` (accessibilité animations) - **NFR7** : i18n SSR via @nuxtjs/i18n avec fichiers JSON - **NFR8** : Images WebP avec lazy loading Les NFRs les plus structurants pour l'architecture sont le budget JS (NFR1), le SSR pour SEO (NFR5/NFR7), et le responsive avec deux paradigmes de navigation (NFR3). **Scale & Complexity:** - Domaine principal : Full-stack web (Nuxt 4 SSR + Laravel 12 API REST) - Niveau de complexité : **Moyenne-haute** — richesse des interactions frontend, faible volume de données - Composants architecturaux estimés : ~15-20 (pages, composants custom, stores, composables, API endpoints, modèles) ### Technical Constraints & Dependencies - **Nuxt 4 SSR** : Impose une architecture hybride serveur/client avec nouvelle structure `app/`. Les composants Konva.js et vis.js doivent être exclusivement client-side (`.client.vue`) - **Laravel 12 API-only** : Backend découplé, communication via API REST JSON. CORS requis. Upgrade vers Laravel 13 prévu dès sa sortie stable (Q1 2026) - **MariaDB** : Schéma relationnel défini dans le brainstorming (7 tables). Migration vers Eloquent ORM - **Budget JS 170kb** : Konva (~50kb) + vis-network (~50kb) + Nuxt (~50kb) = marge très faible. Stratégie de lazy-loading critique - **Monorepo** : `/frontend` (Nuxt) + `/api` (Laravel) dans le même repo — décision validée pour un projet solo avec frontend/backend fortement couplés - **Hébergement dual** : Node.js pour Nuxt SSR + PHP 8.2+ pour Laravel — deux runtimes distincts ### Cross-Cutting Concerns Identified 1. **Gestion d'état & progression** : Le store Pinia `useProgressionStore` irrigue toute l'application — carte, narrateur, barre XP, déblocage contact, easter eggs. Doit être persisté (LocalStorage) et compatible SSR 2. **Internationalisation (i18n)** : Bilingue FR/EN à travers toutes les couches — SSR, API responses, textes narrateur, dialogues PNJ, challenges. Stratégie `prefix_except_default` pour URLs 3. **Système de héros** : Le choix du personnage (Recruteur/Client/Dev) impacte le vouvoiement, le ton du narrateur, le contenu des challenges, et potentiellement l'ordre des suggestions. Transversal à toute la couche de présentation 4. **Accessibilité (WCAG AA)** : Contraste, navigation clavier, `prefers-reduced-motion`, screen readers, skip links. Impacte chaque composant custom (carte, PNJ, narrateur, skill tree) 5. **Performance & lazy-loading** : Composants lourds (Konva, vis.js) chargés à la demande. Images WebP, fonts variables, SSR pour le premier rendu. Budget strict ## Starter Template Evaluation ### Primary Technology Domain Full-stack web (Nuxt 4 SSR + Laravel API REST) basé sur l'analyse des exigences projet. ### Starter Options Considered | Option | Version | Statut | Notes | |--------|---------|--------|-------| | **Nuxt 4** | 4.3+ | Stable (juillet 2025) | Nouvelle structure `app/`, TypeScript strict, data fetching amélioré | | ~~Nuxt 3~~ | 3.21 | EOL juillet 2026 | Écarté — fin de vie trop proche pour un nouveau projet | | **Laravel 12** | 12.x | Stable (février 2025) | Release de maintenance, support bugs jusqu'en août 2026 | | Laravel 13 | 13.x | Imminent (Q1 2026) | PHP 8.3+, support jusqu'en 2028. Upgrade depuis 12 attendu facile | ### Selected Starters **Frontend : Nuxt 4** **Rationale :** Version stable et actuelle. Structure `app/` plus propre, meilleur TypeScript, Nuxt 3 en fin de vie. Tous les modules clés (@nuxtjs/i18n, @pinia/nuxt, @nuxtjs/tailwindcss, nuxt/image, @nuxtjs/sitemap) sont compatibles Nuxt 4. **Initialization Command :** ```bash npx nuxi@latest init frontend ``` **Backend : Laravel 12 (upgrade vers 13 dès sa sortie)** **Rationale :** Stable et maintenu. Laravel 13 est imminent mais pas encore sorti. Démarrer sur 12 avec PHP 8.2+ permet de commencer immédiatement. L'upgrade vers 13 sera minimal. ```bash composer create-project laravel/laravel api ``` ### Architectural Decisions Provided by Starters **Nuxt 4 fournit :** - Structure `app/` avec auto-imports (components, composables, utils) - SSR natif avec hydration client - Routing fichier-based (`app/pages/`) - Nitro comme serveur (build optimisé) - TypeScript par défaut - DevTools intégrés **Laravel 12 fournit :** - Structure MVC avec Eloquent ORM - Routing API (`routes/api.php`) - Migration system pour le schéma BDD - Form Requests pour la validation - API Resources pour les transformations JSON - Rate limiting, CORS, middleware stack - Pest/PHPUnit pour les tests ### Structure Monorepo (Nuxt 4) ``` skycel/ ├── frontend/ # Application Nuxt 4 │ ├── app/ # Code applicatif (structure Nuxt 4) │ │ ├── pages/ │ │ ├── components/ │ │ ├── composables/ │ │ ├── stores/ │ │ ├── layouts/ │ │ ├── plugins/ │ │ ├── assets/ │ │ └── app.vue │ ├── server/ # Server routes/API Nuxt (si besoin) │ ├── public/ │ ├── i18n/ │ ├── nuxt.config.ts │ └── package.json ├── api/ # Backend Laravel 12 │ ├── app/ │ ├── database/ │ ├── routes/ │ ├── config/ │ ├── tests/ │ └── composer.json ├── docs/ # Documentation projet └── README.md ``` ### Third-Party Services | Service | Solution | Intégration | |---------|----------|-------------| | **Email** | PHPMailer via Laravel Mail | Backend | | **Anti-spam** | Google reCAPTCHA v3 | Frontend + Backend validation | | **Images** | `nuxt/image` + Sharp | Frontend (local) | | **Sitemap** | `@nuxtjs/sitemap` | Frontend | | **Analytics** | Matomo (self-hosted) | Frontend script | | **Error tracking** | Sentry | Frontend + Backend | | **Monitoring** | Uptime Kuma | Externe (existant) | | **Backups BDD** | Script cron mysqldump | Serveur | ## Core Architectural Decisions ### Decision Priority Analysis **Critical Decisions (Block Implementation) :** - Stratégie i18n hybride (JSON statique + table translations centralisée) - Architecture API REST avec API Key + CORS strict - Structure composants frontend (ui / feature / layout) - Stratégie lazy-loading pour respecter le budget JS ≤ 170kb - Store Pinia de progression avec persistance LocalStorage **Important Decisions (Shape Architecture) :** - Abandon Swup.js → transitions Nuxt natives + GSAP - Double validation frontend + backend - Format de réponse API Resources avec enveloppe standard - Bandeau RGPD intégré à l'immersion narrative - Environnement staging avec sous-domaine **Deferred Decisions (Post-MVP) :** - Endpoints CRUD admin protégés par tokens exclusifs (après MVP) - Upgrade Laravel 12 → 13 (dès sortie stable) - Sauvegarde cloud de progression via email (Phase 2) ### Data Architecture **Stratégie i18n : Hybride** - **Contenu statique UI** : Fichiers JSON via @nuxtjs/i18n (`i18n/fr.json`, `i18n/en.json`). Labels, boutons, messages d'interface, textes de navigation - **Contenu dynamique** : Table `translations` centralisée en MariaDB. Les tables métier (projects, skills, testimonials, narrator_texts, easter_eggs) stockent des clés i18n (`title_key`, `text_key`). La table `translations` contient les valeurs par langue - **Rationale** : Flexibilité pour ajouter une langue sans modifier le schéma. Séparation claire entre UI (déployée avec le frontend) et contenu (géré via API/BDD) **Schéma table translations :** ```sql CREATE TABLE translations ( id INT AUTO_INCREMENT PRIMARY KEY, lang VARCHAR(5) NOT NULL, key_name VARCHAR(255) NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_translation (lang, key_name), INDEX idx_lang (lang) ); ``` **Cache : File cache Laravel** - Driver : `file` (config `CACHE_DRIVER=file`) - Suffisant pour la volumétrie d'un portfolio (faible nombre de requêtes, peu de données) - Pas de dépendance externe (Redis non requis) **Validation : Double validation** - **Frontend (Nuxt)** : Validation légère en temps réel pour l'UX (champs requis, format email, longueur). Via les composables Vue ou VeeValidate - **Backend (Laravel)** : Validation complète via Form Requests. Source de vérité pour la sécurité. Rejette toute donnée invalide avec réponse 422 ### Authentication & Security **Protection formulaire de contact :** - Google reCAPTCHA v3 (invisible, score-based) côté frontend - Honeypot field (champ caché) comme seconde couche - Rate limiting Laravel : 5 requêtes/minute par IP sur `POST /api/contact` **Sécurité API :** - **API Key** : Token partagé entre Nuxt et Laravel via header `X-API-Key`. Stocké dans les `.env` des deux applications. Middleware Laravel vérifie la présence et validité du token sur chaque requête - **CORS strict** : N'accepte que le domaine du frontend (`Access-Control-Allow-Origin: https://skycel.fr`) - **Endpoints CRUD admin** (post-MVP) : Protégés par tokens exclusifs différents de l'API Key publique. Middleware dédié avec permissions granulaires **Protection des données visiteur :** - Session ID : UUID v4 généré côté client, stocké en LocalStorage - Email : Optionnel, uniquement pour la sauvegarde cloud de progression - Pas de tracking sans consentement **RGPD :** - Bandeau de consentement intégré à l'immersion narrative (dialogue PNJ ou narrateur araignée, style "pacte d'aventurier") - Consentement requis avant activation de Matomo et stockage LocalStorage de progression - État du consentement stocké dans le store Pinia (`consentGiven`) et persisté en LocalStorage ### API & Communication Patterns **Design pattern : REST classique** Endpoints publics (lecture) : | Méthode | Endpoint | Description | |---------|----------|-------------| | `GET` | `/api/projects` | Liste des projets | | `GET` | `/api/projects/{slug}` | Détail d'un projet | | `GET` | `/api/skills` | Arbre de compétences | | `GET` | `/api/testimonials` | Témoignages PNJ | | `GET` | `/api/narrator/{context}` | Textes narrateur par contexte | | `GET` | `/api/easter-eggs` | Métadonnées easter eggs (pas les réponses) | | `GET` | `/api/progress/{session_id}` | Récupérer progression | | `POST` | `/api/progress` | Sauvegarder progression | | `POST` | `/api/contact` | Formulaire contact (rate limited + reCAPTCHA) | Endpoints admin CRUD (post-MVP, tokens exclusifs) : | Méthode | Endpoint | Description | |---------|----------|-------------| | `POST` | `/api/admin/projects` | Créer un projet | | `PUT` | `/api/admin/projects/{id}` | Modifier un projet | | `DELETE` | `/api/admin/projects/{id}` | Supprimer un projet | | _idem_ | _pour skills, testimonials, narrator, easter-eggs_ | _CRUD complet_ | **Gestion de la langue : Header `Accept-Language`** - Le frontend Nuxt envoie `Accept-Language: fr` ou `Accept-Language: en` dans chaque requête API - Middleware Laravel extrait la langue et la passe au query builder pour joindre la table `translations` - Fallback : `fr` si header absent ou langue non supportée **Format de réponse : Laravel API Resources** Réponse standard : ```json { "data": [ { "id": 1, "slug": "skycel", "title": "Skycel Portfolio", "..." : "..." } ], "meta": { "total": 5, "lang": "fr" } } ``` **Gestion d'erreurs : Format standard** ```json { "error": { "code": "VALIDATION_ERROR", "message": "Le champ email est requis", "details": {} } } ``` Codes HTTP : 400 (bad request), 401 (API key invalide), 404 (not found), 422 (validation), 429 (rate limit), 500 (erreur serveur) ### Frontend Architecture **Architecture des composants :** ``` app/components/ ├── ui/ # Composants atomiques réutilisables │ ├── BaseButton.vue │ ├── BaseBadge.vue │ ├── BaseModal.vue │ └── ... ├── feature/ # Composants métier │ ├── NarratorDialogue.vue │ ├── PnjCard.vue │ ├── SkillTree.client.vue # Client-only (vis-network) │ ├── InteractiveMap.client.vue # Client-only (Konva.js) │ ├── ProgressBar.vue │ ├── HeroSelector.vue │ ├── ChallengePanel.vue │ └── EasterEgg.vue ├── layout/ # Structure de page │ ├── AppHeader.vue │ ├── AppFooter.vue │ ├── ConsentBanner.vue # RGPD immersif │ └── NarratorOverlay.vue ``` **Stratégie lazy-loading :** | Couche | Chargement | Poids estimé (gzip) | |--------|------------|---------------------| | Nuxt core + Vue + Pinia | Immédiat | ~50kb | | TailwindCSS (purgé) | Immédiat | ~10kb | | Pages | Lazy (navigation) | ~5-10kb/page | | Konva.js | Lazy (page carte desktop uniquement) | ~50kb | | vis-network | Lazy (page skills uniquement) | ~50kb | | GSAP | Lazy (première animation complexe) | ~25kb | | reCAPTCHA v3 | Lazy (page contact uniquement) | Externe | Budget initial (premier chargement) : **~60-70kb gzip** — bien sous le budget de 170kb. Les librairies lourdes ne se chargent qu'à la demande. **Store Pinia `useProgressionStore` :** ```typescript interface ProgressionState { sessionId: string // UUID v4 hero: 'recruteur' | 'client' | 'dev' | null currentPath: string // Chemin narratif actuel visitedSections: string[] // Sections visitées completionPercent: number // 0-100 easterEggsFound: string[] // Slugs des easter eggs trouvés challengeCompleted: boolean contactUnlocked: boolean narratorStage: number // 1-5 (évolution de l'araignée) choices: Record // Choix narratifs consentGiven: boolean // RGPD } ``` - Persistance : `pinia-plugin-persistedstate` → LocalStorage - Synchronisation API : `POST /api/progress` déclenché quand le visiteur fournit son email (sauvegarde cloud optionnelle) - Compatibilité SSR : Le store s'initialise vide côté serveur, se réhydrate côté client depuis LocalStorage **Transitions et animations :** - **Transitions de page** : Système natif Nuxt/Vue (`` + ``) avec CSS animations - **Animations complexes** : GSAP (narrateur araignée, révélation progressive, transitions de zone immersives) - **Swup.js** : Abandonné — redondant avec les transitions Nuxt natives et potentiellement conflictuel - **`prefers-reduced-motion`** : Respecté via media query, animations réduites ou désactivées ### Infrastructure & Deployment **Architecture serveur :** ``` ┌─────────────────────────┐ │ Nginx (port 80/443) │ │ SSL + gzip + cache │ └─────────┬───────────────┘ │ ┌─────────────┴─────────────┐ │ │ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ │ Node.js :3000 │ │ PHP-FPM :9000 │ │ Nuxt 4 SSR │ │ Laravel 12 API │ └───────────────────┘ └───────────────────┘ │ ▼ ┌───────────────────┐ │ MariaDB :3306 │ └───────────────────┘ ``` - Nginx dispatch : `/api/*` → PHP-FPM, tout le reste → Node.js (Nuxt SSR) - SSL via Let's Encrypt (certbot) - Compression gzip activée - Headers de cache pour les assets statiques **Gestion des environnements :** - **Production** : `skycel.fr` — branche `prod` - **Staging** : `staging.skycel.fr` — branche `staging` ou `main` - Fichiers `.env` distincts par environnement et par application (`frontend/.env.production`, `frontend/.env.staging`, `api/.env.production`, `api/.env.staging`) **CI/CD : Script `deploy.sh` manuel** ```bash # Déploiement déclenché manuellement # Se base sur la branche 'prod' ./deploy.sh [production|staging] ``` Le script automatise : 1. `git pull origin prod` (ou staging) 2. `cd frontend && npm install && npm run build` 3. `cd api && composer install --no-dev && php artisan migrate --force` 4. `php artisan config:cache && php artisan route:cache` 5. Restart du process Node.js (PM2 ou systemd) 6. Notification de succès/échec **Backups BDD :** - Cron quotidien à 3h00 : `mysqldump` complet de la base skycel - Rétention locale : 7 jours (rotation automatique, suppression des dumps > 7j) - Réplication : Copie automatique vers un serveur distant via `rsync` ou `scp` après chaque dump - Nommage : `skycel_backup_YYYY-MM-DD_HH-MM.sql.gz` (compressé) ### Decision Impact Analysis **Séquence d'implémentation recommandée :** 1. Initialisation monorepo (Nuxt 4 + Laravel 12) 2. Configuration Nginx + environnements (.env, staging) 3. Schéma BDD + migrations + table translations 4. API endpoints publics (lecture) + middleware API Key + CORS 5. Store Pinia progression + persistance LocalStorage 6. Composants layout + transitions Nuxt natives 7. Pages et composants feature (par epic) 8. Intégrations tierces (reCAPTCHA, Matomo, Sentry) 9. Script deploy.sh + cron backup 10. Endpoints CRUD admin (post-MVP) **Dépendances inter-composants :** - Le store Pinia dépend du schéma de progression (BDD + API) - Les composants feature dépendent de l'API (endpoints + format de réponse) - L'i18n frontend dépend de la table translations (contenu dynamique) - Le bandeau RGPD doit être en place avant l'activation de Matomo - Le lazy-loading des composants lourds dépend de la structure de routing Nuxt