+
+```
+
+### Pattern 6 : Dockerfile SSR (INFRA-01 / D-12)
+
+```dockerfile
+# Source : nuxt.com/docs/deploy/docker + marcusn.dev article 2025-11
+# Note: Alpine utilisé car nodemailer n'a pas de native deps liées à glibc
+
+# Stage 1: Build
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+# Stage 2: Runtime — copie uniquement .output/
+FROM node:22-alpine AS runner
+ENV NODE_ENV=production
+ENV HOST=0.0.0.0
+ENV PORT=3000
+WORKDIR /app
+
+COPY --from=builder /app/.output /app/.output
+
+EXPOSE 3000
+CMD ["node", "/app/.output/server/index.mjs"]
+```
+
+**docker-compose.yml — modification requise (D-14) :**
+```yaml
+# Ligne à changer :
+- 'traefik.http.services.portfolio.loadbalancer.server.port=3000' # était 80
+```
+
+### Pattern 7 : nuxt-gtag production-only (INFRA-04 / D-15)
+
+```typescript
+// nuxt.config.ts — Source : nuxt.com/modules/gtag
+gtag: {
+ id: '', // Surchargé par NUXT_PUBLIC_GTAG_ID au runtime
+ enabled: process.env.NODE_ENV === 'production', // Off en dev
+},
+runtimeConfig: {
+ public: {
+ gtag: { id: '' }, // NUXT_PUBLIC_GTAG_ID — pas de rebuild nécessaire
+ },
+},
+```
+
+### Pattern 8 : NuxtImg pour les images projets
+
+```vue
+
+
+```
+
+### Anti-patterns à éviter
+
+- **Ne pas utiliser `localStorage`** pour persister état modal/gallery — toujours refs Vue
+- **Ne pas appeler `emblaApi.scrollTo()` directement après `isOpen = true`** — passer par `nextTick()` pour attendre le rendu du modal
+- **Ne pas exposer les credentials SMTP via `runtimeConfig.public`** — les mettre dans la section privée de runtimeConfig uniquement
+- **Ne pas hardcoder le port 80 dans docker-compose** — le changer à 3000 (D-14)
+- **Ne pas copier `dist/` dans le Dockerfile** — le build Nuxt SSR produit `.output/`, pas `dist/`
+
+---
+
+## Don't Hand-Roll
+
+| Problème | Ne pas construire | Utiliser | Pourquoi |
+|---------|------------------|---------|----------|
+| Modal + carousel | Custom overlay + swiper CSS | UModal + UCarousel | Nuxt UI gère a11y, focus trap, transition, dismiss on escape |
+| Validation formulaire | Regex maison ou conditions if/else | Zod + UForm | UForm consomme nativement le schéma Zod, affiche les erreurs sur les champs |
+| Notifications toast | div flottant custom | useToast() + UApp | Nuxt UI gère la pile de toasts, position, durée, icônes |
+| FAQ accordion | div + show/hide custom | UAccordion | Gère a11y ARIA, animation, type single/multiple |
+| SMTP transport | fetch directe vers OVH | nodemailer | nodemailer gère TLS, retry, pooling — critique pour OVH port 465 |
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1 : UToast sans ``
+
+**Ce qui se passe :** `useToast().add()` est appelé mais aucune notification n'apparaît.
+**Pourquoi :** Le rendu des toasts requiert `` comme wrapper — il est normalement dans `app/app.vue`.
+**Comment éviter :** Vérifier que `app/app.vue` contient `...`.
+**Signe d'alerte :** Aucune erreur console, mais les toasts silencieux.
+
+### Pitfall 2 : emblaApi null au moment du scrollTo
+
+**Ce qui se passe :** `carouselRef.value?.emblaApi?.scrollTo(index)` ne fait rien lors de l'ouverture de la galerie.
+**Pourquoi :** Le modal vient d'être monté, Embla n'est pas encore initialisé au même tick.
+**Comment éviter :** Entourer l'appel dans `nextTick(() => { ... })` après avoir mis `isOpen.value = true`.
+**Signe d'alerte :** La galerie s'ouvre toujours à l'index 0 même si on clique sur l'image 3.
+
+### Pitfall 3 : Dockerfile copie dist/ au lieu de .output/
+
+**Ce qui se passe :** `docker build` réussit mais `docker run` échoue avec "Cannot find module".
+**Pourquoi :** L'ancien Dockerfile (SPA nginx) copie `dist/`. Nuxt SSR produit `.output/server/index.mjs`.
+**Comment éviter :** Le nouveau Dockerfile doit `COPY --from=builder /app/.output /app/.output` et lancer `node /app/.output/server/index.mjs`.
+**Signe d'alerte :** `docker run` montre "Error: Cannot find module '/app/server/index.mjs'".
+
+### Pitfall 4 : runtimeConfig SMTP exposé côté client
+
+**Ce qui se passe :** Les credentials SMTP apparaissent dans le HTML rendu ou les DevTools network.
+**Pourquoi :** Si mis dans `runtimeConfig.public`, ils sont sérialisés dans le payload Nuxt visible côté client.
+**Comment éviter :** `smtpHost/User/Pass` doivent être dans la section privée de `runtimeConfig` (pas sous `public`).
+**Signe d'alerte :** `window.__NUXT__` contient les credentials SMTP.
+
+### Pitfall 5 : server/api route non trouvée en développement
+
+**Ce qui se passe :** `$fetch('/api/contact', ...)` retourne 404.
+**Pourquoi :** Nuxt doit détecter automatiquement les fichiers dans `server/api/` — s'assurer que le répertoire `server/` est à la racine du projet, pas dans `app/`.
+**Comment éviter :** Créer `server/api/contact.post.ts` à la racine (même niveau que `app/`, `nuxt.config.ts`).
+**Signe d'alerte :** 404 sur `POST /api/contact` alors que le fichier existe.
+
+### Pitfall 6 : error.vue dans le mauvais répertoire
+
+**Ce qui se passe :** Les erreurs 404 affichent la page Nuxt par défaut, pas la page custom.
+**Pourquoi :** `error.vue` doit être dans `app/` (pas dans `app/pages/`).
+**Comment éviter :** Créer `app/error.vue` (non dans pages/).
+**Signe d'alerte :** La page 404 montre le design Nuxt par défaut gris.
+
+---
+
+## Code Examples
+
+### Route dynamique project/[id].vue
+
+```vue
+
+
+```
+
+### Filtre projets (PAGE-02)
+
+```vue
+
+```
+
+---
+
+## Environment Availability
+
+| Dependency | Required By | Available | Version | Fallback |
+|------------|------------|-----------|---------|----------|
+| Node.js | Build + runtime Docker | ✓ | v25.2.1 (local) / v22 dans Docker | — |
+| Docker | INFRA-01 | [ASSUMED] | — | Tester manuellement |
+| nodemailer | COMP-02 SMTP | ✗ (à installer) | 8.0.5 sur npm | — |
+| zod | COMP-02 validation | ✗ (à installer) | 4.3.6 sur npm | — |
+| OVH SMTP (ssl0.ovh.net:465) | COMP-02 envoi email | [ASSUMED] | — | Tester avec `NUXT_SMTP_HOST` réel |
+
+**Dépendances manquantes sans fallback :**
+- OVH SMTP credentials — doivent être fournis par l'utilisateur dans `.env` avant test du formulaire
+
+**Dépendances manquantes avec fallback :**
+- Aucune
+
+---
+
+## Validation Architecture
+
+Tests automatisés exclus du scope (REQUIREMENTS.md Out of Scope : "Tests automatisés — Migration d'abord"). Validation manuelle uniquement.
+
+**Critères de succès Phase 3 (vérification manuelle) :**
+
+| Critère | Commande de vérification |
+|---------|-------------------------|
+| 8 routes SSR | `curl http://localhost:3000/` — vérifie HTML complet |
+| Galerie clavier | Ouvrir modal → flèches → Escape dans navigateur |
+| Formulaire envoi | Soumettre formulaire → vérifier réception email + toast succès |
+| Docker build | `docker build -t portfolio .` |
+| Docker run | `docker run -p 3000:3000 portfolio` → `curl localhost:3000` |
+| GA4 DebugView | Naviguer en production → vérifier events dans GA4 DebugView |
+
+---
+
+## Security Domain
+
+### Applicable ASVS Categories
+
+| ASVS Category | Applies | Standard Control |
+|---------------|---------|-----------------|
+| V2 Authentication | Non | Pas d'auth sur le portfolio |
+| V3 Session Management | Non | Pas de session |
+| V4 Access Control | Non | Toutes les pages sont publiques |
+| V5 Input Validation | Oui | Zod côté client + validation côté serveur recommandée |
+| V6 Cryptography | Non | SMTP TLS géré par nodemailer |
+
+### Threat Patterns pour le stack formulaire
+
+| Pattern | STRIDE | Mitigation standard |
+|---------|--------|---------------------|
+| Spam SMTP via API ouverte | Spoofing | Rate limiting Nitro ou validation honeypot |
+| XSS dans corps email | Tampering | Échapper le HTML dans `html:` nodemailer (pas de `innerHTML` direct) |
+| Credentials SMTP leakés | Information disclosure | Section privée runtimeConfig uniquement (jamais `public`) |
+
+**Note importante :** `server/api/contact.post.ts` est un endpoint public sans auth. Sans rate limiting, il peut être utilisé pour spammer l'adresse OVH. Pour Phase 3, ajouter une simple validation côté serveur (longueur champs) à défaut d'un vrai rate limiter.
+
+**Validation côté serveur minimale à inclure dans contact.post.ts :**
+```typescript
+const { name, email, message } = await readBody(event)
+if (!name || !email || !message || message.length > 5000) {
+ throw createError({ statusCode: 400, message: 'Invalid input' })
+}
+```
+
+---
+
+## State of the Art
+
+| Ancienne approche | Approche actuelle | Quand changé | Impact |
+|-------------------|------------------|--------------|--------|
+| nginx + dist/ (SPA) | node + .output/ (SSR) | Ce projet | Le Dockerfile entier à réécrire |
+| Custom GalleryModal.vue | UModal + UCarousel | Phase 3 | Moins de code, a11y gratuit |
+| useSeo() composable custom | useSeoMeta() Nuxt builtin | Phase 2 | Déjà migré |
+| localStorage thème | Cookie color-mode | Phase 2 | Déjà migré |
+
+---
+
+## Assumptions Log
+
+| # | Claim | Section | Risk si faux |
+|---|-------|---------|-------------|
+| A1 | `@types/nodemailer` est le package de types correct pour nodemailer 8.x | Standard Stack | Types manquants — TypeScript strict échouera ; vérifier avec `npm view @types/nodemailer` |
+| A2 | OVH SMTP fonctionne sur ssl0.ovh.net:465 avec auth PLAIN | Pattern 3 | L'envoi échoue — tester avec les vraies credentials avant de fermer la phase |
+| A3 | Docker est disponible sur la machine de déploiement de Killian | Environment Availability | INFRA-01 bloqué — confirmer avec `docker --version` |
+| A4 | `UCarousel` expose `emblaApi` directement via `useTemplateRef` dans Nuxt UI v3 | Pattern 1 | Navigation thumbnail cassée — fallback : gérer index manuellement avec `watch` |
+
+---
+
+## Open Questions
+
+1. **Port OVH SMTP**
+ - Ce qu'on sait : OVH supporte 465 (SSL) et 587 (STARTTLS)
+ - Ce qui est flou : lequel utiliser avec les credentials Killian
+ - Recommandation : tester les deux ; 465 avec `secure: true` en premier
+
+2. **Page Formation (D-19 supprimée)**
+ - Ce qu'on sait : la page est supprimée du contenu, mais un stub `fiverr.vue` + route `/fiverr` existent
+ - Ce qui est flou : faut-il une redirection `/formation` → `/` ou laisser une 404
+ - Recommandation : ajouter un middleware ou `definePageMeta({ redirect: '/' })` dans formation.vue si le stub existe encore
+
+3. **UApp dans app.vue Phase 2**
+ - Ce qu'on sait : UToast requiert `` wrapper
+ - Ce qui est flou : est-ce que `app/app.vue` de Phase 2 l'a déjà inclus
+ - Recommandation : vérifier `app/app.vue` avant d'implémenter le formulaire toast
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+- [ui.nuxt.com/components/modal](https://ui.nuxt.com/components/modal) — Props UModal, v-model:open, slots
+- [ui.nuxt.com/components/carousel](https://ui.nuxt.com/components/carousel) — Props UCarousel, emblaApi.scrollTo pattern
+- [ui.nuxt.com/components/form](https://ui.nuxt.com/components/form) — UForm + Zod schema, FormSubmitEvent
+- [ui.nuxt.com/components/accordion](https://ui.nuxt.com/components/accordion) — UAccordion items array + slots
+- [ui.nuxt.com/components/toast](https://ui.nuxt.com/components/toast) — useToast() API + UApp
+- [nuxt.com/docs/guide/directory-structure/error](https://nuxt.com/docs/guide/directory-structure/error) — error.vue pattern + clearError
+- [nuxt.com/docs/guide/directory-structure/server](https://nuxt.com/docs/guide/directory-structure/server) — defineEventHandler, readBody, useRuntimeConfig(event)
+- [image.nuxt.com/usage/nuxt-img](https://image.nuxt.com/usage/nuxt-img) — NuxtImg props loading, format, width/height
+- package.json du projet — versions installées vérifiées
+
+### Secondary (MEDIUM confidence)
+- [nuxt.com/modules/gtag](https://nuxt.com/modules/gtag) — nuxt-gtag v4 runtimeConfig + enabled production
+- [marcusn.dev/articles/2025-11/ultraslim-multistage-image-for-ssr-nuxt-4-typescript-docker](https://marcusn.dev/articles/2025-11/ultraslim-multistage-image-for-ssr-nuxt-4-typescript-docker) — Dockerfile SSR Nuxt 4 (pattern node-server)
+- [github.com/thaikolja/nuxt-nodemailer-example](https://github.com/thaikolja/nuxt-nodemailer-example) — Nodemailer dans Nuxt 4 server route
+
+### Tertiary (LOW confidence)
+- A1 à A4 dans Assumptions Log — non vérifiés en session
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard Stack : HIGH — packages vérifiés npm registry + package.json existant
+- Architecture patterns : HIGH — APIs vérifiées docs officielles Nuxt UI v3 + Nuxt 4
+- Nodemailer SMTP : MEDIUM — pattern confirmé par GitHub example, credentials OVH non testés
+- Dockerfile SSR : MEDIUM — pattern node-server confirmé par article 2025, non testé localement
+- Pitfalls : HIGH — basés sur les APIs vérifiées + erreurs connues
+
+**Research date:** 2026-04-08
+**Valid until:** 2026-05-08 (stack stable, Nuxt UI v3 en GA)