```
### 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)