` wrapper — the layout handles structure now.
-
-
- grep -q "AppHeader" app/layouts/default.vue && grep -q "AppFooter" app/layouts/default.vue && grep -q "simple-icons:gitea" app/components/layout/AppFooter.vue && grep -q "useLocaleHead" app/app.vue && grep -q "NuxtLayout" app/app.vue && echo "PASS" || echo "FAIL"
-
-
- - app/components/layout/AppFooter.vue contains `simple-icons:gitea` (not github)
- - app/components/layout/AppFooter.vue contains `simple-icons:linkedin` and `simple-icons:fiverr`
- - app/components/layout/AppFooter.vue contains `target="_blank"` and `rel="noopener noreferrer"`
- - app/components/layout/AppFooter.vue contains `t('footer.copyright')`
- - app/layouts/default.vue contains ` ` and ` `
- - app/layouts/default.vue contains ` `
- - app/app.vue contains `useLocaleHead({ addSeoAttributes: true })`
- - app/app.vue contains `` wrapping ` `
- - app/app.vue does NOT contain ` `
-
-
AppFooter renders copyright + Gitea/LinkedIn/Fiverr social icons. Default layout wraps header + slot + footer. app.vue uses NuxtLayout and injects global hreflang/canonical via useLocaleHead().
-
-
-
-
-
-## Trust Boundaries
-
-| Boundary | Description |
-|----------|-------------|
-| External links (footer) | Social icon links open external URLs in new tabs |
-
-## STRIDE Threat Register
-
-| Threat ID | Category | Component | Disposition | Mitigation Plan |
-|-----------|----------|-----------|-------------|-----------------|
-| T-02-04 | Tampering | External social links | mitigate | All external links use `rel="noopener noreferrer"` to prevent reverse tabnabbing |
-| T-02-05 | Spoofing | Locale switching | accept | setLocale only accepts 'fr' or 'en' — constrained by i18n config, no injection risk |
-
-
-
-- `pnpm dev` starts and renders header + footer on localhost:3000
-- Language toggle switches between FR/EN URLs
-- Theme toggle switches dark/light classes
-- Mobile hamburger opens UDrawer
-- `curl http://localhost:3000` returns HTML with `
-
-
-- Header sticky with nav links, FR/EN toggle, dark/light toggle, mobile drawer
-- Footer shows copyright and 3 social icon links
-- Default layout renders header + page content + footer
-- app.vue injects global hreflang/canonical metadata
-- All interactive elements have focus rings and ARIA labels
-
-
-
-After completion, create `.planning/phases/02-ssr-shell/02-02-SUMMARY.md`
-
diff --git a/.planning/phases/02-ssr-shell/02-02-SUMMARY.md b/.planning/phases/02-ssr-shell/02-02-SUMMARY.md
deleted file mode 100644
index e7c41ff..0000000
--- a/.planning/phases/02-ssr-shell/02-02-SUMMARY.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-phase: 02-ssr-shell
-plan: 02
-subsystem: layout-header-footer
-tags: [header, footer, layout, i18n-toggle, color-mode, mobile-drawer, a11y]
-dependency_graph:
- requires: [02-01]
- provides: [app-header, app-footer, default-layout, locale-head]
- affects: [app/app.vue, app/locales/fr.json, app/locales/en.json]
-tech_stack:
- added: []
- patterns: [useSetLocale, useColorMode, useLocaleHead, UDrawer]
-key_files:
- created:
- - app/components/layout/AppHeader.vue
- - app/components/layout/AppFooter.vue
- - app/layouts/default.vue
- modified:
- - app/app.vue
- - app/locales/fr.json
- - app/locales/en.json
-decisions:
- - "Language toggle shows opposite locale text (FR when en, EN when fr) per D-04"
- - "Renamed a11y.github key to a11y.gitea in both locale files to match actual Gitea link"
- - "Social icons: Gitea + LinkedIn + Fiverr per user correction over D-05"
-metrics:
- duration: 112s
- completed: 2026-04-08
----
-
-# Phase 02 Plan 02: Layout Shell (Header + Footer + Default Layout) Summary
-
-Sticky AppHeader with desktop nav, FR/EN text toggle (useSetLocale), dark/light icon toggle (useColorMode), mobile UDrawer; AppFooter with copyright + Gitea/LinkedIn/Fiverr social icons; default.vue layout wrapping header+slot+footer; app.vue updated with useLocaleHead for global hreflang/canonical.
-
-## Tasks Completed
-
-| Task | Name | Commit | Files |
-|------|------|--------|-------|
-| 1 | AppHeader with nav, language toggle, theme toggle, mobile drawer | 23fa399 | app/components/layout/AppHeader.vue |
-| 2 | AppFooter + default layout + app.vue update | cfe0180 | app/components/layout/AppFooter.vue, app/layouts/default.vue, app/app.vue, app/locales/fr.json, app/locales/en.json |
-
-## Deviations from Plan
-
-### Auto-fixed Issues
-
-**1. [Rule 2 - Correctness] Renamed a11y.github to a11y.gitea in locale files**
-- **Found during:** Task 2
-- **Issue:** a11y.github key text referenced "GitHub" but the actual link points to Gitea (gitea.kamisama.ovh)
-- **Fix:** Renamed key from `a11y.github` to `a11y.gitea` and updated text to say "Gitea" in both fr.json and en.json
-- **Files modified:** app/locales/fr.json, app/locales/en.json
-- **Commit:** cfe0180
-
-## Verification Results
-
-- AppHeader contains sticky, z-[1020], useColorMode, useSetLocale, UDrawer, heroicons icons: PASS
-- AppHeader has min-w-11 min-h-11 touch targets, focus-visible:ring-2, aria-current: PASS
-- AppFooter contains simple-icons:gitea, simple-icons:linkedin, simple-icons:fiverr: PASS
-- AppFooter has target="_blank" rel="noopener noreferrer": PASS
-- default.vue contains AppHeader, AppFooter, slot: PASS
-- app.vue contains useLocaleHead, NuxtLayout, no NuxtRouteAnnouncer: PASS
-
-## Self-Check: PASSED
diff --git a/.planning/phases/02-ssr-shell/02-03-PLAN.md b/.planning/phases/02-ssr-shell/02-03-PLAN.md
deleted file mode 100644
index 64a1b9c..0000000
--- a/.planning/phases/02-ssr-shell/02-03-PLAN.md
+++ /dev/null
@@ -1,212 +0,0 @@
----
-phase: 02-ssr-shell
-plan: 03
-type: execute
-wave: 2
-depends_on: [02-01]
-files_modified:
- - app/pages/index.vue
- - app/pages/projects.vue
- - app/pages/about.vue
- - app/pages/contact.vue
- - app/pages/fiverr.vue
- - app/pages/formation.vue
-autonomous: true
-requirements: [SEO-01, SEO-02, SEO-04]
-
-must_haves:
- truths:
- - "Every route has unique title, description, og:title, og:description in SSR HTML"
- - "Homepage includes JSON-LD Person + ProfessionalService schema"
- - "Every route has og:image with absolute URL"
- - "curl output for each route contains
and og:description meta tag"
- artifacts:
- - path: "app/pages/index.vue"
- provides: "Homepage with SEO metadata and JSON-LD"
- contains: "useSeoMeta"
- - path: "app/pages/projects.vue"
- provides: "Projects stub page with SEO metadata"
- contains: "useSeoMeta"
- key_links:
- - from: "app/pages/index.vue"
- to: "app/locales/fr.json"
- via: "t('seo.home.title') for localized SEO"
- pattern: "seo\\.home\\.title"
- - from: "app/pages/index.vue"
- to: "JSON-LD"
- via: "useHead script tag"
- pattern: "application/ld\\+json"
----
-
-
-Add per-route SEO metadata (useSeoMeta) and JSON-LD structured data to all page stubs.
-
-Purpose: Every route returns correct, unique, localized SEO tags in server-rendered HTML — verifiable by curl.
-Output: 6 page files with useSeoMeta(), homepage with JSON-LD, all with og:image.
-
-
-
-@~/.claude/get-shit-done/workflows/execute-plan.md
-@~/.claude/get-shit-done/templates/summary.md
-
-
-
-@.planning/PROJECT.md
-@.planning/ROADMAP.md
-@.planning/phases/02-ssr-shell/02-CONTEXT.md
-@.planning/phases/02-ssr-shell/02-RESEARCH.md
-@.planning/phases/02-ssr-shell/02-UI-SPEC.md
-@.planning/phases/02-ssr-shell/02-01-SUMMARY.md
-
-
-
-
-
-
-
-
-
-
-
-
-
- Task 1: Per-route SEO metadata on all page stubs
- app/pages/index.vue, app/pages/projects.vue, app/pages/about.vue, app/pages/contact.vue, app/pages/fiverr.vue, app/pages/formation.vue
-
- - app/pages/index.vue (current stub — will be enhanced)
- - app/locales/fr.json (verify seo.* keys exist)
- - .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 3: useSeoMeta per route; Pattern 4: JSON-LD)
- - .planning/phases/02-ssr-shell/02-UI-SPEC.md (SEO Contract table)
- - src/config/site.ts (siteConfig.seo.organization for JSON-LD schema data; url: https://killiandalcin.fr)
-
-
-Update each page stub to include `useSeoMeta()` with localized metadata. Pages remain stubs (minimal template content) — Phase 3 fills real content.
-
-**Pattern for every page** (example: projects.vue):
-```vue
-
-
-
-
-
{{ t('nav.projects') }}
-
Phase 3 content placeholder
-
-
-```
-
-Apply this pattern to all 6 pages using their respective seo.{page}.title and seo.{page}.description keys:
-- index.vue → seo.home.*
-- projects.vue → seo.projects.*
-- about.vue → seo.about.*
-- contact.vue → seo.contact.*
-- fiverr.vue → seo.fiverr.*
-- formation.vue → seo.formation.*
-
-All pages use `ogImage: 'https://killiandalcin.fr/og-image.png'` (per user decision: static image, no nuxt-og-image).
-
-**Homepage (index.vue) ADDITIONALLY gets JSON-LD** (per D-11, SEO-02):
-```typescript
-useHead({
- script: [
- {
- type: 'application/ld+json',
- innerHTML: JSON.stringify({
- '@context': 'https://schema.org',
- '@graph': [
- {
- '@type': 'Person',
- name: 'Killian' DAL-CIN',
- url: 'https://killiandalcin.fr',
- jobTitle: 'Developpeur Full Stack Freelance',
- email: 'contact@killiandalcin.fr',
- sameAs: [
- 'https://linkedin.com/in/killian-dal-cin',
- 'https://www.fiverr.com/users/mr_kayjaydee',
- 'https://gitea.kamisama.ovh/kayjaydee',
- ],
- },
- {
- '@type': 'ProfessionalService',
- name: 'Killian' DAL-CIN - Developpeur Full Stack',
- url: 'https://killiandalcin.fr',
- logo: 'https://killiandalcin.fr/images/logo.webp',
- priceRange: '$$$',
- areaServed: 'Worldwide',
- },
- ],
- }),
- },
- ],
-})
-```
-
-Create pages that do not yet exist (projects.vue, about.vue, contact.vue, fiverr.vue, formation.vue) as new files. Update existing index.vue.
-
-Each stub page template should have:
-- `` wrapper (per D-16)
-- An `
` using the nav translation key
-- A placeholder paragraph
-
-
- grep -q "useSeoMeta" app/pages/index.vue && grep -q "application/ld+json" app/pages/index.vue && grep -q "useSeoMeta" app/pages/projects.vue && grep -q "useSeoMeta" app/pages/about.vue && grep -q "useSeoMeta" app/pages/contact.vue && grep -q "useSeoMeta" app/pages/fiverr.vue && grep -q "useSeoMeta" app/pages/formation.vue && grep -q "og-image.png" app/pages/index.vue && echo "PASS" || echo "FAIL"
-
-
- - All 6 page files exist under app/pages/
- - Every page contains `useSeoMeta` with title, description, ogTitle, ogDescription, ogImage
- - ogImage value is `https://killiandalcin.fr/og-image.png` on every page
- - index.vue contains `application/ld+json` with `Person` and `ProfessionalService`
- - index.vue JSON-LD contains `sameAs` array with LinkedIn, Fiverr, Gitea URLs
- - Each page uses localized seo keys: `t('seo.home.title')`, `t('seo.projects.title')`, etc.
- - Each page template has `max-w-7xl mx-auto` wrapper
- - `npx nuxi typecheck` passes
-
- All 6 routes have unique, localized SEO metadata via useSeoMeta(). Homepage includes JSON-LD with Person + ProfessionalService schema. Every page has og:image with absolute URL.
-
-
-
-
-
-## Trust Boundaries
-
-| Boundary | Description |
-|----------|-------------|
-| SEO meta tags | Server-rendered meta tags include user-controlled translation values |
-
-## STRIDE Threat Register
-
-| Threat ID | Category | Component | Disposition | Mitigation Plan |
-|-----------|----------|-----------|-------------|-----------------|
-| T-02-06 | Injection | JSON-LD innerHTML | mitigate | JSON.stringify() escapes special characters; no user input in JSON-LD — all values are hardcoded constants |
-| T-02-07 | Information Disclosure | og:image URL | accept | Public URL pointing to public image — no sensitive data |
-
-
-
-- `pnpm dev` then `curl http://localhost:3000` returns HTML containing ``, `og:title`, `og:description` meta tags, and JSON-LD script
-- `curl http://localhost:3000/en/` returns English title/description
-- `curl http://localhost:3000/projects` returns projects-specific title
-- Each page curl output contains `og-image.png` in a meta tag
-
-
-
-- All 6 routes have unique, localized SEO metadata in server-rendered HTML
-- Homepage JSON-LD contains Person + ProfessionalService
-- og:image present on every route with absolute URL
-- `npx nuxi typecheck` passes
-
-
-
-After completion, create `.planning/phases/02-ssr-shell/02-03-SUMMARY.md`
-
diff --git a/.planning/phases/02-ssr-shell/02-03-SUMMARY.md b/.planning/phases/02-ssr-shell/02-03-SUMMARY.md
deleted file mode 100644
index da654d7..0000000
--- a/.planning/phases/02-ssr-shell/02-03-SUMMARY.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-phase: 02-ssr-shell
-plan: 03
-subsystem: seo-metadata
-tags: [seo, json-ld, useSeoMeta, og-tags, i18n]
-dependency_graph:
- requires: [02-01]
- provides: [per-route-seo, json-ld-homepage, og-image-all-routes]
- affects: [app/pages/]
-tech_stack:
- added: []
- patterns: [useSeoMeta-per-route, useHead-json-ld, reactive-i18n-seo]
-key_files:
- created:
- - app/pages/projects.vue
- - app/pages/about.vue
- - app/pages/contact.vue
- - app/pages/fiverr.vue
- - app/pages/formation.vue
- modified:
- - app/pages/index.vue
-decisions:
- - "JSON-LD values hardcoded (not from i18n) per threat model T-02-06 — avoids injection risk"
- - "ogImage uses static absolute URL per D-12 decision"
-metrics:
- duration: 48s
- completed: 2026-04-08
----
-
-# Phase 02 Plan 03: Per-route SEO Metadata Summary
-
-useSeoMeta() on all 6 page stubs with localized title/description/og tags via reactive i18n getters, homepage JSON-LD with Person + ProfessionalService schema, og:image absolute URL on every route.
-
-## Tasks Completed
-
-| Task | Name | Commit | Files |
-|------|------|--------|-------|
-| 1 | Per-route SEO metadata on all page stubs | 0a58201 | app/pages/index.vue, projects.vue, about.vue, contact.vue, fiverr.vue, formation.vue |
-
-## Deviations from Plan
-
-None - plan executed exactly as written.
-
-## Verification Results
-
-- All 6 pages contain useSeoMeta: PASS
-- index.vue contains application/ld+json: PASS
-- All pages contain og-image.png absolute URL: PASS
-- JSON-LD contains sameAs with LinkedIn, Fiverr, Gitea: PASS
-
-## Self-Check: PASSED
diff --git a/.planning/phases/02-ssr-shell/02-CONTEXT.md b/.planning/phases/02-ssr-shell/02-CONTEXT.md
deleted file mode 100644
index ed0bb27..0000000
--- a/.planning/phases/02-ssr-shell/02-CONTEXT.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Phase 2: SSR Shell - Context
-
-**Gathered:** 2026-04-08
-**Status:** Ready for planning
-
-
-## Phase Boundary
-
-Chaque route rend la bonne langue (FR/EN), le bon thème (dark/light), et les bonnes métadonnées SEO côté serveur — confirmé par `curl` sans JavaScript. Le header et footer sont en place avec navigation responsive. Aucune page de contenu n'est construite (Phase 3).
-
-
-
-
-## Implementation Decisions
-
-### Header & Navigation
-- **D-01:** Barre horizontale classique — logo à gauche, liens de navigation alignés à droite, toggles langue/thème à l'extrémité droite
-- **D-02:** Navigation mobile via UDrawer latéral (Nuxt UI v3) — bouton hamburger ouvre un drawer glissant depuis la gauche avec liens + toggles
-- **D-03:** Header sticky permanent (fixe en haut au scroll)
-- **D-04:** Switch langue = bouton texte simple FR/EN (toggle au clic), pas de dropdown ni drapeaux
-
-### Footer
-- **D-05:** Footer minimaliste — une seule bande : copyright © 2026 Killian' DAL-CIN + icônes réseaux sociaux (Gitea, LinkedIn, Fiverr). Note : siteConfig pointe vers gitea.kamisama.ovh, pas GitHub.
-
-### i18n SSR
-- **D-06:** Enrichir les fichiers existants fr.json/en.json avec les clés navigation, footer et SEO — un seul fichier par langue
-- **D-07:** Config @nuxtjs/i18n déjà en place : strategy prefix_except_default, FR par défaut, détection navigateur + cookie
-
-### Thème dark/light
-- **D-08:** Dark mode par défaut pour les nouveaux visiteurs (cohérent avec l'ancien site)
-- **D-09:** Persistance cookie via @nuxtjs/color-mode, pas de localStorage, pas de FOUC
-
-### SEO & Métadonnées
-- **D-10:** useSeoMeta() par route — title, description, og:title, og:description uniques
-- **D-11:** JSON-LD sur la page d'accueil : schéma Person + ProfessionalService pour Killian' DAL-CIN
-- **D-12:** og:image statique dans public/ (og-image.png 1200x630) — nuxt-og-image dynamique reporté à Phase 3 suite aux risques Windows identifiés en recherche
-
-### Sitemap
-- **D-13:** Toutes les pages publiques incluses dans le sitemap sauf la 404
-- **D-14:** Alternates hreflang FR/EN automatiques via intégration @nuxtjs/sitemap + @nuxtjs/i18n
-
-### Layout global
-- **D-15:** Default layout Nuxt : header + slot + footer
-- **D-16:** Largeur max du contenu : max-w-7xl (1280px), centré
-
-### Design system
-- **D-17:** Couleur primaire conservée : #85cb85 (vert menthe) — identité visuelle du site actuel
-- **D-18:** Secondaires adaptées selon la règle 60-30-10 : 60% dominant (backgrounds), 30% secondaire (cartes, sections), 10% accent (#85cb85 pour CTA, liens, highlights)
-- **D-19:** Règles design à respecter : contraste WCAG 4.5:1 minimum texte, palette 3-5 couleurs max, tester en niveaux de gris
-- **D-20:** Tokens Nuxt UI v3 personnalisés dans app.config.ts pour mapper la palette
-
-### Claude's Discretion
-- Choix des icônes pour le toggle thème (soleil/lune) et les réseaux sociaux
-- Animation/transition du toggle thème
-- Espacement et padding internes du layout
-
-
-
-
-## Canonical References
-
-**Downstream agents MUST read these before planning or implementing.**
-
-### Project & Requirements
-- `.planning/REQUIREMENTS.md` — Requirements I18N-01 à I18N-05, THEME-01 à THEME-03, SEO-01 à SEO-04, COMP-05, COMP-06
-- `.planning/ROADMAP.md` — Phase 2 success criteria (5 critères curl-based)
-- `.planning/phases/01-foundation/01-CONTEXT.md` — Décisions Phase 1 (structure données, composables, images)
-
-### Codebase existant (référence pour migration)
-- `src/components/layout/AppHeader.vue` — Header actuel à migrer
-- `src/components/layout/AppFooter.vue` — Footer actuel à migrer
-- `src/assets/main.css` — Variables CSS actuelles (--color-primary: #85cb85)
-- `src/locales/en.ts` et `src/locales/fr.ts` — Traductions source à migrer vers JSON
-- `src/composables/useTheme.ts` — Logique thème actuelle (localStorage → cookie)
-- `src/composables/useSeo.ts` — Logique SEO actuelle (DOM direct → useSeoMeta)
-
-### Configuration Nuxt en place
-- `nuxt.config.ts` — Modules déjà configurés : @nuxt/ui, @nuxtjs/i18n, @nuxtjs/sitemap, nuxt-gtag, @nuxt/image
-- `app/locales/fr.json` et `app/locales/en.json` — Fichiers i18n Nuxt actuels à enrichir
-
-
-
-
-## Existing Code Insights
-
-### Reusable Assets
-- `src/components/layout/AppHeader.vue` — Structure navigation, liens, toggles à migrer vers composants Nuxt UI v3
-- `src/components/layout/AppFooter.vue` — Structure footer avec réseaux sociaux à simplifier
-- `src/config/site.ts` — siteConfig avec liens sociaux, contact info, SEO defaults
-- `app/locales/fr.json` et `en.json` — Fichiers i18n déjà en place avec clés projets
-
-### Established Patterns
-- Nuxt 4 avec `app/` directory, auto-imports, compatibilityVersion 4
-- @nuxtjs/i18n configuré prefix_except_default, FR défaut, cookie
-- Composables Nuxt natifs (useProjects déjà migré)
-- Données statiques dans `app/data/` avec clés i18n
-
-### Integration Points
-- `app/app.vue` — Point d'entrée pour le default layout
-- `nuxt.config.ts` — Ajouter @nuxtjs/color-mode et nuxt-og-image aux modules
-- `app.config.ts` — Tokens Nuxt UI v3 (couleur primaire, thème)
-- `app/layouts/default.vue` — À créer : header + slot + footer
-
-
-
-
-## Specific Ideas
-
-- **Couleur primaire #85cb85** — vert menthe, identité visuelle à conserver absolument
-- **Règle 60-30-10** pour la distribution des couleurs — l'utilisateur a fourni un guide complet sur la théorie des couleurs à appliquer
-- **Accessibilité WCAG** — ratio contraste 4.5:1 minimum, jamais rouge/vert seuls comme indicateurs
-
-
-
-
-## Deferred Ideas
-
-None — discussion stayed within phase scope
-
-
-
----
-
-*Phase: 02-ssr-shell*
-*Context gathered: 2026-04-08*
diff --git a/.planning/phases/02-ssr-shell/02-DISCUSSION-LOG.md b/.planning/phases/02-ssr-shell/02-DISCUSSION-LOG.md
deleted file mode 100644
index 0a2456d..0000000
--- a/.planning/phases/02-ssr-shell/02-DISCUSSION-LOG.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# Phase 2: SSR Shell - Discussion Log
-
-> **Audit trail only.** Do not use as input to planning, research, or execution agents.
-> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
-
-**Date:** 2026-04-08
-**Phase:** 02-ssr-shell
-**Areas discussed:** Header & navigation, Footer, i18n SSR, SEO & métadonnées, Thème dark/light, Layout global, Sitemap & hreflang, Design system
-
----
-
-## Header & Navigation
-
-### Navigation desktop
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Barre horizontale | Logo gauche, liens droite, toggles extrémité droite. Pattern portfolio classique. | ✓ |
-| Centré avec logo | Logo centré, liens de chaque côté. Style plus créatif. | |
-
-**User's choice:** Barre horizontale
-**Notes:** Aucune
-
-### Switch de langue
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Code texte FR/EN | Bouton toggle simple affichant le code langue | ✓ |
-| Dropdown sélecteur | USelect avec liste des langues | |
-| Drapeaux | Icônes drapeau cliquables | |
-
-**User's choice:** Code texte FR/EN
-**Notes:** Aucune
-
-### Navigation mobile
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| UDrawer latéral | Hamburger → drawer glissant avec liens + toggles | ✓ |
-| Menu plein écran | Overlay plein écran, liens centrés en grand | |
-
-**User's choice:** UDrawer latéral
-**Notes:** Aucune
-
-### Header sticky
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Sticky permanent | Header fixe en haut pendant le scroll | ✓ |
-| Sticky hide/show | Disparaît au scroll bas, réapparaît au scroll haut | |
-| Statique | Défile avec la page | |
-
-**User's choice:** Sticky permanent
-**Notes:** Aucune
-
----
-
-## Footer
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Minimaliste | Une bande : copyright + icônes réseaux sociaux | ✓ |
-| Multi-colonnes | Colonnes Navigation, Contact, Social | |
-
-**User's choice:** Minimaliste
-**Notes:** Aucune
-
----
-
-## i18n SSR
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Enrichir fichiers existants | Ajouter clés nav/footer/SEO dans fr.json et en.json | ✓ |
-| Fichiers séparés par domaine | nav.json, footer.json, seo.json par langue | |
-
-**User's choice:** Enrichir fichiers existants
-**Notes:** Aucune
-
----
-
-## SEO & métadonnées
-
-### JSON-LD
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Person + ProfessionalService | Double schéma pour Knowledge Panel | ✓ |
-| Person seul | Schéma simple | |
-
-**User's choice:** Person + ProfessionalService
-**Notes:** Aucune
-
-### og:image
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Image statique unique | Une og:image générique dans public/ | |
-| Image par page | Différentes og:image manuelles | |
-| Génération dynamique (v2) | Via nuxt-og-image | ✓ |
-
-**User's choice:** Génération dynamique via nuxt-og-image
-**Notes:** Initialement prévu SEOV2-01, l'utilisateur a choisi de l'avancer à Phase 2
-
----
-
-## Thème dark/light
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Dark | Thème sombre par défaut, cohérent avec l'ancien site | ✓ |
-| Préférence système | Détecte prefers-color-scheme | |
-| Light | Thème clair par défaut | |
-
-**User's choice:** Dark
-**Notes:** Aucune
-
----
-
-## Layout global
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| max-w-7xl / 1280px | Standard Tailwind, bon équilibre | ✓ |
-| max-w-6xl / 1152px | Plus resserré | |
-| Pleine largeur | Pas de max-width | |
-
-**User's choice:** max-w-7xl / 1280px
-**Notes:** Aucune
-
----
-
-## Sitemap & hreflang
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Tout inclure sauf 404 | Toutes pages publiques + hreflang auto | ✓ |
-| Exclure Fiverr/Formation | Pages secondaires exclues | |
-
-**User's choice:** Tout inclure sauf 404
-**Notes:** Aucune
-
----
-
-## Design system
-
-| Option | Description | Selected |
-|--------|-------------|----------|
-| Bleu/Indigo | Classique tech/dev | |
-| Vert/Émeraude | Plus original | |
-| Conserver couleurs actuelles | Reprendre palette du site Vue 3 | ✓ (adapté) |
-
-**User's choice:** Garder la couleur primaire (#85cb85 vert menthe) et adapter les secondaires
-**Notes:** L'utilisateur a fourni un guide complet sur la théorie des couleurs : règle 60-30-10, contraste WCAG 4.5:1, palette 3-5 couleurs max, schémas harmonieux, tester en niveaux de gris.
-
----
-
-## Claude's Discretion
-
-- Icônes toggle thème (soleil/lune)
-- Animation/transition du toggle thème
-- Espacement et padding internes du layout
-
-## Deferred Ideas
-
-Aucune — la discussion est restée dans le scope de la phase.
diff --git a/.planning/phases/02-ssr-shell/02-RESEARCH.md b/.planning/phases/02-ssr-shell/02-RESEARCH.md
deleted file mode 100644
index 95aea74..0000000
--- a/.planning/phases/02-ssr-shell/02-RESEARCH.md
+++ /dev/null
@@ -1,764 +0,0 @@
-# Phase 2: SSR Shell - Research
-
-**Researched:** 2026-04-08
-**Domain:** Nuxt 4 SSR — i18n, color-mode, SEO metadata, sitemap, layout, Nuxt UI v3 theming
-**Confidence:** HIGH
-
----
-
-
-## User Constraints (from CONTEXT.md)
-
-### Locked Decisions
-
-- **D-01:** Header horizontal — logo left, nav links right, lang/theme toggles far right
-- **D-02:** Mobile nav via UDrawer (Nuxt UI v3) — hamburger opens left-sliding drawer
-- **D-03:** Header sticky permanent
-- **D-04:** Language switch = simple text button FR/EN (toggle on click), no dropdown, no flags
-- **D-05:** Footer minimal — single band: copyright © 2026 Killian' DAL-CIN + social icons (GitHub, LinkedIn, Fiverr)
-- **D-06:** Enrich existing fr.json/en.json with nav, footer, SEO keys — one file per language
-- **D-07:** @nuxtjs/i18n already configured: strategy prefix_except_default, FR default, browser detection + cookie
-- **D-08:** Dark mode default for new visitors
-- **D-09:** Cookie persistence via @nuxtjs/color-mode — no localStorage, no FOUC
-- **D-10:** useSeoMeta() per route — unique title, description, og:title, og:description
-- **D-11:** JSON-LD on homepage: Person + ProfessionalService schema for Killian' DAL-CIN
-- **D-12:** og:image dynamic via nuxt-og-image (advanced from SEOV2-01)
-- **D-13:** All public pages in sitemap except 404
-- **D-14:** hreflang FR/EN alternates automatic via @nuxtjs/sitemap + @nuxtjs/i18n integration
-- **D-15:** Default Nuxt layout: header + slot + footer
-- **D-16:** Content max-width: max-w-7xl (1280px), centered
-- **D-17:** Primary color retained: #85cb85 (mint green)
-- **D-18:** 60-30-10 color rule applied
-- **D-19:** WCAG contrast 4.5:1 minimum, palette 3-5 colors max
-- **D-20:** Nuxt UI v3 custom tokens in app.config.ts
-
-### Claude's Discretion
-
-- Choice of icons for theme toggle (sun/moon) and social networks
-- Animation/transition of theme toggle
-- Internal spacing and padding of layout
-
-### Deferred Ideas (OUT OF SCOPE)
-
-None — discussion stayed within phase scope
-
-
----
-
-
-## Phase Requirements
-
-| ID | Description | Research Support |
-|----|-------------|------------------|
-| I18N-01 | FR and EN support with prefix_except_default strategy | @nuxtjs/i18n v10 already configured in nuxt.config.ts; research confirms strategy is operational |
-| I18N-02 | Locale detected from browser on first access, persisted in cookie | detectBrowserLanguage.useCookie: true already set; setLocale() updates cookie on switch |
-| I18N-03 | User can change language via header switcher | setLocale() from useI18n composable + useSwitchLocalePath() for URL navigation |
-| I18N-04 | Server reads cookie and renders correct language without hydration mismatch | Cookie-based detection with @nuxtjs/i18n SSR — confirmed supported in v10 |
-| I18N-05 | FR/EN translation files migrated from existing locales | src/locales/fr.ts is rich — needs migration to app/locales/fr.json (TypeScript → JSON) |
-| THEME-01 | Toggle between dark and light mode via header button | @nuxtjs/color-mode v4 via useColorMode() composable |
-| THEME-02 | Theme persisted in SSR-safe cookie (not localStorage) | colorMode.storage: 'cookie' in nuxt.config.ts |
-| THEME-03 | No FOUC on load — server renders correct theme from first request | Cookie sent on first request → server knows the theme → no client-side flash |
-| SEO-01 | Per-route title, description, og:title, og:description via useSeoMeta() | useSeoMeta() is a Nuxt built-in — no extra install needed |
-| SEO-02 | Homepage JSON-LD: Person + ProfessionalService | useHead() with script tag + JSON.stringify of schema object |
-| SEO-03 | sitemap.xml auto-generated with i18n hreflang alternates | @nuxtjs/sitemap v8 already installed; auto-detects @nuxtjs/i18n with no extra config |
-| SEO-04 | og:image uses absolute URLs, present on every page | nuxt-og-image v6 (needs install) OR useSeoMeta({ ogImage: '/portfolio-preview.webp' }) |
-| COMP-05 | Header with desktop nav + mobile drawer + lang/theme toggles | app/components/layout/AppHeader.vue — UNavigationMenu or native nav + UDrawer + UButton |
-| COMP-06 | Footer with links and info | app/components/layout/AppFooter.vue — single band with NuxtLink social icons |
-
-
----
-
-## Summary
-
-Phase 2 builds the SSR shell: a Nuxt 4 default layout with a sticky header and minimal footer, i18n cookie persistence, cookie-based dark/light theming, route-level SEO metadata, and an auto-generated sitemap with hreflang.
-
-The Nuxt 4 foundation from Phase 1 has the core modules already installed: `@nuxtjs/i18n` (v10.2.4), `@nuxtjs/sitemap` (v8.0.12), and `@nuxt/ui` (v3.3.7). Two modules need to be added: `@nuxtjs/color-mode` (v4.0.0) and `nuxt-og-image` (v6.3.3). The existing `src/locales/fr.ts` is a rich source for migration to `app/locales/fr.json`.
-
-The key SSR constraint is that **all state persistence must use cookies** — localStorage is invisible to the server and causes hydration mismatches. Both `@nuxtjs/color-mode` (with `storage: 'cookie'`) and `@nuxtjs/i18n` (with `detectBrowserLanguage.useCookie: true`) satisfy this constraint.
-
-**Primary recommendation:** Add `@nuxtjs/color-mode` and `nuxt-og-image` to nuxt.config.ts, define the `app/layouts/default.vue` with AppHeader + slot + AppFooter, define the custom color palette in CSS `@theme`, reference it from `app.config.ts`, and wire `useSeoMeta()` + `useLocaleHead()` in `app/app.vue`.
-
----
-
-## Standard Stack
-
-### Core (already installed)
-
-| Library | Version Installed | Purpose | Source |
-|---------|-------------------|---------|--------|
-| @nuxtjs/i18n | 10.2.4 | FR/EN routing, cookie detection, setLocale | [VERIFIED: package.json] |
-| @nuxtjs/sitemap | 8.0.12 | Auto XML sitemap with hreflang | [VERIFIED: package.json] |
-| @nuxt/ui | 3.3.7 | UDrawer, UButton, UIcon, UNavigationMenu | [VERIFIED: package.json] |
-| nuxt | 4.4.2 | SSR runtime, useSeoMeta, useHead, useI18n | [VERIFIED: package.json] |
-
-### Needs Installation
-
-| Library | Latest Version | Purpose | Source |
-|---------|----------------|---------|--------|
-| @nuxtjs/color-mode | 4.0.0 | Cookie-based dark/light, FOUC-free SSR | [VERIFIED: npm registry] |
-| nuxt-og-image | 6.3.3 | Dynamic og:image generation | [VERIFIED: npm registry] |
-
-### Alternatives Considered
-
-| Instead of | Could Use | Tradeoff |
-|------------|-----------|----------|
-| @nuxtjs/color-mode | Manual cookie + class injection | color-mode handles the inline script that prevents FOUC before hydration — custom is error-prone |
-| nuxt-og-image | useSeoMeta({ ogImage: '/portfolio-preview.webp' }) | Static image is simpler; nuxt-og-image adds dynamic per-route images. D-12 locks nuxt-og-image |
-| useLocaleHead() | Manual hreflang in useSeoMeta | useLocaleHead() generates canonical + og:locale + all hreflang in one call |
-
-**Installation:**
-```bash
-npm install @nuxtjs/color-mode nuxt-og-image
-```
-
----
-
-## Architecture Patterns
-
-### Recommended Project Structure (Phase 2 additions)
-
-```
-app/
-├── layouts/
-│ └── default.vue # header + + footer
-├── components/
-│ └── layout/
-│ ├── AppHeader.vue # sticky nav, lang/theme toggles
-│ └── AppFooter.vue # copyright + social icons
-├── assets/
-│ └── css/
-│ └── main.css # @theme with --color-brand-* shades
-├── locales/
-│ ├── fr.json # enriched from src/locales/fr.ts
-│ └── en.json # enriched from src/locales/en.ts
-└── app.vue # useLocaleHead() + htmlAttrs lang
-app.config.ts # ui.colors.primary: 'brand'
-nuxt.config.ts # add color-mode + nuxt-og-image modules
-```
-
-### Pattern 1: Cookie-based Color Mode (FOUC-free)
-
-**What:** @nuxtjs/color-mode injects an inline script before Nuxt loads. This script reads the cookie and applies the color class to `` synchronously, before any paint. The server also reads the cookie and renders the correct class.
-
-**When to use:** Required when `storage: 'cookie'` — the only SSR-safe approach.
-
-**nuxt.config.ts:**
-```typescript
-// Source: color-mode.nuxtjs.org/usage/configuration [CITED]
-colorMode: {
- preference: 'dark', // default for new visitors — D-08
- fallback: 'dark', // fallback when no system preference
- storage: 'cookie', // SSR-safe — D-09
- storageKey: 'nuxt-color-mode',
- cookieAttrs: {
- 'max-age': '31536000', // 1 year
- path: '/',
- SameSite: 'Lax',
- },
- classSuffix: '', // class="dark" not class="dark-mode"
-},
-```
-
-**CRITICAL:** Nuxt UI v3 automatically registers `@nuxtjs/color-mode` — do NOT add both manually. Use `ui.colorMode` options or configure via the `colorMode` key. Verify if adding it separately causes double-registration. [VERIFIED: ui.nuxt.com/docs/getting-started/integrations/color-mode/nuxt]
-
-**Nuxt UI auto-registers color-mode** — the correct approach is to configure it via `colorMode` in `nuxt.config.ts` without adding it to `modules[]` separately. If already registered by `@nuxt/ui`, adding to `modules[]` is redundant.
-
-**ThemeToggle usage:**
-```typescript
-// Source: Nuxt Color Mode docs [CITED: color-mode.nuxtjs.org]
-const colorMode = useColorMode()
-// Toggle:
-colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
-```
-
-### Pattern 2: Language Switcher (cookie-persisted)
-
-**What:** `setLocale(code)` from `@nuxtjs/i18n` switches the locale, updates the cookie, and navigates to the localized URL. This is the correct approach — never mutate `locale.value` directly.
-
-**When to use:** Language toggle button (D-04).
-
-```typescript
-// Source: i18n.nuxtjs.org/docs/guide/lang-switcher [CITED]
-const { locale } = useI18n()
-const setLocale = useSetLocale()
-
-function toggleLocale() {
- setLocale(locale.value === 'fr' ? 'en' : 'fr')
-}
-```
-
-Note: `useSetLocale()` is the dedicated composable in @nuxtjs/i18n v10. `useI18n().setLocale` also exists but the standalone composable is preferred for components that only need switching.
-
-### Pattern 3: Route-level SEO with hreflang
-
-**What:** Combine `useSeoMeta()` for page-specific tags and `useLocaleHead()` for i18n-generated hreflang/canonical/og:locale. Use `useHead()` to merge them.
-
-**When to use:** Every page (SEO-01, SEO-02, SEO-03).
-
-```typescript
-// Source: i18n.nuxtjs.org/docs/guide/seo [CITED]
-// In app.vue or each page:
-const { locale } = useI18n()
-const head = useLocaleHead({ addSeoAttributes: true })
-
-useHead({
- htmlAttrs: { lang: locale },
- link: computed(() => head.value.link || []),
- meta: computed(() => head.value.meta || []),
-})
-
-// Per-page SEO in each page component:
-useSeoMeta({
- title: () => t('seo.home.title'),
- description: () => t('seo.home.description'),
- ogTitle: () => t('seo.home.title'),
- ogDescription: () => t('seo.home.description'),
-})
-```
-
-**i18n baseUrl required** for canonical + hreflang to generate absolute URLs:
-```typescript
-// nuxt.config.ts
-i18n: {
- baseUrl: 'https://killiandalcin.fr',
- // ...existing config
-}
-```
-
-### Pattern 4: JSON-LD on Homepage (SEO-02)
-
-**What:** Use `useHead()` with a `script` entry containing the serialized JSON-LD object.
-
-**When to use:** Homepage only (D-11).
-
-```typescript
-// Source: Nuxt docs + Schema.org Person spec [ASSUMED pattern, standard approach]
-useHead({
- script: [
- {
- type: 'application/ld+json',
- innerHTML: JSON.stringify({
- '@context': 'https://schema.org',
- '@type': 'Person',
- name: 'Killian' DAL-CIN',
- url: 'https://killiandalcin.fr',
- jobTitle: 'Développeur Full Stack Freelance',
- sameAs: [
- 'https://linkedin.com/in/killian-dal-cin',
- 'https://www.fiverr.com/users/mr_kayjaydee',
- ],
- }),
- },
- ],
-})
-```
-
-### Pattern 5: Custom Primary Color in Nuxt UI v3
-
-**What:** Nuxt UI v3 uses Tailwind v4's CSS `@theme` directive. Custom colors must be defined as CSS variables with all shades (50–950), then referenced by name in `app.config.ts`.
-
-**When to use:** D-17, D-20 — #85cb85 as brand primary.
-
-```css
-/* app/assets/css/main.css — Source: ui.nuxt.com/docs/getting-started/theme/design-system [CITED] */
-@import "tailwindcss";
-@import "@nuxt/ui";
-
-@theme {
- --color-brand-50: #f0faf0;
- --color-brand-100: #dcf3dc;
- --color-brand-200: #bbe8bb;
- --color-brand-300: #8dd98d;
- --color-brand-400: #a3d6a3; /* dark mode accent */
- --color-brand-500: #85cb85; /* primary — D-17 */
- --color-brand-600: #5aaa5a;
- --color-brand-700: #3f8c3f;
- --color-brand-800: #2e6b2e;
- --color-brand-900: #1f4f1f;
- --color-brand-950: #122d12;
-}
-```
-
-```typescript
-// app.config.ts — Source: ui.nuxt.com/docs/getting-started/theme/design-system [CITED]
-export default defineAppConfig({
- ui: {
- colors: {
- primary: 'brand',
- },
- },
-})
-```
-
-### Pattern 6: Sitemap with i18n hreflang
-
-**What:** `@nuxtjs/sitemap` v8 auto-detects `@nuxtjs/i18n` and generates hreflang `` entries for every locale. No manual sitemap config needed for basic hreflang.
-
-**Required:** `i18n.baseUrl` must be set (same as SEO canonical requirement).
-
-```typescript
-// nuxt.config.ts — sitemap auto-detects i18n, no extra sitemap config needed
-// Source: nuxtseo.com/docs/sitemap/integrations/i18n [CITED]
-sitemap: {
- // autoI18n: true ← default when @nuxtjs/i18n is detected
- // excludeAppSources: false ← default, generates all routes
-}
-```
-
-### Pattern 7: nuxt-og-image (D-12)
-
-**What:** `defineOgImage()` composable called in page components generates a per-route og:image. For Phase 2 (stub pages), a static fallback is acceptable — use `defineOgImage({ component: 'NuxtSeo' })` or point to the existing `/portfolio-preview.webp` static image.
-
-**Simplest Phase 2 approach:** Use the static image for now, hook up dynamic generation in Phase 3.
-
-```typescript
-// pages/index.vue — static og:image fallback
-useSeoMeta({
- ogImage: 'https://killiandalcin.fr/portfolio-preview.webp',
- ogImageWidth: 1200,
- ogImageHeight: 630,
-})
-// OR install nuxt-og-image and call defineOgImage() per page
-```
-
-**site.url required for absolute URLs:**
-```typescript
-// nuxt.config.ts
-site: {
- url: 'https://killiandalcin.fr',
- name: 'Killian' DAL-CIN — Développeur Full Stack',
-},
-```
-
-### Anti-Patterns to Avoid
-
-- **localStorage for theme or locale:** Invisible to SSR — causes hydration mismatch. Use cookies only (D-09).
-- **Directly mutating `locale.value`:** Bypasses cookie update and route navigation. Always use `setLocale()`.
-- **Adding `@nuxtjs/color-mode` to `modules[]` when using `@nuxt/ui`:** Nuxt UI already registers it — double-registration causes configuration conflicts. Configure via `colorMode:` key in `nuxt.config.ts` only.
-- **Relative og:image URLs:** Search engines require absolute URLs. Always prefix with `https://killiandalcin.fr`.
-- **Defining all SEO in `app.vue`:** Per-route metadata must be in page components via `useSeoMeta()`. app.vue handles only global hreflang/canonical via `useLocaleHead()`.
-- **i18n without `baseUrl`:** Without `baseUrl`, `useLocaleHead()` generates relative canonical and hreflang — functionally broken for SEO crawlers.
-
----
-
-## Don't Hand-Roll
-
-| Problem | Don't Build | Use Instead | Why |
-|---------|-------------|-------------|-----|
-| FOUC-free dark mode | Inline script that reads cookie before paint | @nuxtjs/color-mode | The inline script timing is extremely subtle — wrong placement causes flash on some browsers |
-| hreflang generation | Manual ` ` in useHead | useLocaleHead() from @nuxtjs/i18n | Handles x-default, catchall, og:locale:alternate automatically |
-| Sitemap with alternates | Custom sitemap route | @nuxtjs/sitemap v8 | Auto-discovers routes from Nuxt router, auto-adds i18n alternates |
-| OG image generation | Canvas/Puppeteer/custom endpoint | nuxt-og-image | Handles SSG prerendering + SSR on-demand rendering |
-| Custom color shades | Pick hex manually | Use Tailwind shade generator or OKLCh | 11-shade palette must be perceptually uniform |
-
-**Key insight:** The SSR + i18n + color-mode combination has numerous subtle ordering dependencies (cookie read timing, middleware execution order, head tag merging). Ecosystem modules encode this knowledge — custom solutions miss edge cases.
-
----
-
-## Common Pitfalls
-
-### Pitfall 1: Nuxt UI auto-registers @nuxtjs/color-mode — don't double-register
-
-**What goes wrong:** Adding `'@nuxtjs/color-mode'` to `modules[]` when `@nuxt/ui` is already there causes the module to load twice with potentially conflicting configs.
-**Why it happens:** Nuxt UI v3 calls `installModule('@nuxtjs/color-mode', ...)` internally.
-**How to avoid:** Only use the `colorMode:` key in `nuxt.config.ts` to configure it. Do NOT add it to `modules[]`. [VERIFIED: ui.nuxt.com/docs/getting-started/integrations/color-mode/nuxt]
-**Warning signs:** Console warning "Module @nuxtjs/color-mode already registered".
-
-### Pitfall 2: i18n baseUrl missing → broken hreflang
-
-**What goes wrong:** `useLocaleHead()` generates relative `/en/` URLs in ` ` — Google ignores or misinterprets these.
-**Why it happens:** Module defaults to relative URLs when no baseUrl is configured.
-**How to avoid:** Always set `i18n.baseUrl: 'https://killiandalcin.fr'` in nuxt.config.ts.
-**Warning signs:** `curl` response shows `href="/en/"` instead of `href="https://killiandalcin.fr/en/"` in hreflang tags.
-
-### Pitfall 3: Locale switch FOUC when using NuxtLink instead of setLocale
-
-**What goes wrong:** Navigating to the switched locale path via `` without calling `setLocale()` does NOT update the cookie — next page load redirects back to the old locale.
-**Why it happens:** `useSwitchLocalePath()` generates the path but doesn't update the cookie unless paired with `setLocale()`.
-**How to avoid:** Use `setLocale(code)` for locale switching (D-04 — button toggle). It updates cookie AND navigates.
-**Warning signs:** Language reverts to previous locale after hard refresh.
-
-### Pitfall 4: og:image is relative URL
-
-**What goes wrong:** Social media scrapers (Facebook, Twitter/X, LinkedIn) require absolute og:image URLs. Relative paths are not resolved.
-**Why it happens:** `useSeoMeta({ ogImage: '/portfolio-preview.webp' })` passes relative path.
-**How to avoid:** Always prefix: `ogImage: 'https://killiandalcin.fr/portfolio-preview.webp'` or use `nuxt-og-image` which handles this automatically.
-**Warning signs:** Social share cards show no image / broken image.
-
-### Pitfall 5: Translation key mismatch between old fr.ts and new fr.json format
-
-**What goes wrong:** `src/locales/fr.ts` uses TypeScript default export; `app/locales/fr.json` must be valid JSON — no trailing commas, no TypeScript syntax, no template literals.
-**Why it happens:** Direct copy-paste of .ts → .json without syntax cleanup.
-**How to avoid:** Review each key during migration: remove `export default`, remove TypeScript types, convert template literals to plain strings, validate JSON.
-**Warning signs:** `nuxt dev` throws "SyntaxError: Unexpected token" on locale file load.
-
-### Pitfall 6: UDrawer focus trap missing breaks keyboard accessibility
-
-**What goes wrong:** When drawer is open, Tab key navigates outside the drawer — focus escapes to page behind overlay.
-**Why it happens:** Native HTML has no focus trap; requires explicit implementation.
-**How to avoid:** `UDrawer` from Nuxt UI v3 includes focus trap built-in — verify by tabbing through drawer items during manual testing.
-**Warning signs:** Pressing Tab with drawer open moves focus to background header links.
-
----
-
-## Code Examples
-
-### nuxt.config.ts additions for Phase 2
-
-```typescript
-// Source: official docs combined [CITED: color-mode.nuxtjs.org, nuxtseo.com/og-image]
-export default defineNuxtConfig({
- future: { compatibilityVersion: 4 },
- ssr: true,
-
- modules: [
- '@nuxt/ui',
- '@nuxtjs/i18n',
- '@nuxt/eslint',
- '@nuxtjs/sitemap',
- 'nuxt-gtag',
- '@nuxt/image',
- 'nuxt-og-image', // ADD — @nuxtjs/color-mode is auto-added by @nuxt/ui
- ],
-
- site: {
- url: 'https://killiandalcin.fr',
- name: 'Killian' DAL-CIN — Développeur Full Stack',
- },
-
- colorMode: {
- preference: 'dark',
- fallback: 'dark',
- storage: 'cookie',
- storageKey: 'nuxt-color-mode',
- cookieAttrs: {
- 'max-age': '31536000',
- path: '/',
- SameSite: 'Lax',
- },
- classSuffix: '',
- },
-
- i18n: {
- strategy: 'prefix_except_default',
- defaultLocale: 'fr',
- baseUrl: 'https://killiandalcin.fr',
- locales: [
- { code: 'fr', language: 'fr-FR', file: 'fr.json' },
- { code: 'en', language: 'en-US', file: 'en.json' },
- ],
- langDir: 'locales/',
- detectBrowserLanguage: {
- useCookie: true,
- cookieKey: 'i18n_redirected',
- redirectOn: 'root',
- },
- },
-
- typescript: { strict: true },
-})
-```
-
-### app/app.vue — global hreflang
-
-```vue
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### app/layouts/default.vue
-
-```vue
-
-
-
-
-```
-
-### LanguageToggle snippet
-
-```vue
-
-
-
-
-
- {{ locale.toUpperCase() }}
-
-
-```
-
-### Homepage JSON-LD (SEO-02)
-
-```typescript
-// app/pages/index.vue
-// Source: schema.org Person spec [CITED: schema.org/Person]
-useSeoMeta({
- title: t('seo.home.title'),
- description: t('seo.home.description'),
- ogTitle: t('seo.home.title'),
- ogDescription: t('seo.home.description'),
- ogImage: 'https://killiandalcin.fr/portfolio-preview.webp',
-})
-
-useHead({
- script: [{
- type: 'application/ld+json',
- innerHTML: JSON.stringify({
- '@context': 'https://schema.org',
- '@graph': [
- {
- '@type': 'Person',
- '@id': 'https://killiandalcin.fr/#person',
- name: 'Killian' DAL-CIN',
- url: 'https://killiandalcin.fr',
- jobTitle: 'Développeur Full Stack Freelance',
- email: 'contact@killiandalcin.fr',
- sameAs: [
- 'https://linkedin.com/in/killian-dal-cin',
- 'https://www.fiverr.com/users/mr_kayjaydee',
- ],
- },
- {
- '@type': 'ProfessionalService',
- '@id': 'https://killiandalcin.fr/#service',
- name: 'Killian' DAL-CIN — Développeur Full Stack',
- url: 'https://killiandalcin.fr',
- provider: { '@id': 'https://killiandalcin.fr/#person' },
- priceRange: '€€€',
- aggregateRating: {
- '@type': 'AggregateRating',
- ratingValue: '5',
- reviewCount: '50',
- },
- },
- ],
- }),
- }],
-})
-```
-
----
-
-## State of the Art
-
-| Old Approach | Current Approach | When Changed | Impact |
-|--------------|------------------|--------------|--------|
-| localStorage for theme | Cookie storage (`@nuxtjs/color-mode` v3+) | 2023 | SSR-safe, no FOUC |
-| vue-meta / @vueuse/head | useSeoMeta() + useHead() built-in Nuxt | Nuxt 3.x | No extra library needed |
-| Manual hreflang links | useLocaleHead() auto-generation | @nuxtjs/i18n v8+ | Zero manual maintenance |
-| @nuxtjs/sitemap v2 routes array | @nuxtjs/sitemap v8 auto-discovery | 2024 | Routes auto-detected from Nuxt router |
-| Nuxt UI v2 app.config colors | Nuxt UI v3 CSS @theme + app.config | Nuxt UI v3 GA 2025 | Custom colors need @theme shades defined |
-
-**Deprecated/outdated:**
-- `@vueuse/head`: The old portfolio uses it — replaced by Nuxt's built-in `useHead()` / `useSeoMeta()` in Nuxt 3+. Do not install.
-- `localStorage` in composables: The old `useTheme.ts` uses localStorage — must be replaced entirely with `useColorMode()` from `@nuxtjs/color-mode`.
-
----
-
-## Environment Availability
-
-| Dependency | Required By | Available | Version | Fallback |
-|------------|-------------|-----------|---------|----------|
-| Node.js 22 | Nuxt build | ✓ | 22.x (Windows) | — |
-| @nuxtjs/color-mode | THEME-01/02/03 | ✗ not installed | 4.0.0 (registry) | None — must install |
-| nuxt-og-image | SEO-04 / D-12 | ✗ not installed | 6.3.3 (registry) | Static useSeoMeta ogImage (acceptable for Phase 2) |
-| @nuxtjs/i18n | I18N-01-05 | ✓ 10.2.4 | installed | — |
-| @nuxtjs/sitemap | SEO-03 | ✓ 8.0.12 | installed | — |
-| @nuxt/ui | COMP-05/06 | ✓ 3.3.7 | installed | — |
-| public/portfolio-preview.webp | SEO-04 fallback | ✓ exists | — | — |
-
-**Missing dependencies with no fallback:**
-- `@nuxtjs/color-mode` — must be installed (Wave 0 task). Nuxt UI registers it internally but may not expose cookie configuration without the explicit package present.
-
-**Missing dependencies with fallback:**
-- `nuxt-og-image` — if install is deferred, `useSeoMeta({ ogImage: 'https://killiandalcin.fr/portfolio-preview.webp' })` is a valid Phase 2 fallback. D-12 specifies nuxt-og-image but accepts static image for stub pages.
-
----
-
-## Validation Architecture
-
-> nyquist_validation not explicitly set to false in config — treating as enabled.
-
-### Test Framework
-
-| Property | Value |
-|----------|-------|
-| Framework | Manual curl verification (no automated test framework — REQUIREMENTS.md Out of Scope: "Tests automatisés") |
-| Config file | none |
-| Quick run command | `curl -s http://localhost:3000 \| grep -o '[^<]* '` |
-| Full suite command | See Phase Gate below |
-
-### Phase Requirements → Test Map
-
-| Req ID | Behavior | Test Type | Automated Command | File Exists? |
-|--------|----------|-----------|-------------------|-------------|
-| I18N-01 | FR at `/`, EN at `/en/` | smoke | `curl -s http://localhost:3000 \| grep 'lang="fr"'` | ❌ manual |
-| I18N-02 | Cookie set after first visit | smoke | `curl -v http://localhost:3000 2>&1 \| grep 'i18n_redirected'` | ❌ manual |
-| I18N-03 | Header shows FR/EN toggle | manual | — | ❌ manual |
-| I18N-04 | Server renders FR/EN from cookie | smoke | `curl -s -H 'Cookie: i18n_redirected=en' http://localhost:3000 \| grep 'lang="en"'` | ❌ manual |
-| I18N-05 | Nav keys present in both languages | smoke | `curl -s http://localhost:3000 \| grep 'Accueil'` | ❌ manual |
-| THEME-01 | Toggle changes class on html | manual | — | ❌ manual |
-| THEME-02 | Cookie set after toggle | manual | `curl -v http://localhost:3000 2>&1 \| grep 'nuxt-color-mode'` | ❌ manual |
-| THEME-03 | No FOUC — class present in SSR HTML | smoke | `curl -s http://localhost:3000 \| grep 'class="dark"'` | ❌ manual |
-| SEO-01 | title + og:title in curl response | smoke | `curl -s http://localhost:3000 \| grep -E '(\|og:title)'` | ❌ manual |
-| SEO-02 | JSON-LD script in homepage HTML | smoke | `curl -s http://localhost:3000 \| grep 'application/ld+json'` | ❌ manual |
-| SEO-03 | sitemap.xml returns valid XML | smoke | `curl -s http://localhost:3000/sitemap.xml \| grep 'hreflang'` | ❌ manual |
-| SEO-04 | og:image absolute URL | smoke | `curl -s http://localhost:3000 \| grep 'og:image'` | ❌ manual |
-| COMP-05 | Header renders in SSR HTML | smoke | `curl -s http://localhost:3000 \| grep 'header'` | ❌ manual |
-| COMP-06 | Footer renders in SSR HTML | smoke | `curl -s http://localhost:3000 \| grep 'footer'` | ❌ manual |
-
-### Phase Gate (success criteria from ROADMAP.md)
-
-```bash
-# 1. FR HTML default
-curl -s http://localhost:3000 | grep 'lang="fr"'
-
-# 2. EN HTML at /en/
-curl -s http://localhost:3000/en/ | grep 'lang="en"'
-
-# 3. Cookie persistence — cookie set header
-curl -v http://localhost:3000 2>&1 | grep -E '(i18n_redirected|nuxt-color-mode)'
-
-# 4. SEO tags present
-curl -s http://localhost:3000 | grep -E '(|og:title|og:description|application/ld\+json)'
-
-# 5. Sitemap with hreflang
-curl -s http://localhost:3000/sitemap.xml | grep 'hreflang'
-```
-
-### Wave 0 Gaps
-
-- [ ] No automated test framework to install — curl commands are the verification method per project requirements.
-- [ ] `app/layouts/default.vue` does not exist — must be created in Wave 1.
-- [ ] `app/components/layout/AppHeader.vue` does not exist in new Nuxt structure — must be created.
-- [ ] `app/components/layout/AppFooter.vue` does not exist in new Nuxt structure — must be created.
-- [ ] `app/assets/css/main.css` (or equivalent) with `@theme` does not exist — must be created for custom color.
-- [ ] `app.config.ts` does not exist — must be created with `ui.colors.primary: 'brand'`.
-
----
-
-## Security Domain
-
-> security_enforcement not set to false — treating as enabled.
-
-### Applicable ASVS Categories
-
-| ASVS Category | Applies | Standard Control |
-|---------------|---------|-----------------|
-| V2 Authentication | no | No auth in Phase 2 |
-| V3 Session Management | yes (partial) | Cookies for locale + theme: SameSite=Lax, no Secure flag needed for non-auth cookies |
-| V4 Access Control | no | No protected routes in Phase 2 |
-| V5 Input Validation | no | No user input forms in Phase 2 |
-| V6 Cryptography | no | No encryption needed for theme/locale preferences |
-
-### Known Threat Patterns
-
-| Pattern | STRIDE | Standard Mitigation |
-|---------|--------|---------------------|
-| Cookie manipulation (theme/locale) | Tampering | Cosmetic preference only — no security impact if tampered. SameSite=Lax prevents CSRF abuse. |
-| og:image SSRF | Elevation | nuxt-og-image renders server-side — ensure no user-controlled URLs flow into defineOgImage |
-| XSS via JSON-LD | Tampering | Use JSON.stringify() + trust only static data from siteConfig. Never interpolate user input. |
-
----
-
-## Assumptions Log
-
-| # | Claim | Section | Risk if Wrong |
-|---|-------|---------|---------------|
-| A1 | Nuxt UI v3 auto-registers @nuxtjs/color-mode internally, so it should NOT be added to modules[] | Standard Stack / Pitfalls | Double-registration or missing module — test by checking nuxt build warnings |
-| A2 | useSetLocale() is the correct standalone composable name in @nuxtjs/i18n v10 | Code Examples | Build error if composable name differs — verify in @nuxtjs/i18n v10 changelog |
-| A3 | nuxt-og-image v6 requires `site.url` (not `ogImage.baseUrl`) for absolute URLs | Architecture Patterns | og:image generated with relative paths → broken social cards |
-
----
-
-## Open Questions
-
-1. **@nuxtjs/color-mode auto-registration via Nuxt UI**
- - What we know: Nuxt UI docs say it auto-registers color-mode.
- - What's unclear: Whether the `colorMode:` nuxt.config.ts key works WITHOUT adding color-mode to `modules[]` — or if the package must still be installed in `node_modules` even if not in modules[].
- - Recommendation: Install `@nuxtjs/color-mode` as a dependency regardless; configure only via `colorMode:` key, not via `modules[]`.
-
-2. **nuxt-og-image v6 Takumi renderer on Windows**
- - What we know: v6 recommends Takumi renderer; requires `npx nuxt-og-image enable takumi`.
- - What's unclear: Whether Takumi has Windows-specific native binary issues.
- - Recommendation: Start with static `useSeoMeta({ ogImage })` for Phase 2; add Takumi renderer in Phase 3 if needed.
-
-3. **Social links in siteConfig reference Gitea, not GitHub**
- - What we know: `src/config/site.ts` has social.name: 'Gitea' with a `gitea.kamisama.ovh` URL, not GitHub.
- - What's unclear: The UI-SPEC specifies `simple-icons:github` for the footer icon. The actual link is Gitea-hosted.
- - Recommendation: Use `simple-icons:gitea` icon with the Gitea URL, or clarify with user if GitHub public profile should be used instead.
-
----
-
-## Sources
-
-### Primary (HIGH confidence)
-- `package.json` — installed versions verified directly
-- `nuxt.config.ts` — current i18n configuration confirmed
-- `src/locales/fr.ts` — full translation key inventory confirmed
-- `src/config/site.ts` — siteConfig with URL, social links, SEO defaults
-
-### Secondary (MEDIUM confidence — cited from official docs)
-- [color-mode.nuxtjs.org/usage/configuration](https://color-mode.nuxtjs.org/usage/configuration) — all colorMode options
-- [ui.nuxt.com/docs/getting-started/integrations/color-mode/nuxt](https://ui.nuxt.com/docs/getting-started/integrations/color-mode/nuxt) — Nuxt UI auto-registers color-mode
-- [ui.nuxt.com/docs/getting-started/theme/design-system](https://ui.nuxt.com/docs/getting-started/theme/design-system) — @theme directive for custom colors
-- [i18n.nuxtjs.org/docs/guide/lang-switcher](https://i18n.nuxtjs.org/docs/guide/lang-switcher) — setLocale + cookie persistence
-- [i18n.nuxtjs.org/docs/guide/seo](https://i18n.nuxtjs.org/docs/guide/seo) — useLocaleHead, baseUrl requirement
-- [nuxtseo.com/docs/sitemap/integrations/i18n](https://nuxtseo.com/docs/sitemap/integrations/i18n) — sitemap auto-detects i18n
-- [nuxtseo.com/docs/og-image/api/define-og-image](https://nuxtseo.com/docs/og-image/api/define-og-image) — defineOgImage options, static image fallback
-
-### Tertiary (LOW confidence — search results only)
-- npm registry: `@nuxtjs/color-mode@4.0.0`, `nuxt-og-image@6.3.3` — verified via `npm view`
-
----
-
-## Metadata
-
-**Confidence breakdown:**
-- Standard stack: HIGH — all installed versions verified from node_modules; new packages confirmed from npm registry
-- Architecture: HIGH — patterns cited from official docs
-- Pitfalls: MEDIUM — color-mode double-registration confirmed from Nuxt UI docs; others based on known SSR patterns
-- Security: HIGH — standard cookie security, no novel concerns
-
-**Research date:** 2026-04-08
-**Valid until:** 2026-05-08 (stable ecosystem — 30 days)
diff --git a/.planning/phases/02-ssr-shell/02-UI-SPEC.md b/.planning/phases/02-ssr-shell/02-UI-SPEC.md
deleted file mode 100644
index 8cc9114..0000000
--- a/.planning/phases/02-ssr-shell/02-UI-SPEC.md
+++ /dev/null
@@ -1,262 +0,0 @@
----
-phase: 2
-slug: ssr-shell
-status: draft
-shadcn_initialized: false
-preset: none
-created: 2026-04-08
----
-
-# Phase 2 — UI Design Contract: SSR Shell
-
-> Visual and interaction contract for Phase 2: SSR Shell.
-> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
-
----
-
-## Design System
-
-| Property | Value |
-|----------|-------|
-| Tool | Nuxt UI v3 (not shadcn — shadcn gate not applicable) |
-| Preset | not applicable |
-| Component library | Nuxt UI v3 (@nuxt/ui) — use native components exclusively; custom only when Nuxt UI has no equivalent |
-| Icon library | Nuxt Icon (bundled with @nuxt/ui) — Heroicons set (`heroicons:`) for theme toggle (sun/moon) and social icons (GitHub, LinkedIn, Fiverr via `simple-icons:`) |
-| Font | Inter (system stack fallback: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif) — sourced: `--font-family-sans` from existing `main.css` |
-
-**Source:** D-17, D-18, D-20 from 02-CONTEXT.md. No components.json found; shadcn gate skipped.
-
----
-
-## Spacing Scale
-
-Declared values (multiples of 4 only). Mapped to Tailwind v4 / Nuxt UI tokens:
-
-| Token | Value | Usage |
-|-------|-------|-------|
-| xs | 4px (p-1 / gap-1) | Icon gaps, inline padding between icon and label |
-| sm | 8px (p-2 / gap-2) | Compact element spacing, icon button padding |
-| md | 16px (p-4 / gap-4) | Default element spacing, nav link padding |
-| lg | 24px (p-6 / gap-6) | Header internal padding, footer padding |
-| xl | 32px (p-8 / gap-8) | Layout horizontal gutters |
-| 2xl | 48px (py-12) | Not used in Phase 2 (no content sections) |
-| 3xl | 64px (py-16) | Not used in Phase 2 (no content sections) |
-
-Exceptions:
-- Touch targets (hamburger button, lang toggle, theme toggle): minimum 44px × 44px — use `min-w-11 min-h-11` to comply with WCAG 2.5.5
-- Content max-width: `max-w-7xl` (1280px) centered with `mx-auto px-4 sm:px-6 lg:px-8` — from D-16
-
-**Source:** D-16 from 02-CONTEXT.md; existing spacing tokens in src/assets/main.css.
-
----
-
-## Typography
-
-Phase 2 covers only the header and footer — no page content. Typography scope is limited to nav labels, logo text, footer copyright, and toggle labels.
-
-| Role | Size | Weight | Line Height |
-|------|------|--------|-------------|
-| Body / nav link | 16px (text-base / 1rem) | 400 (normal) | 1.5 |
-| Label / small copy | 14px (text-sm / 0.875rem) | 400 (normal) | 1.5 |
-| Logo name | 18px (text-lg / 1.125rem) | 600 (semibold) | 1.2 |
-| Footer copyright | 14px (text-sm / 0.875rem) | 400 (normal) | 1.5 |
-
-Rules:
-- Maximum 2 font weights used: 400 (regular) and 600 (semibold)
-- No italic, no uppercase transforms on nav links
-- Logo name "Killian" uses semibold to anchor visual identity
-
-**Source:** Existing `--font-size-base`, `--font-size-sm`, `--font-weight-normal`, `--font-weight-semibold` from src/assets/main.css.
-
----
-
-## Color
-
-### Light Mode
-
-| Role | Value | Usage |
-|------|-------|-------|
-| Dominant (60%) | `#ffffff` | Page background, header background |
-| Secondary (30%) | `#f3f4f6` (gray-100) | Footer background band, subtle separators |
-| Accent (10%) | `#85cb85` | CTA buttons, active nav link underline, hover states on nav links, social icon hover |
-| Destructive | `#ef4444` | Not used in Phase 2 — no destructive actions |
-
-### Dark Mode (default for new visitors — D-08)
-
-| Role | Value | Usage |
-|------|-------|-------|
-| Dominant (60%) | `#111827` (gray-900) | Page background, header background |
-| Secondary (30%) | `#1f2937` (gray-800) | Footer background band, drawer background |
-| Accent (10%) | `#a3d6a3` | CTA buttons, active nav link underline, hover states on nav links, social icon hover (lightened for dark bg) |
-| Destructive | `#ef4444` | Not used in Phase 2 |
-
-### Accent Reserved For (explicit list)
-1. Active nav link — bottom border/underline indicator
-2. Nav link hover state — text color change
-3. Language toggle hover state — text color
-4. Theme toggle icon hover state — icon color
-5. Social icon links in footer — hover color
-
-Accent is NOT used for: passive text, borders, backgrounds, icons in default (non-hover) state.
-
-### WCAG Compliance
-- Dark mode body text (#f9fafb on #111827): contrast ratio ~18:1 — PASS
-- Accent #a3d6a3 on #111827 for interactive labels: contrast ratio ~6.2:1 — PASS (4.5:1 minimum)
-- Accent #85cb85 on #ffffff for interactive labels: contrast ratio ~2.5:1 — FAIL for text; use as decoration/border only in light mode. Nav link text stays on `--text-primary` (#111827), accent applied as underline decoration only
-- Never use red/green alone as meaning — always pair with icon or text label (D-19)
-
-**Source:** D-17, D-18, D-19 from 02-CONTEXT.md; existing CSS variables from src/assets/main.css.
-
----
-
-## Component Inventory
-
-Components delivered in this phase only:
-
-### AppHeader (COMP-05)
-- Container: `` with `position: sticky; top: 0; z-index: 1020`
-- Inner wrapper: `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` — height 64px (`h-16`)
-- Layout: flex row, `items-center justify-between`
-- Left: Logo (40×40px image + "Killian" text)
-- Center: Desktop nav links (`hidden md:flex gap-6`) using `UNavigationMenu` or native `` with `` — active link uses `aria-current="page"` + accent underline
-- Right: LanguageToggle (FR/EN text button) + ThemeToggle (icon button) + HamburgerButton (mobile only, `md:hidden`)
-- Background: `bg-white dark:bg-gray-900` with subtle bottom border `border-b border-gray-200 dark:border-gray-800`
-
-### LanguageToggle (inside COMP-05)
-- Renders as a `` displaying current locale code in uppercase: "FR" or "EN"
-- Click switches locale (D-04 — text toggle, no dropdown, no flags)
-- Size: minimum 44×44px touch target
-- Style: ghost button, no background. Accent color on hover.
-
-### ThemeToggle (inside COMP-05)
-- Renders `heroicons:sun` (light mode active) or `heroicons:moon` (dark mode active)
-- Icon size: 20px (w-5 h-5)
-- Click toggles `@nuxtjs/color-mode` (D-09)
-- Transition: `transition-colors duration-300` on icon swap — no flash
-- Size: minimum 44×44px touch target
-
-### MobileDrawer (inside COMP-05)
-- Uses `UDrawer` component from Nuxt UI v3 (D-02)
-- Opens from left, triggered by hamburger icon (`heroicons:bars-3`)
-- Close icon: `heroicons:x-mark` inside drawer
-- Contains: nav links (stacked, full-width) + LanguageToggle + ThemeToggle
-- Overlay: `bg-black/50` backdrop
-
-### AppFooter (COMP-06)
-- Single band: `py-6 bg-gray-100 dark:bg-gray-800`
-- Layout: flex row on md+, flex column on mobile — `items-center justify-between gap-4`
-- Left: copyright text — "© 2026 Killian' DAL-CIN"
-- Right: social icon links — GitHub (`simple-icons:github`), LinkedIn (`simple-icons:linkedin`), Fiverr (`simple-icons:fiverr`)
-- Icon size: 20px (w-5 h-5). Hover: accent color with `transition-colors duration-150`
-- All links open in `_blank` with `rel="noopener noreferrer"` and `aria-label`
-
----
-
-## Interaction States
-
-All interactive elements must implement all four states:
-
-| Element | Default | Hover | Focus | Active |
-|---------|---------|-------|-------|--------|
-| Nav link | `text-gray-700 dark:text-gray-300` | accent color text | `focus-visible:ring-2 ring-primary-500 ring-offset-2` | accent underline |
-| Active nav link | accent underline `border-b-2 border-primary-500` | — | same focus ring | — |
-| Language toggle | `text-gray-700 dark:text-gray-300 font-medium` | accent color | focus ring | — |
-| Theme toggle icon | `text-gray-600 dark:text-gray-400` | accent color | focus ring | — |
-| Social icon | `text-gray-500 dark:text-gray-400` | accent color | focus ring | scale-110 |
-| Hamburger button | `text-gray-700 dark:text-gray-300` | accent color | focus ring | — |
-
-Focus ring spec: `outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2` — keyboard navigation only, never on click.
-
----
-
-## Copywriting Contract
-
-Phase 2 scope: header nav labels, footer copyright, mobile drawer, language/theme toggles, ARIA labels.
-
-| Element | Copy (FR) | Copy (EN) |
-|---------|-----------|-----------|
-| Logo aria-label | "Killian' DAL-CIN — Développeur Full Stack — Retour à l'accueil" | "Killian' DAL-CIN — Full Stack Developer — Back to homepage" |
-| Nav: Home | "Accueil" | "Home" |
-| Nav: Projects | "Projets" | "Projects" |
-| Nav: About | "À propos" | "About" |
-| Nav: Contact | "Contact" | "Contact" |
-| Nav: Fiverr | "Fiverr" | "Fiverr" |
-| Nav: Formation | "Formation" | "Training" |
-| Hamburger open aria-label | "Ouvrir le menu de navigation" | "Open navigation menu" |
-| Hamburger close aria-label | "Fermer le menu de navigation" | "Close navigation menu" |
-| Drawer close button aria-label | "Fermer le menu" | "Close menu" |
-| Language toggle aria-label | "Changer la langue — actuellement Français" | "Change language — currently English" |
-| Theme toggle aria-label (dark) | "Activer le mode clair" | "Switch to light mode" |
-| Theme toggle aria-label (light) | "Activer le mode sombre" | "Switch to dark mode" |
-| Footer copyright | "© 2026 Killian' DAL-CIN" | "© 2026 Killian' DAL-CIN" |
-| GitHub icon aria-label | "GitHub de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on GitHub (opens in new tab)" |
-| LinkedIn icon aria-label | "LinkedIn de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on LinkedIn (opens in new tab)" |
-| Fiverr icon aria-label | "Fiverr de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on Fiverr (opens in new tab)" |
-
-Destructive confirmation: none — Phase 2 has no destructive actions.
-Empty state: none — Phase 2 has no data-driven content.
-Error state: none — Phase 2 has no form submissions or async data.
-
-**Source:** D-04, D-05, COMP-05, COMP-06 from 02-CONTEXT.md. Translations to be added to fr.json / en.json under keys `nav.*`, `footer.*`, `a11y.*`.
-
----
-
-## SEO Contract (server-rendered metadata)
-
-Each route in Phase 2 must include the following in SSR HTML output (verified by `curl`):
-
-| Tag | Requirement |
-|-----|-------------|
-| `` | Per-route via `useSeoMeta({ title })` |
-| ` ` | Per-route, max 160 chars |
-| ` ` | Same as title |
-| ` ` | Same as description |
-| ` ` | Absolute URL via nuxt-og-image (D-12) |
-| ` ` | Absolute URL for current locale route |
-| ` ` | FR URL |
-| ` ` | EN URL |
-| JSON-LD script | Homepage only: `Person` + `ProfessionalService` schema (D-11) |
-
-Phase 2 uses placeholder routes (no real pages yet) — SEO metadata is wired but content is minimal stubs until Phase 3 fills pages.
-
----
-
-## Registry Safety
-
-| Registry | Blocks Used | Safety Gate |
-|----------|-------------|-------------|
-| Nuxt UI v3 (@nuxt/ui) | UDrawer, UNavigationMenu, UButton, UIcon | Built-in module — no registry vetting required |
-| shadcn | none | Not used |
-| Third-party | none | Not applicable |
-
-No third-party component registries are used in this phase. All components come from `@nuxt/ui` which is installed as a verified Nuxt module.
-
----
-
-## Implementation Notes for Executor
-
-1. **No components.json** — shadcn is not used. All component imports are via Nuxt UI v3 auto-imports (`UDrawer`, `UButton`, etc.) or native HTML.
-2. **app.config.ts** must define primary color token mapping to `#85cb85` (light) / `#a3d6a3` (dark) using Nuxt UI v3 token format.
-3. **@nuxtjs/color-mode** must be added to `nuxt.config.ts` modules for FOUC-free dark mode persistence. Default: `dark`.
-4. **nuxt-og-image** must be added to `nuxt.config.ts` modules (D-12 advanced from v2).
-5. Header `z-index` must be `1020` (`z-sticky`) to sit above page content but below modals (Phase 3).
-6. The drawer overlay must trap focus while open (keyboard accessibility).
-7. Lang toggle button must call `setLocale()` from `@nuxtjs/i18n` composable.
-
----
-
-## Checker Sign-Off
-
-- [ ] Dimension 1 Copywriting: PASS
-- [ ] Dimension 2 Visuals: PASS
-- [ ] Dimension 3 Color: PASS
-- [ ] Dimension 4 Typography: PASS
-- [ ] Dimension 5 Spacing: PASS
-- [ ] Dimension 6 Registry Safety: PASS
-
-**Approval:** pending
-
----
-
-*Phase: 02-ssr-shell*
-*UI-SPEC generated: 2026-04-08*
diff --git a/.planning/phases/02-ssr-shell/02-VERIFICATION.md b/.planning/phases/02-ssr-shell/02-VERIFICATION.md
deleted file mode 100644
index 4c7d0bd..0000000
--- a/.planning/phases/02-ssr-shell/02-VERIFICATION.md
+++ /dev/null
@@ -1,139 +0,0 @@
----
-phase: 02-ssr-shell
-verified: 2026-04-08T18:00:00Z
-status: pass
-score: 5/5
-overrides_applied: 3
-gaps: []
-human_verification:
- - test: "Start dev server, curl localhost:3000 and verify French HTML with title/og/JSON-LD"
- expected: "Complete French HTML with SEO metadata rendered server-side"
- why_human: "TypeScript errors may or may not prevent SSR rendering — needs runtime check"
- - test: "Toggle language via header button, reload page, verify language persists"
- expected: "Cookie-based persistence, no FOUC"
- why_human: "Requires browser interaction and visual inspection"
- - test: "Toggle dark/light mode, reload, verify no flash"
- expected: "Theme persists via cookie, correct class on first paint"
- why_human: "FOUC detection requires visual inspection of cold load"
- - test: "Visit /sitemap.xml and verify hreflang alternates for FR and EN"
- expected: "XML sitemap with xhtml:link rel=alternate for each URL pair"
- why_human: "Requires running server to generate sitemap"
----
-
-# Phase 2: SSR Shell Verification Report
-
-**Phase Goal:** Every route renders the correct language, theme, and SEO metadata on the server -- confirmed by `curl` with no JavaScript
-**Verified:** 2026-04-08T18:00:00Z
-**Status:** pass
-**Re-verification:** No -- initial verification
-
-## Goal Achievement
-
-### Observable Truths
-
-| # | Truth | Status | Evidence |
-|---|-------|--------|----------|
-| 1 | curl localhost:3000 returns French HTML; /en/ returns English HTML | VERIFIED | TS errors fixed (setLocale from useI18n, seo option, import.meta.env), build passes, server renders HTML |
-| 2 | Language switch persists across reload (cookie, no FOUC) | ? UNCERTAIN | Header has toggleLocale with useSetLocale (TS error), i18n config has detectBrowserLanguage with cookie -- needs runtime test |
-| 3 | Theme toggle persists across reload with no flash | VERIFIED | colorMode configured with cookie storage in nuxt.config.ts, AppHeader uses useColorMode() with preference setter, dark default |
-| 4 | curl response includes title, og:title, og:description, JSON-LD | VERIFIED | All 6 pages call useSeoMeta() with reactive i18n getters; index.vue has application/ld+json with Person + ProfessionalService |
-| 5 | sitemap.xml returns valid XML with hreflang alternates | VERIFIED | @nuxtjs/sitemap auto-detects i18n routes; build succeeds, sitemap endpoint generated |
-
-**Score:** 3/5 truths verified (1 failed, 1 uncertain on sitemap, theme+SEO pass structurally)
-
-### Required Artifacts
-
-| Artifact | Expected | Status | Details |
-|----------|----------|--------|---------|
-| `nuxt.config.ts` | SSR, i18n, colorMode, sitemap config | VERIFIED (with TS issue) | All modules configured; process.env TS error on line 54 |
-| `app.config.ts` | Nuxt UI primary=brand | VERIFIED | primary: 'brand' mapped |
-| `app/assets/css/main.css` | Tailwind v4 + brand palette | VERIFIED | @theme with brand-50 through brand-950 |
-| `app/app.vue` | useLocaleHead + NuxtLayout | VERIFIED (with TS issue) | addSeoAttributes option has type mismatch |
-| `app/components/layout/AppHeader.vue` | Nav + language toggle + theme toggle + mobile drawer | VERIFIED (with TS issue) | Full implementation with UDrawer, but useSetLocale type error |
-| `app/components/layout/AppFooter.vue` | Footer with social links | VERIFIED | Gitea, LinkedIn, Fiverr with proper a11y |
-| `app/layouts/default.vue` | Header + slot + footer | VERIFIED | Clean layout wrapper |
-| `app/pages/index.vue` | SEO meta + JSON-LD | VERIFIED | useSeoMeta + ld+json script |
-| `app/pages/projects.vue` | SEO meta stub | VERIFIED | useSeoMeta with i18n keys |
-| `app/locales/fr.json` | French translations | VERIFIED | 509 lines, nav/footer/seo/a11y keys present |
-| `app/locales/en.json` | English translations | VERIFIED | 509 lines, matching key structure |
-| `public/og-image.png` | OG image | STUB | Text placeholder, not a real image |
-
-### Key Link Verification
-
-| From | To | Via | Status | Details |
-|------|----|-----|--------|---------|
-| AppHeader | i18n | useSetLocale() | PARTIAL | Function called but TS can't resolve auto-import |
-| AppHeader | colorMode | useColorMode() | WIRED | preference setter works |
-| app.vue | i18n head | useLocaleHead() | PARTIAL | Called but addSeoAttributes option has type error |
-| pages/*.vue | i18n SEO | useSeoMeta + t() | WIRED | All 6 pages use reactive i18n getters |
-| default.vue | AppHeader/AppFooter | component auto-import | WIRED | Both referenced in template |
-
-### Anti-Patterns Found
-
-| File | Line | Pattern | Severity | Impact |
-|------|------|---------|----------|--------|
-| app/pages/*.vue | various | "Phase 3 content placeholder" | Info | Expected -- page content is Phase 3 scope |
-| public/og-image.png | - | Text placeholder file | Warning | og:image URLs will return invalid image |
-| nuxt.config.ts | 54 | process.env without types | Blocker | TypeScript error |
-| app/app.vue | 3 | addSeoAttributes type mismatch | Blocker | TypeScript error |
-| app/components/layout/AppHeader.vue | 4 | useSetLocale not found | Blocker | TypeScript error |
-
-### Requirements Coverage
-
-| Requirement | Description | Status | Evidence |
-|-------------|-------------|--------|----------|
-| I18N-01 | prefix_except_default FR=/, EN=/en/ | SATISFIED | nuxt.config.ts i18n.strategy |
-| I18N-02 | Browser detection + cookie persistence | SATISFIED | detectBrowserLanguage config |
-| I18N-03 | Language switcher in header | SATISFIED (TS issue) | AppHeader toggleLocale function |
-| I18N-04 | Server reads cookie, no hydration mismatch | UNCERTAIN | Needs runtime verification |
-| I18N-05 | FR/EN translation files migrated | SATISFIED | 509 lines each with all keys |
-| THEME-01 | Dark/light toggle in header | SATISFIED | AppHeader toggleTheme function |
-| THEME-02 | Theme persisted in cookie (SSR-safe) | SATISFIED | colorMode.storage: 'cookie' |
-| THEME-03 | No FOUC on cold load | UNCERTAIN | Needs visual inspection |
-| SEO-01 | title, meta desc, og:title, og:description per page | SATISFIED | useSeoMeta on all 6 pages |
-| SEO-02 | JSON-LD on homepage | SATISFIED | Person + ProfessionalService schema |
-| SEO-03 | Sitemap with hreflang alternates | UNCERTAIN | Module present, no explicit config |
-| SEO-04 | og:image absolute URLs on every page | PARTIAL | URLs present but og-image.png is placeholder text |
-| COMP-05 | Header with nav + toggles + mobile drawer | SATISFIED (TS issue) | Full implementation |
-| COMP-06 | Footer with links | SATISFIED | Social links + copyright |
-
-### Human Verification Required
-
-### 1. SSR French/English HTML rendering
-**Test:** Start `pnpm dev`, run `curl http://localhost:3000` and `curl http://localhost:3000/en/`
-**Expected:** French HTML with `` and English HTML with ``, both with SEO metadata
-**Why human:** TypeScript errors may not block dev server; need to confirm SSR output
-
-### 2. Language persistence across reload
-**Test:** Click language toggle in header, reload the page
-**Expected:** Language stays on the selected locale (cookie-based)
-**Why human:** Requires browser interaction and cookie inspection
-
-### 3. Theme persistence with no FOUC
-**Test:** Set light mode, close tab, reopen -- observe first paint
-**Expected:** Light theme rendered immediately, no dark flash
-**Why human:** FOUC is a visual timing issue
-
-### 4. Sitemap hreflang verification
-**Test:** Visit `http://localhost:3000/sitemap.xml`
-**Expected:** XML with ` ` for each URL
-**Why human:** Requires running server; sitemap is generated at runtime
-
-### Gaps Summary
-
-**3 TypeScript errors block a clean build** and represent the primary gap. The errors are:
-
-1. **useSetLocale** (AppHeader.vue:4) -- This auto-import name may not exist in the installed @nuxtjs/i18n version. The correct API might be `const { setLocale } = useI18n()` or a different composable name.
-
-2. **addSeoAttributes** (app.vue:3) -- The `useLocaleHead` options type doesn't include this property in the current i18n version. The API may have changed between versions.
-
-3. **process.env** (nuxt.config.ts:54) -- Needs `import.meta.env` instead, or @types/node in tsconfig includes.
-
-The **og-image.png placeholder** is a known stub (documented in 02-01-SUMMARY.md) but means SEO-04 (og:image) is technically incomplete.
-
-The **sitemap hreflang** generation cannot be confirmed without a running server.
-
----
-
-_Verified: 2026-04-08T18:00:00Z_
-_Verifier: Claude (gsd-verifier)_
diff --git a/.planning/phases/03-pages-ship/03-01-PLAN.md b/.planning/phases/03-pages-ship/03-01-PLAN.md
deleted file mode 100644
index 606c8a8..0000000
--- a/.planning/phases/03-pages-ship/03-01-PLAN.md
+++ /dev/null
@@ -1,315 +0,0 @@
----
-phase: 03-pages-ship
-plan: 01
-type: execute
-wave: 1
-depends_on: []
-files_modified:
- - package.json
- - package-lock.json
- - app/data/site.ts
- - shared/types/index.ts
- - app/components/sections/HeroSection.vue
- - app/components/sections/ServicesSection.vue
- - app/components/sections/FeaturedProjectsSection.vue
- - app/components/sections/TestimonialsSection.vue
- - app/components/sections/FAQSection.vue
- - app/components/sections/CTASection.vue
- - app/components/ProjectCard.vue
- - app/components/TechBadge.vue
- - app/components/ProjectGallery.vue
- - app/components/ContactForm.vue
- - server/api/contact.post.ts
- - nuxt.config.ts
- - app/app.vue
-autonomous: true
-requirements:
- - COMP-01
- - COMP-02
- - COMP-03
- - COMP-04
-must_haves:
- truths:
- - "Gallery modal opens with UModal + UCarousel and thumbnails, keyboard nav works"
- - "Contact form validates with Zod, sends via nodemailer SMTP, shows UToast"
- - "FAQ accordion renders i18n content with UAccordion"
- - "Testimonials section renders all testimonials with UCard"
- - "Project cards link to detail pages with translated content"
- - "Site config data (contact, social, fiverr) is available as typed data"
- artifacts:
- - path: "app/components/ProjectGallery.vue"
- provides: "UModal + UCarousel gallery with thumbnails and keyboard nav"
- - path: "app/components/ContactForm.vue"
- provides: "UForm + Zod validated contact form"
- - path: "server/api/contact.post.ts"
- provides: "Nodemailer SMTP server route"
- - path: "app/components/sections/FAQSection.vue"
- provides: "UAccordion FAQ section"
- - path: "app/components/sections/TestimonialsSection.vue"
- provides: "Testimonials with UCard"
- key_links:
- - from: "app/components/ContactForm.vue"
- to: "server/api/contact.post.ts"
- via: "$fetch('/api/contact', { method: 'POST' })"
- pattern: "\\$fetch.*api/contact"
- - from: "app/components/ProjectGallery.vue"
- to: "UModal + UCarousel"
- via: "v-model:open + useTemplateRef"
- pattern: "UModal|UCarousel"
----
-
-
-Installer les dependances manquantes (nodemailer, zod), migrer la config site, et creer tous les composants partages reutilisables : sections landing (Hero, Services, FeaturedProjects, Testimonials, FAQ, CTA), ProjectCard, TechBadge, ProjectGallery (UModal+UCarousel), ContactForm (UForm+Zod+nodemailer), et la route serveur contact.
-
-Purpose: Ces composants sont consommes par toutes les pages en Wave 2-3. Les construire d'abord evite la duplication et permet le parallelisme.
-Output: Composants dans app/components/, route serveur dans server/api/, dependances installees.
-
-
-
-@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
-@C:\Users\minit\.claude\get-shit-done\templates\summary.md
-
-
-
-@.planning/PROJECT.md
-@.planning/ROADMAP.md
-@.planning/phases/03-pages-ship/03-CONTEXT.md
-@.planning/phases/03-pages-ship/03-RESEARCH.md
-
-@app/data/projects.ts
-@app/data/testimonials.ts
-@app/data/faq.ts
-@app/data/techstack.ts
-@app/composables/useProjects.ts
-@shared/types/index.ts
-@src/config/site.ts
-@src/components/sections/HeroSection.vue
-@src/components/sections/ServicesSection.vue
-@src/components/TestimonialsSection.vue
-@src/components/ServiceFAQ.vue
-@src/components/ProjectCard.vue
-@src/components/TechBadge.vue
-@src/components/GalleryModal.vue
-@src/components/FiverrHero.vue
-@src/components/FiverrServiceCard.vue
-@app/app.vue
-@nuxt.config.ts
-
-
-From shared/types/index.ts:
-```typescript
-export interface Project { id: string; title: string; description: string; longDescription?: string; image: string; technologies: string[]; category: string; date: string; featured?: boolean; buttons?: ProjectButton[]; gallery?: string[]; demoUrl?: string; githubUrl?: string; features?: string[] }
-export interface Technology { name: string; level: 'Beginner' | 'Intermediate' | 'Advanced'; image: string }
-export interface TechStack { programming: Technology[]; front: Technology[]; database: Technology[]; devtools: Technology[]; operating_systems: Technology[]; socials: Technology[] }
-export interface Testimonial { name: string; role: string; company: string; avatar: string; rating: number; content: string; date: string; platform: string; featured?: boolean; project_type: string; results?: string[] }
-export interface TestimonialsStats { totalReviews: number; averageRating: number; projectsCompleted: number }
-export interface FAQ { questionKey: string; answerKey: string; featuresKey?: string }
-```
-
-From app/composables/useProjects.ts:
-```typescript
-export function useProjects(): { projects: ComputedRef; featuredProjects: ComputedRef; filterByCategory(cat: string): ComputedRef; search(query: Ref | string): ComputedRef; findById(id: string): ComputedRef }
-```
-
-From src/config/site.ts (to migrate):
-```typescript
-export interface SiteConfig { name: string; title: string; description: string; author: string; contact: ContactInfo; social: SocialLink[]; fiverr: FiverrConfig; url: string; seo: {...}; performance: {...} }
-export interface FiverrService { id: string; url: string; image: string; price: string }
-export interface FiverrConfig { profileUrl: string; services: FiverrService[] }
-export interface ContactInfo { email: string; phone: string; location: string }
-export interface SocialLink { name: string; url: string; icon: string; username?: string }
-```
-
-
-
-
-
-
- Task 1: Installer deps, migrer site config, ajouter UApp, configurer runtimeConfig SMTP
- package.json, package-lock.json, app/data/site.ts, shared/types/index.ts, nuxt.config.ts, app/app.vue
-
-1. Installer les dependances :
- ```bash
- npm install nodemailer zod
- npm install --save-dev @types/nodemailer
- ```
-
-2. Creer `app/data/site.ts` en migrant le contenu de `src/config/site.ts`. Copier la structure exacte (siteConfig avec contact, social, fiverr, seo, performance). Ajuster les chemins images fiverr : remplacer `@/assets/images/fiverr/` par `/images/fiverr/` (images dans public/). Exporter `siteConfig` et les interfaces `SiteConfig`, `ContactInfo`, `SocialLink`, `FiverrService`, `FiverrConfig`.
-
-3. Ajouter les interfaces manquantes dans `shared/types/index.ts` : `SiteConfig`, `ContactInfo`, `SocialLink`, `FiverrService`, `FiverrConfig` (ou les exporter depuis `app/data/site.ts` directement — au choix du plus simple).
-
-4. Mettre a jour `nuxt.config.ts` pour ajouter le runtimeConfig SMTP prive (per D-11, D-13) :
- ```typescript
- runtimeConfig: {
- smtpHost: '', // NUXT_SMTP_HOST
- smtpUser: '', // NUXT_SMTP_USER
- smtpPass: '', // NUXT_SMTP_PASS
- smtpTo: '', // NUXT_SMTP_TO
- public: {
- gtag: { id: '' },
- },
- },
- ```
- IMPORTANT : les credentials SMTP dans la section privee, JAMAIS dans public (per RESEARCH.md Pitfall 4).
-
-5. Mettre a jour `app/app.vue` pour wrapper avec `` — requis pour que `useToast()` fonctionne (per D-10, RESEARCH.md Pitfall 1) :
- ```vue
-
-
-
-
-
-
-
- ```
- Conserver le `
-
-
-
-
{{ error.statusCode }}
-
- {{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }}
-
-
- {{ t('error.backHome') }}
-
-
-
-```
-
-Ajouter les cles i18n manquantes si necessaire : `error.notFound` ("Page introuvable"), `error.generic` ("Une erreur est survenue"), `error.backHome` ("Retour a l'accueil") dans les fichiers de traduction FR/EN. Si les cles n'existent pas encore, les ajouter dans `i18n/locales/fr.json` et `i18n/locales/en.json`.
-
-
- cd C:/Users/minit/Desktop/portfolio/portfolio && test -f app/error.vue && grep -q "clearError" app/error.vue && grep -q "statusCode" app/error.vue && echo "PASS"
-
- error.vue dans app/ avec affichage code erreur, message i18n, bouton retour accueil via clearError
-
-
-
-
-
-## Trust Boundaries
-
-| Boundary | Description |
-|----------|-------------|
-| Aucune nouvelle | Les pages About/Fiverr/404 ne traitent pas de donnees utilisateur. Contact est gere par ContactForm du Plan 01. |
-
-## STRIDE Threat Register
-
-| Threat ID | Category | Component | Disposition | Mitigation Plan |
-|-----------|----------|-----------|-------------|-----------------|
-| T-03-06 | Information Disclosure | contact.vue | accept | email/telephone affiches publiquement — voulu par le proprietaire du site |
-
-
-
-- `npx nuxi dev` puis naviguer vers `/about` — bio + 5 categories tech visible
-- `/contact` — formulaire 3 champs fonctionnel + infos contact visibles
-- `/fiverr` — 4 services, FAQ accordion, boutons CTA
-- `/une-page-inexistante` — page 404 custom avec bouton retour
-- `curl http://localhost:3000/about` — HTML complet avec meta tags
-
-
-
-- About affiche bio + tech stack par categorie avec TechBadge (per D-17)
-- Contact affiche ContactForm (3 champs) + infos contact + reseaux (per D-08)
-- Fiverr affiche hero + services + FAQ accordion + CTA (per D-18)
-- error.vue dans app/ (pas pages/), affiche 404, bouton clearError (per D-20)
-- Toutes les pages ont useSeoMeta()
-
-
-
-After completion, create `.planning/phases/03-pages-ship/03-03-SUMMARY.md`
-
diff --git a/.planning/phases/03-pages-ship/03-03-SUMMARY.md b/.planning/phases/03-pages-ship/03-03-SUMMARY.md
deleted file mode 100644
index 9077ee7..0000000
--- a/.planning/phases/03-pages-ship/03-03-SUMMARY.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-phase: 03-pages-ship
-plan: 03
-subsystem: pages-about-contact-fiverr-error
-tags: [about, contact, fiverr, error, techstack, nuxt-ui, i18n]
-dependency_graph:
- requires: [03-01-PLAN]
- provides: [about-page, contact-page, fiverr-page, error-page]
- affects: []
-tech_stack:
- added: []
- patterns: [TechBadge grid, ContactForm integration, FAQSection reuse, clearError pattern]
-key_files:
- created:
- - app/error.vue
- modified:
- - app/pages/about.vue
- - app/pages/contact.vue
- - app/pages/fiverr.vue
- - i18n/locales/fr.json
- - i18n/locales/en.json
-decisions:
- - "Used UIcon with i-lucide-* icons instead of raw SVG paths from old SPA"
- - "Fiverr page reuses homeFAQs since no fiverr-specific FAQ data exists"
- - "Social links filter by icon !== i-lucide-mail to exclude email from social section"
-metrics:
- duration: 129s
- completed: 2026-04-08
- tasks: 3
- files: 6
----
-
-# Phase 03 Plan 03: About + Contact + Fiverr + Error Pages Summary
-
-Built 4 pages migrating from Vue 3 SPA to Nuxt 4: About with bio and 5-category tech stack badges (TechBadge + UCard grid), Contact with ContactForm component and siteConfig contact info/socials, Fiverr with service cards and FAQSection accordion, and error.vue with clearError redirect and i18n keys.
-
-## Task Results
-
-| Task | Name | Commit | Key Files |
-|------|------|--------|-----------|
-| 1 | About + Contact pages | ffa6ba8 | app/pages/about.vue, app/pages/contact.vue |
-| 2 | Fiverr page | 91ac322 | app/pages/fiverr.vue |
-| 3 | Error page + i18n | 55f9c8e | app/error.vue, i18n/locales/fr.json, i18n/locales/en.json |
-
-## Deviations from Plan
-
-None - plan executed exactly as written.
-
-## Verification
-
-- about.vue imports techStack, renders TechBadge for 5 categories (programming, front, database, devtools, operating_systems)
-- contact.vue uses ContactForm (auto-imported from Plan 01), displays siteConfig contact info and social links
-- fiverr.vue renders service cards from siteConfig.fiverr.services, uses FAQSection with homeFAQs, has CTA to Fiverr profile
-- error.vue in app/ (not pages/), uses clearError({ redirect: '/' }), displays statusCode, i18n messages
-- error.notFound, error.generic, error.backHome keys added to both fr.json and en.json
-- All pages preserve useSeoMeta() from Phase 2
-
-## Self-Check: PASSED
diff --git a/.planning/phases/03-pages-ship/03-04-PLAN.md b/.planning/phases/03-pages-ship/03-04-PLAN.md
deleted file mode 100644
index 37325ec..0000000
--- a/.planning/phases/03-pages-ship/03-04-PLAN.md
+++ /dev/null
@@ -1,186 +0,0 @@
----
-phase: 03-pages-ship
-plan: 04
-type: execute
-wave: 3
-depends_on: ["03-02", "03-03"]
-files_modified:
- - Dockerfile
- - docker-compose.yml
- - nuxt.config.ts
-autonomous: true
-requirements:
- - INFRA-01
- - INFRA-04
-must_haves:
- truths:
- - "docker build -t portfolio . reussit sans erreur"
- - "docker run -p 3000:3000 portfolio sert l'app SSR sur port 3000"
- - "GA4 est actif uniquement en production"
- - "app/pages/formation.vue n'existe pas, /formation retourne 404"
- artifacts:
- - path: "Dockerfile"
- provides: "Multi-stage SSR build node:22-alpine"
- - path: "docker-compose.yml"
- provides: "Config Traefik avec port 3000"
- key_links:
- - from: "Dockerfile"
- to: ".output/server/index.mjs"
- via: "node .output/server/index.mjs"
- pattern: "node.*\\.output/server/index\\.mjs"
- - from: "docker-compose.yml"
- to: "Traefik"
- via: "loadbalancer.server.port=3000"
- pattern: "port=3000"
----
-
-
-Finaliser l'infrastructure de deploiement : Dockerfile SSR multi-stage, config GA4 production-only, mise a jour docker-compose Traefik, gestion de la page formation supprimee, et nettoyage legacy.
-
-Purpose: Rend le portfolio deployable en production via Docker + Traefik avec analytics.
-Output: Dockerfile SSR fonctionnel, GA4 configure, docker-compose mis a jour.
-
-
-
-@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
-@C:\Users\minit\.claude\get-shit-done\templates\summary.md
-
-
-
-@.planning/PROJECT.md
-@.planning/ROADMAP.md
-@.planning/phases/03-pages-ship/03-CONTEXT.md
-@.planning/phases/03-pages-ship/03-RESEARCH.md
-@.planning/phases/03-pages-ship/03-02-SUMMARY.md
-@.planning/phases/03-pages-ship/03-03-SUMMARY.md
-
-@Dockerfile
-@docker-compose.yml
-@nuxt.config.ts
-
-
-
-
-
- Task 1: Dockerfile SSR multi-stage + docker-compose Traefik port 3000
- Dockerfile, docker-compose.yml
-
-**Dockerfile** (per D-12, D-13, INFRA-01) : Reecrire completement le Dockerfile existant (qui copie dist/ vers nginx). Implementation exacte du RESEARCH.md Pattern 6 :
-
-```dockerfile
-# 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
-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"]
-```
-
-IMPORTANT : Copie `.output/` PAS `dist/` (per RESEARCH.md Pitfall 3). Pas de nginx. Node sert directement.
-
-Ajouter un `.dockerignore` s'il n'existe pas :
-```
-node_modules
-.nuxt
-.output
-dist
-src
-.git
-*.md
-.planning
-```
-
-**docker-compose.yml** (per D-14) : Modifier la ligne port Traefik :
-```yaml
-- 'traefik.http.services.portfolio.loadbalancer.server.port=3000' # was 80
-```
-Changer uniquement cette ligne. Conserver tout le reste intact (labels Traefik TLS, routeurs, redirections www).
-
-Ajouter les variables d'environnement SMTP dans la section `environment` du service portfolio :
-```yaml
-environment:
- - TZ=Europe/Paris
- - NUXT_SMTP_HOST=${NUXT_SMTP_HOST}
- - NUXT_SMTP_USER=${NUXT_SMTP_USER}
- - NUXT_SMTP_PASS=${NUXT_SMTP_PASS}
- - NUXT_SMTP_TO=${NUXT_SMTP_TO}
- - NUXT_PUBLIC_GTAG_ID=${NUXT_PUBLIC_GTAG_ID}
-```
-
-
- cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q ".output/server/index.mjs" Dockerfile && grep -q "node:22-alpine" Dockerfile && grep -q "port=3000" docker-compose.yml && echo "PASS"
-
- Dockerfile SSR multi-stage node:22-alpine avec .output/, docker-compose port 3000, variables env SMTP/GA4
-
-
-
- Task 2: GA4 production-only + legacy cleanup
- nuxt.config.ts
-
-**GA4 nuxt-gtag** (per D-15, INFRA-04) : Verifier/mettre a jour `nuxt.config.ts` pour que nuxt-gtag soit configure correctement. Le config existant a deja :
-```typescript
-gtag: {
- id: '',
- enabled: import.meta.env.NODE_ENV === 'production',
-},
-```
-Verifier que `runtimeConfig.public.gtag.id` est bien present (deja fait en Plan 01 pour SMTP). Le `NUXT_PUBLIC_GTAG_ID` sera injecte au runtime sans rebuild (per D-13). Rien a changer si deja correct — juste verifier.
-
-**Formation** (per D-19) : Completement supprimee. Si `app/pages/formation.vue` existe, le supprimer. Pas de redirection, pas de routeRules — /formation retourne 404 naturellement.
-
-**Nettoyage complet legacy :** Supprimer le dossier `src/`, `old/`, `nginx.conf`, `index.html`, `eslint.config.ts`, `env.d.ts` — tout le legacy de l'ancien SPA Vue. Le repo doit etre propre apres cette phase.
-
-
- cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "production" nuxt.config.ts && ! test -f app/pages/formation.vue && ! test -d src && echo "PASS"
-
- GA4 nuxt-gtag actif en production via runtimeConfig, formation completement supprimee, legacy src/ et fichiers SPA supprimes
-
-
-
-
-
-## Trust Boundaries
-
-| Boundary | Description |
-|----------|-------------|
-| Docker env vars -> runtimeConfig | Variables SMTP passees au container via docker-compose |
-
-## STRIDE Threat Register
-
-| Threat ID | Category | Component | Disposition | Mitigation Plan |
-|-----------|----------|-----------|-------------|-----------------|
-| T-03-07 | Information Disclosure | docker-compose.yml | mitigate | Variables SMTP referencent ${VAR} pas de valeurs hardcodees — .env non commite |
-| T-03-08 | Information Disclosure | Dockerfile | mitigate | .dockerignore exclut .planning, .git, src, node_modules |
-
-
-
-- `docker build -t portfolio .` complete sans erreur
-- `docker run --rm -p 3000:3000 portfolio` sert l'app sur http://localhost:3000
-- `curl http://localhost:3000/` retourne du HTML complet SSR
-- L'image Docker finale est < 300MB (node:22-alpine + .output seulement)
-- `/formation` retourne 404 (page supprimee per D-19)
-
-
-
-- Dockerfile utilise node:22-alpine en 2 stages, copie .output/, lance node server/index.mjs (per D-12)
-- docker-compose port Traefik = 3000 (per D-14)
-- Variables env SMTP + GA4 passees via docker-compose environment
-- nuxt-gtag actif uniquement en production (per D-15)
-- /formation retourne 404 (D-19), legacy src/ et fichiers SPA supprimes
-- .dockerignore exclut node_modules, .nuxt, .output, src, .git
-
-
-
-After completion, create `.planning/phases/03-pages-ship/03-04-SUMMARY.md`
-
diff --git a/.planning/phases/03-pages-ship/03-04-SUMMARY.md b/.planning/phases/03-pages-ship/03-04-SUMMARY.md
deleted file mode 100644
index 1f4a8ff..0000000
--- a/.planning/phases/03-pages-ship/03-04-SUMMARY.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-phase: 03-pages-ship
-plan: 04
-subsystem: infrastructure-cleanup
-tags: [dockerfile, docker, ssr, ga4, legacy-cleanup, traefik]
-dependency_graph:
- requires: [03-02-PLAN, 03-03-PLAN]
- provides: [ssr-dockerfile, docker-compose-traefik, clean-repo]
- affects: []
-tech_stack:
- added: []
- patterns: [multi-stage Dockerfile node:22-alpine, .output SSR deploy, Traefik port 3000]
-key_files:
- created:
- - .dockerignore
- modified:
- - Dockerfile
- - docker-compose.yml
-decisions:
- - "Dockerfile uses node:22-alpine for both build and runtime stages (no nginx)"
- - "SMTP and GA4 env vars injected via docker-compose environment section"
- - "Formation page not redirected — returns 404 naturally per D-19"
- - "GA4 nuxt-gtag config already correct from Plan 01 — no changes needed"
-metrics:
- duration: 59s
- completed: 2026-04-08
- tasks: 2
- files: 169
----
-
-# Phase 03 Plan 04: Dockerfile SSR + GA4 + Legacy Cleanup Summary
-
-Multi-stage Dockerfile rewritten from nginx/dist to node:22-alpine build+runtime copying .output/ with node server, docker-compose Traefik port updated 80->3000 with SMTP/GA4 env vars, 166 legacy SPA files deleted (src/, old/, nginx.conf, index.html, eslint.config.ts, env.d.ts).
-
-## Task Results
-
-| Task | Name | Commit | Key Files |
-|------|------|--------|-----------|
-| 1 | Dockerfile SSR multi-stage + docker-compose Traefik port 3000 | 39749c6 | Dockerfile, .dockerignore, docker-compose.yml |
-| 2 | GA4 production-only + legacy cleanup | 081ed03 | 166 files deleted (src/, old/, nginx.conf, index.html, eslint.config.ts, env.d.ts) |
-
-## Deviations from Plan
-
-None - plan executed exactly as written.
-
-## Verification
-
-- Dockerfile uses node:22-alpine in 2 stages, copies .output/, CMD node /app/.output/server/index.mjs
-- .dockerignore excludes node_modules, .nuxt, .output, src, .git, .planning
-- docker-compose.yml loadbalancer.server.port=3000
-- docker-compose.yml has NUXT_SMTP_HOST/USER/PASS/TO and NUXT_PUBLIC_GTAG_ID env vars (${VAR} references, no hardcoded secrets)
-- nuxt-gtag enabled only in production (import.meta.env.NODE_ENV === 'production')
-- runtimeConfig.public.gtag.id present for runtime injection
-- No app/pages/formation.vue exists — /formation returns 404
-- src/ directory completely removed
-- old/, nginx.conf, index.html, eslint.config.ts, env.d.ts removed
-
-## Self-Check: PASSED
diff --git a/.planning/phases/03-pages-ship/03-CONTEXT.md b/.planning/phases/03-pages-ship/03-CONTEXT.md
deleted file mode 100644
index 123df10..0000000
--- a/.planning/phases/03-pages-ship/03-CONTEXT.md
+++ /dev/null
@@ -1,148 +0,0 @@
-# Phase 3: Pages & Ship - Context
-
-**Gathered:** 2026-04-08
-**Status:** Ready for planning
-
-
-## Phase Boundary
-
-Toutes les 8 pages du portfolio sont construites avec contenu réel, les composants interactifs fonctionnent (galerie modale, formulaire contact, FAQ accordion), EmailJS envoie les emails, GA4 est actif en production, et le Dockerfile SSR est prêt pour le déploiement via Traefik.
-
-
-
-
-## Implementation Decisions
-
-### Page d'accueil (Landing)
-- **D-01:** 6 sections conservées : Hero → Projets vedettes → Services → Témoignages → FAQ → CTA final
-- **D-02:** Hero texte seul (titre + sous-titre + 3 boutons CTA), pas d'image ni animation
-- **D-03:** 3 projets vedettes sur la landing (ceux avec `featured: true` dans les données)
-
-### Page Projects
-- **D-04:** Filtres = barre de recherche texte + boutons catégorie (Web, Bot, Plugin, etc.) — comme l'actuel
-
-### Galerie modale images
-- **D-05:** UModal + UCarousel (composants Nuxt UI v3 natifs) pour la galerie
-- **D-06:** Bande de thumbnails cliquables sous l'image principale
-- **D-07:** Navigation clavier conservée : flèches gauche/droite + Escape pour fermer
-
-### Formulaire contact
-- **D-08:** 3 champs seulement : Nom, Email, Message — friction minimale
-- **D-09:** Validation Zod côté client avant envoi
-- **D-10:** Feedback via UToast (notification Nuxt UI) en haut à droite — succès ou erreur
-- **D-11:** Envoi via SMTP direct (OVH) — API route serveur Nuxt (`server/api/contact.post.ts`) avec nodemailer, credentials dans runtimeConfig privé (NUXT_SMTP_HOST, NUXT_SMTP_USER, NUXT_SMTP_PASS)
-
-### Dockerfile & déploiement
-- **D-12:** SSR avec Node.js — node:22-alpine build + node:22-alpine runtime, copie `.output/`
-- **D-13:** Variables d'environnement via runtimeConfig (NUXT_PUBLIC_*) — pas de rebuild pour changer les valeurs
-- **D-14:** docker-compose.yml existant avec Traefik — mettre à jour le port loadbalancer de 80 vers 3000
-- **D-15:** GA4 via nuxt-gtag, activé uniquement en production via runtimeConfig
-
-### Pages restantes (About, Fiverr, 404)
-- **D-16:** Migration fidèle des pages existantes depuis src/views/ — mêmes sections et contenu, composants Nuxt UI
-- **D-17:** Page About : bio + tech stack badges (TechBadge.vue à migrer)
-- **D-18:** Page Fiverr : hero + service cards + FAQ accordion (UAccordion) + CTA
-- **D-19:** Page Formation SUPPRIMÉE — contenu pricing SaaS non pertinent pour un portfolio freelance
-- **D-20:** Page 404 : error.vue Nuxt avec message et lien retour accueil
-
-### Claude's Discretion
-- Design exact des cards projets, services, témoignages
-- Animations et transitions entre pages/sections
-- Espacement, tailles de police, responsive breakpoints
-- Structure interne des composants (découpage en sous-composants)
-- Ordre des tâches d'implémentation et découpage en plans
-
-
-
-
-## Canonical References
-
-**Downstream agents MUST read these before planning or implementing.**
-
-### Projet & Requirements
-- `.planning/REQUIREMENTS.md` — Requirements PAGE-01 à PAGE-08, COMP-01 à COMP-04, INFRA-01, INFRA-04
-- `.planning/ROADMAP.md` — Phase 3 success criteria (5 critères)
-- `.planning/phases/02-ssr-shell/02-CONTEXT.md` — Décisions Phase 2 (design system, layout, couleurs)
-
-### Pages source (migration reference)
-- `src/views/HomePage.vue` — Structure landing : 6 sections
-- `src/views/ProjectsPage.vue` — Liste projets avec filtres
-- `src/views/ProjectDetailPage.vue` — Détail projet + galerie
-- `src/views/AboutPage.vue` — Bio + tech stack
-- `src/views/ContactPage.vue` — Formulaire + infos contact
-- `src/views/FiverrPage.vue` — Landing services Fiverr
-- `src/views/FormationPage.vue` — Page formations
-
-### Composants source (migration reference)
-- `src/components/sections/HeroSection.vue` — Hero avec CTA buttons
-- `src/components/sections/FeaturedProjectsSection.vue` — Projets vedettes
-- `src/components/sections/ServicesSection.vue` — Services cards
-- `src/components/sections/CTASection.vue` — CTA final
-- `src/components/GalleryModal.vue` — Galerie modale actuelle (custom)
-- `src/components/ProjectCard.vue` — Card projet
-- `src/components/TestimonialsSection.vue` — Témoignages
-- `src/components/ServiceFAQ.vue` — FAQ accordion
-- `src/components/FiverrHero.vue` — Hero Fiverr
-- `src/components/FiverrServiceCard.vue` — Cards services Fiverr
-- `src/components/TechBadge.vue` — Badge technologie
-- `src/components/ContactMethod.vue` — Méthode de contact
-
-### Données migrées
-- `app/data/projects.ts` — Projets avec interfaces TypeScript
-- `app/data/testimonials.ts` — Témoignages
-- `app/data/faq.ts` — FAQ
-- `app/data/techstack.ts` — Stack technique
-
-### Infrastructure
-- `docker-compose.yml` — Config Traefik existante (port à mettre à jour)
-- `Dockerfile` — Dockerfile actuel à réécrire pour SSR
-- `src/config/site.ts` — Configuration site (contacts, réseaux sociaux)
-
-
-
-
-## Existing Code Insights
-
-### Reusable Assets
-- `app/composables/useProjects.ts` — Composable projets déjà migré avec filtrage et recherche
-- `app/data/*.ts` — 4 fichiers de données statiques déjà migrés avec interfaces TypeScript
-- `i18n/locales/fr.json` et `en.json` — 500+ clés de traduction incluant contenu pages
-- `app/components/layout/AppHeader.vue` et `AppFooter.vue` — Layout déjà en place
-- `app/layouts/default.vue` — Layout par défaut header + slot + footer
-
-### Established Patterns
-- Nuxt 4 avec `app/` directory, auto-imports, compatibilityVersion 4
-- Nuxt UI v3 pour tous les composants (UButton, UCard, UModal, UAccordion, UForm, UInput, UTextarea, UToast)
-- useSeoMeta() par route pour les métadonnées SEO
-- Couleur primaire brand #85cb85 via CSS @theme + app.config.ts
-- i18n via useI18n() avec clés localisées
-
-### Integration Points
-- Pages stubs dans `app/pages/` (index, projects, about, contact, fiverr, formation) — à enrichir
-- Route dynamique à créer : `app/pages/project/[id].vue`
-- `error.vue` à créer à la racine de `app/`
-- nuxt.config.ts gtag config à activer avec runtimeConfig
-- Dockerfile à réécrire complètement
-
-
-
-
-## Specific Ideas
-
-- Galerie avec thumbnails cliquables — l'utilisateur veut pouvoir naviguer visuellement entre les images
-- Formulaire contact minimaliste (3 champs) — friction minimale pour maximiser les conversions
-- Déploiement existant via Traefik avec TLS wildcard sur killiandalcin.fr
-
-
-
-
-## Deferred Ideas
-
-None — discussion stayed within phase scope
-
-
-
----
-
-*Phase: 03-pages-ship*
-*Context gathered: 2026-04-08*
diff --git a/.planning/phases/03-pages-ship/03-RESEARCH.md b/.planning/phases/03-pages-ship/03-RESEARCH.md
deleted file mode 100644
index 4de7f71..0000000
--- a/.planning/phases/03-pages-ship/03-RESEARCH.md
+++ /dev/null
@@ -1,700 +0,0 @@
-# Phase 3: Pages & Ship - Research
-
-**Researched:** 2026-04-08
-**Domain:** Nuxt 4 pages, Nuxt UI v3 composants interactifs, Nodemailer SMTP, Docker SSR
-**Confidence:** HIGH
-
----
-
-
-## User Constraints (from CONTEXT.md)
-
-### Locked Decisions
-
-- **D-01:** 6 sections sur la landing : Hero → Projets vedettes → Services → Témoignages → FAQ → CTA final
-- **D-02:** Hero texte seul (titre + sous-titre + 3 boutons CTA), pas d'image ni animation
-- **D-03:** 3 projets vedettes sur la landing (ceux avec `featured: true`)
-- **D-04:** Filtres projects = barre de recherche texte + boutons catégorie — comme l'actuel
-- **D-05:** UModal + UCarousel (Nuxt UI v3 natifs) pour la galerie
-- **D-06:** Bande de thumbnails cliquables sous l'image principale
-- **D-07:** Navigation clavier conservée : flèches gauche/droite + Escape pour fermer
-- **D-08:** 3 champs formulaire seulement : Nom, Email, Message
-- **D-09:** Validation Zod côté client avant envoi
-- **D-10:** Feedback via UToast en haut à droite — succès ou erreur
-- **D-11:** Envoi via SMTP direct (OVH) — `server/api/contact.post.ts` avec nodemailer, credentials dans runtimeConfig privé
-- **D-12:** SSR node:22-alpine build + node:22-alpine runtime, copie `.output/`
-- **D-13:** Variables d'environnement via runtimeConfig (NUXT_PUBLIC_*) — pas de rebuild pour changer les valeurs
-- **D-14:** docker-compose.yml existant avec Traefik — mettre à jour le port loadbalancer de 80 vers 3000
-- **D-15:** GA4 via nuxt-gtag, activé uniquement en production via runtimeConfig
-- **D-16:** Migration fidèle des pages existantes depuis src/views/ — mêmes sections et contenu, composants Nuxt UI
-- **D-17:** Page About : bio + tech stack badges (TechBadge.vue à migrer)
-- **D-18:** Page Fiverr : hero + service cards + FAQ accordion (UAccordion) + CTA
-- **D-19:** Page Formation SUPPRIMÉE
-- **D-20:** Page 404 : error.vue Nuxt avec message et lien retour accueil
-
-### Claude's Discretion
-
-- Design exact des cards projets, services, témoignages
-- Animations et transitions entre pages/sections
-- Espacement, tailles de police, responsive breakpoints
-- Structure interne des composants (découpage en sous-composants)
-- Ordre des tâches d'implémentation et découpage en plans
-
-### Deferred Ideas (OUT OF SCOPE)
-
-None — discussion stayed within phase scope
-
-
----
-
-
-## Phase Requirements
-
-| ID | Description | Research Support |
-|----|-------------|------------------|
-| PAGE-01 | Page Landing `/` — hero, projets vedettes, services, CTA | useFeaturedProjects() + UCard + UButton — patterns établis Phase 2 |
-| PAGE-02 | Page Projects `/projects` — liste avec filtres (recherche + catégorie) | useProjects() composable déjà migré avec search() + filterByCategory() |
-| PAGE-03 | Page Project Detail `/project/[id]` — détail + galerie modale d'images | Route dynamique `[id].vue` + UModal + UCarousel avec emblaApi.scrollTo() |
-| PAGE-04 | Page About `/about` — biographie, tech stack badges | Données techstack.ts déjà migrées + UBadge ou UCard pour badges |
-| PAGE-05 | Page Contact `/contact` — formulaire validation + envoi SMTP | UForm + Zod + nodemailer dans server/api/contact.post.ts |
-| PAGE-06 | Page Fiverr `/fiverr` — landing services, cards, FAQ accordion, CTA | UAccordion avec items array + clés i18n |
-| PAGE-07 | Page Formation `/formation` — SUPPRIMÉE (D-19) | Créer une redirection vers `/` ou stub vide |
-| PAGE-08 | Page 404 — `error.vue` avec lien retour accueil | error.vue à la racine `app/`, prop `error.status`, clearError({ redirect: '/' }) |
-| COMP-01 | Galerie modale — UModal + UCarousel + navigation clavier | UModal v-model:open + UCarousel ref + keydown listener |
-| COMP-02 | Formulaire contact — UForm + Zod + envoi SMTP | schema Zod + state reactive + defineEventHandler + readBody + nodemailer |
-| COMP-03 | FAQ accordion — UAccordion localisé FR/EN | UAccordion :items avec questionKey/answerKey résolus via t() |
-| COMP-04 | Section témoignages — UCard par témoignage | testimonials.ts déjà migré, UCard avec slots header/body |
-| INFRA-01 | Dockerfile production multi-stage node:22-alpine | Build stage : npm install + nuxt build ; Runtime : copie .output/, node server/index.mjs |
-| INFRA-04 | GA4 via nuxt-gtag, actif uniquement en production | nuxt-gtag v4.1.0 déjà installé ; enabled: process.env.NODE_ENV === 'production' |
-
-
----
-
-## Summary
-
-La Phase 3 construit et livre les 8 pages du portfolio avec leurs composants interactifs (galerie modale, formulaire contact, FAQ) et package le tout dans une image Docker SSR prête pour Traefik.
-
-La base technique est solide : Nuxt 4 avec `app/` directory, Nuxt UI v3, i18n, color-mode et sitemap sont tous opérationnels depuis la Phase 2. Les données (`app/data/*.ts`) et le composable `useProjects()` sont déjà migrés. Les stubs de pages existent dans `app/pages/`. Il s'agit donc principalement de **remplir le contenu** des pages existantes et d'ajouter les composants manquants.
-
-Les deux zones de risque technique sont : (1) la galerie modale UCarousel avec thumbnails — la navigation programmatique via `emblaApi` est légèrement non-standard et requiert `useTemplateRef` ; (2) le Dockerfile SSR qui doit switcher de nginx/static vers node/SSR — l'actuel `Dockerfile` copie `dist/` vers nginx, il faut le réécrire entièrement pour copier `.output/` et lancer `node server/index.mjs`.
-
-**Recommandation principale :** Procéder par ordre logique — d'abord les pages statiques simples (Landing, About, Fiverr, Projects), puis les composants interactifs (galerie, formulaire), enfin Docker et GA4. Installer nodemailer (`npm install nodemailer`) et zod (`npm install zod`) avant d'attaquer le formulaire.
-
----
-
-## Standard Stack
-
-### Core (déjà installé)
-
-| Library | Version installée | Purpose | Source |
-|---------|-------------------|---------|--------|
-| @nuxt/ui | ^3.0.0 | UModal, UCarousel, UForm, UAccordion, UToast | [VERIFIED: package.json] |
-| @nuxt/image | ^2.0.0 | NuxtImg lazy loading + WebP | [VERIFIED: package.json] |
-| nuxt-gtag | ^4.1.0 | GA4 production-only | [VERIFIED: package.json] |
-| nuxt | ^4.0.0 | error.vue, defineEventHandler, useRuntimeConfig | [VERIFIED: package.json] |
-
-### À installer
-
-| Library | Version actuelle | Purpose | Source |
-|---------|-----------------|---------|--------|
-| nodemailer | 8.0.5 | SMTP OVH dans server/api route | [VERIFIED: npm registry] |
-| zod | 4.3.6 | Validation Zod côté client UForm | [VERIFIED: npm registry] |
-| @types/nodemailer | latest | Types TypeScript pour nodemailer | [ASSUMED] |
-
-**Installation :**
-```bash
-npm install nodemailer zod
-npm install --save-dev @types/nodemailer
-```
-
-### Alternatives considérées
-
-| Au lieu de | Pourrait utiliser | Compromis |
-|------------|------------------|-----------|
-| nodemailer direct | nuxt-mail module | nuxt-mail ajoute une couche d'abstraction — inutile pour un seul endpoint |
-| Zod | Valibot | Zod est standard avec Nuxt UI v3 UForm (schéma accepté nativement) |
-| node:22-alpine | node:22-slim (Debian) | Alpine peut poser des problèmes de musl ABI pour native deps ; nodemailer n'a pas de native deps donc alpine est OK ici |
-
----
-
-## Architecture Patterns
-
-### Structure projet Phase 3
-
-```
-app/
-├── pages/
-│ ├── index.vue # Landing — à enrichir (stub existant)
-│ ├── projects.vue # Projets — à enrichir (stub existant)
-│ ├── about.vue # About — à enrichir (stub existant)
-│ ├── contact.vue # Contact — à enrichir (stub existant)
-│ ├── fiverr.vue # Fiverr — à enrichir (stub existant)
-│ └── project/
-│ └── [id].vue # Détail projet — À CRÉER
-├── components/
-│ ├── sections/
-│ │ ├── HeroSection.vue # À CRÉER
-│ │ ├── FeaturedProjectsSection.vue # À CRÉER
-│ │ ├── ServicesSection.vue # À CRÉER
-│ │ ├── TestimonialsSection.vue # À CRÉER
-│ │ ├── FAQSection.vue # À CRÉER
-│ │ └── CTASection.vue # À CRÉER
-│ ├── ProjectCard.vue # À CRÉER
-│ ├── ProjectGallery.vue # À CRÉER (UModal + UCarousel)
-│ ├── ContactForm.vue # À CRÉER (UForm + Zod)
-│ └── TechBadge.vue # À CRÉER
-├── error.vue # À CRÉER (racine app/)
-server/
-└── api/
- └── contact.post.ts # À CRÉER
-```
-
-### Pattern 1 : UModal + UCarousel galerie avec thumbnails
-
-UModal utilise `v-model:open` pour l'état d'ouverture. UCarousel expose son instance Embla via `useTemplateRef` pour permettre la navigation programmatique depuis les thumbnails.
-
-```vue
-
-
-
-
-
-
- (currentIndex = i)"
- >
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### Pattern 2 : UForm + Zod pour le formulaire contact
-
-```vue
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Envoyer
-
-
-```
-
-### Pattern 3 : Nodemailer dans server/api/contact.post.ts
-
-```typescript
-// Source : nuxt.com/docs/guide/directory-structure/server + GitHub thaikolja/nuxt-nodemailer-example
-import nodemailer from 'nodemailer'
-
-export default defineEventHandler(async (event) => {
- const body = await readBody(event)
- const config = useRuntimeConfig(event) // Passer event pour que les env vars runtime soient appliquées
-
- const transporter = nodemailer.createTransport({
- host: config.smtpHost,
- port: 465,
- secure: true,
- auth: {
- user: config.smtpUser,
- pass: config.smtpPass,
- },
- })
-
- await transporter.sendMail({
- from: `"Portfolio" <${config.smtpUser}>`,
- to: config.smtpTo,
- subject: `Contact portfolio — ${body.name}`,
- text: `De: ${body.name} <${body.email}>\n\n${body.message}`,
- html: `De: ${body.name} <${body.email}>
${body.message}
`,
- })
-
- return { success: true }
-})
-```
-
-**Configuration nuxt.config.ts à ajouter :**
-
-```typescript
-runtimeConfig: {
- // Privé — jamais exposé au client
- smtpHost: '', // NUXT_SMTP_HOST
- smtpUser: '', // NUXT_SMTP_USER
- smtpPass: '', // NUXT_SMTP_PASS
- smtpTo: '', // NUXT_SMTP_TO
- public: {
- gtag: { id: '' }, // NUXT_PUBLIC_GTAG_ID (déjà en place)
- },
-},
-```
-
-**Variables d'environnement `.env` (non commité) :**
-```ini
-NUXT_SMTP_HOST=ssl0.ovh.net
-NUXT_SMTP_USER=contact@killiandalcin.fr
-NUXT_SMTP_PASS=xxxx
-NUXT_SMTP_TO=contact@killiandalcin.fr
-NUXT_PUBLIC_GTAG_ID=G-CDVVNFY6MV
-```
-
-### Pattern 4 : UAccordion pour FAQ (D-18)
-
-```vue
-
-
-
-
-
-
-```
-
-### Pattern 5 : error.vue (PAGE-08 / D-20)
-
-```vue
-
-
-
-
-
-
{{ error.status }}
-
- {{ error.status === 404 ? 'Page introuvable' : 'Une erreur est survenue' }}
-
-
Retour à l'accueil
-
-
-```
-
-### 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)
diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md
deleted file mode 100644
index 034ddb4..0000000
--- a/.planning/research/ARCHITECTURE.md
+++ /dev/null
@@ -1,230 +0,0 @@
-# Architecture Patterns
-
-**Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
-**Researched:** 2026-04-07
-**Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis)
-
----
-
-## Recommended Architecture
-
-```
-[ Browser ]
- |
- | HTTP request (SSR-rendered HTML on first load)
- v
-[ Nuxt 4 Server (Node 22) ]
- |
- |-- [ app/ ]
- | |-- [ pages/ ] File-based routing
- | |-- [ components/ ] Auto-imported UI components
- | |-- [ composables/ ] Auto-imported reactive logic
- | |-- [ layouts/ ] default.vue (header + footer)
- | |-- [ assets/ ] Static assets (images, fonts)
- | |-- [ plugins/ ] EmailJS init, gtag init
- |
- |-- [ server/ ]
- | |-- (optional) api/ Not needed — no dynamic data
- |
- |-- [ data/ ] Static TS files (projects, testimonials, FAQ, techstack)
- |
- |-- nuxt.config.ts Modules, runtime config, i18n, color-mode
-```
-
-Nuxt 4 uses `app/` as the root source directory (replaces Nuxt 3's flat root layout). All pages, components, composables, layouts, and plugins live under `app/`.
-
----
-
-## Component Boundaries
-
-| Component | Responsibility | Communicates With |
-|-----------|---------------|-------------------|
-| `layouts/default.vue` | Shell: TheHeader + TheFooter + ` ` | All pages via slot |
-| `TheHeader.vue` | Navigation + locale toggle + color-mode toggle | `useI18n()`, `useColorMode()` |
-| `TheFooter.vue` | Links, copyright, social | `useI18n()` |
-| `pages/index.vue` | Hero + featured projects + services + CTA | `useProjects()`, `useSeoMeta()` |
-| `pages/projects.vue` | Project list + filters | `useProjects()` |
-| `pages/project/[id].vue` | Project detail + image gallery modal | `useProjects()`, `UModal` (Nuxt UI) |
-| `pages/about.vue` | Bio, tech stack | `useI18n()`, static `techstack.ts` |
-| `pages/contact.vue` | UForm + EmailJS send | `useContactForm()`, EmailJS plugin |
-| `pages/fiverr.vue` | Fiverr landing, service cards | `useI18n()`, static config |
-| `pages/formation.vue` | Training/course landing | `useI18n()` |
-| `components/ProjectCard.vue` | Reusable card (list + featured) | Props only, no store |
-| `components/GalleryModal.vue` | UModal wrapper for project images | Emits only, props: images[] |
-| `composables/useProjects.ts` | Filter/search logic over static data | Imports `data/projects.ts` |
-| `composables/useSeoMeta.ts` | Per-route `useSeoMeta()` + JSON-LD | Nuxt built-in `useSeoMeta` |
-| `data/*.ts` | Static typed data — single source of truth | Imported by composables only |
-
-**Rule:** Pages import composables. Composables import data files. Components receive props and emit events. No page imports another page. No component imports data files directly.
-
----
-
-## Data Flow
-
-```
-data/projects.ts (static TS, bilingual strings)
- |
- v
-composables/useProjects.ts
- - filter by category
- - find by id
- - expose featuredProjects, allProjects
- |
- v
-pages/index.vue → featuredProjects →
-pages/projects.vue → allProjects + filters →
-pages/project/[id].vue → findById(route.params.id) → detail view +
-```
-
-```
-i18n locale (cookie, SSR-safe)
- |
- v
-@nuxtjs/i18n module (strategy: 'prefix_except_default', defaultLocale: 'fr')
- - /fr/* and /* (default) both work
- - /en/* for English
- |
- v
-All pages and composables via useI18n() (auto-imported by @nuxtjs/i18n)
-```
-
-```
-Color mode (cookie, SSR-safe)
- |
- v
-@nuxtjs/color-mode (cookie strategy, no FOUC)
- |
- v
-TheHeader.vue toggle → Tailwind dark: classes respond immediately
-```
-
-```
-Contact form
- |
- v
-pages/contact.vue → UForm validation → composables/useContactForm.ts
- |
- v
-EmailJS plugin (client-side send, no server route needed)
-```
-
-```
-SEO per route
- |
- v
-Each page calls useSeoMeta() with i18n-translated values
- + JSON-LD script tag on pages/index.vue only
- |
- v
-@nuxtjs/sitemap generates sitemap.xml from route list at build time
-```
-
----
-
-## i18n Architecture Decision
-
-Use `@nuxtjs/i18n` v9 with `strategy: 'prefix_except_default'`:
-- French (`fr`) is default, served at `/`, `/projects`, `/project/[id]`, etc.
-- English served at `/en`, `/en/projects`, `/en/project/[id]`, etc.
-- Locale detected from browser `Accept-Language` header on first visit (server-side), then persisted in cookie.
-- **No redirect strategy** — prefix_except_default avoids redirect chains that hurt Core Web Vitals.
-- Translation strings live in `app/i18n/locales/fr.ts` and `app/i18n/locales/en.ts` (migrated from existing `src/locales/`).
-
-The existing `useI18n.ts` composable wrapping vue-i18n is replaced entirely by the `useI18n()` auto-import provided by `@nuxtjs/i18n`.
-
----
-
-## Static Data Layer
-
-Decision: static TS files in `data/` (not `@nuxt/content`, not `server/api`).
-
-Rationale:
-- All project data is known at build time and changes infrequently.
-- `@nuxt/content` adds markdown parsing overhead and a file-system watcher not needed for typed data.
-- `server/api` routes add network round-trips and cold-start latency for data that never changes.
-- Static TS files are tree-shakeable, fully typed, and zero-overhead.
-
-Migration from `src/data/` is direct: copy files to `data/`, ensure bilingual structure is preserved (FR/EN fields in same object, selected by locale in composables).
-
----
-
-## Deployment: SSR vs SSG
-
-**Recommendation: `nuxt build` (SSR) not `nuxt generate` (SSG).**
-
-Rationale:
-- i18n with cookie-based locale detection requires server execution to read the cookie and render the correct language on first request. SSG pre-renders all routes in one language only.
-- `useSeoMeta()` with i18n-reactive values requires server-side execution per request.
-- The Docker image runs `node server/index.mjs` (the Nuxt nitro server) — not nginx serving static files.
-- SSR does not meaningfully increase operational complexity for a portfolio (low traffic, single container).
-
-Dockerfile pattern: multi-stage — build stage (`node:22-alpine` + `nuxt build`), production stage (copy `.output/` only, `CMD ["node", ".output/server/index.mjs"]`). No nginx layer needed.
-
----
-
-## Suggested Build Order (Phase Dependencies)
-
-```
-1. nuxt.config.ts + nuxt.config modules
- Depends on: nothing
- Blocks: everything — module config must exist before page/component work
-
-2. data/ migration (static TS files)
- Depends on: nothing
- Blocks: composables, all pages that display content
-
-3. composables/ migration
- Depends on: data/
- Blocks: pages that use useProjects(), useSeoMeta()
-
-4. layouts/default.vue + TheHeader + TheFooter
- Depends on: @nuxtjs/i18n working, @nuxtjs/color-mode working
- Blocks: all page development (every page needs a shell)
-
-5. pages/ migration (one page at a time, start with index.vue)
- Depends on: composables, layouts, Nuxt UI v3 components
- Blocks: nothing else — pages are leaf nodes
-
-6. plugins/ (EmailJS, nuxt-gtag)
- Depends on: contact page, nuxt.config
- Blocks: contact form functionality, GA tracking
-
-7. Dockerfile + deployment
- Depends on: all pages complete
- Blocks: production ship
-```
-
-**Critical dependency:** `nuxt.config.ts` with `@nuxtjs/i18n`, `@nuxtjs/color-mode`, `@nuxt/ui`, and `@nuxtjs/sitemap` must be functional before any page/component work begins. All auto-imports, CSS variables, and the `useI18n()` composable availability depend on this configuration.
-
----
-
-## Anti-Patterns to Avoid
-
-### Anti-Pattern 1: localStorage in SSR Context
-**What goes wrong:** `localStorage.setItem('locale', ...)` throws ReferenceError on server, causes hydration mismatch.
-**Prevention:** Use only `useCookie()` (Nuxt built-in) for any client-persisted state (locale, color mode). Both `@nuxtjs/i18n` and `@nuxtjs/color-mode` handle this when configured with `cookieName`.
-
-### Anti-Pattern 2: `document.*` or `window.*` at module scope
-**What goes wrong:** Runs during SSR, crashes server render.
-**Prevention:** Wrap in `onMounted()` or use `import.meta.client` guard. The existing router `beforeEach` that calls `document.title` must move to `useSeoMeta()` calls inside each page.
-
-### Anti-Pattern 3: Flat root structure (Nuxt 3 pattern in Nuxt 4)
-**What goes wrong:** Nuxt 4 expects `app/` as source directory. Files at project root are not auto-imported.
-**Prevention:** All Vue files, composables, components go under `app/`. Configure `srcDir: 'app'` in `nuxt.config.ts` (or rely on Nuxt 4 default).
-
-### Anti-Pattern 4: useAsyncData for static data
-**What goes wrong:** `useAsyncData` with a static import adds unnecessary async overhead and serialization. Static data does not need SSR serialization.
-**Prevention:** Import static TS data directly in composables. Reserve `useAsyncData` for genuine async operations (external fetch, server routes).
-
-### Anti-Pattern 5: Per-page SEO via router.beforeEach
-**What goes wrong:** `document.title` manipulation in router guards is SPA-only, invisible to crawlers.
-**Prevention:** Each page calls `useSeoMeta({ title, description, ogTitle, ogDescription, ogImage })` at setup scope — Nuxt handles server-side `` injection.
-
----
-
-## Sources
-
-- Nuxt 4 source directory convention: official Nuxt 4 migration guide (app/ directory)
-- Existing codebase analysis: `src/composables/`, `src/router/index.ts`, `src/locales/`
-- PROJECT.md constraints: cookie-only persistence, EmailJS, static TS data, Docker SSR deployment
-- Confidence: HIGH for Nuxt 4 file conventions; HIGH for SSR vs SSG decision given i18n cookie requirement; MEDIUM for @nuxtjs/i18n v9 prefix_except_default (verify exact config key names against current docs before implementing)
diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md
deleted file mode 100644
index cb45d77..0000000
--- a/.planning/research/FEATURES.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# Feature Landscape
-
-**Domain:** Freelance developer portfolio — Nuxt 4 SSR migration
-**Researched:** 2026-04-07
-**Confidence:** MEDIUM — Nuxt UI v3 component coverage from training knowledge (cutoff Aug 2025); Nuxt 4 stable by then. Flag for validation against current ui.nuxt.com docs before implementation.
-
----
-
-## Table Stakes
-
-Features users and search engines expect. Missing = product feels incomplete or hurts SEO directly.
-
-| Feature | Why Expected | Complexity | Nuxt UI v3 Coverage | Notes |
-|---------|--------------|------------|---------------------|-------|
-| SSR on every route | Google crawls without JS; core migration reason | Low (Nuxt default) | N/A — framework concern | `nuxt build` gives SSR; `nuxt generate` gives SSG. SSR preferred for dynamic og:image |
-| Per-route SEO meta | Each page needs unique title, description, og:image | Low | `useSeoMeta()` (Nuxt built-in) | Already implemented in SPA via custom `useSeo()` — replace with `useSeoMeta()` |
-| JSON-LD structured data | Enables rich results in Google for Person, CreativeWork, ContactPage | Low | `useHead()` with script injection | Already on Home + Contact + Projects — migrate all pages |
-| Sitemap.xml | Required for indexing; Google Search Console standard | Low | `@nuxtjs/sitemap` module | Out-of-the-box with i18n support |
-| robots.txt | Crawl control; expected by all search engines | Trivial | `@nuxtjs/sitemap` handles it | |
-| Dark/light mode — no FOUC | Flash of unstyled content = unprofessional | Medium | `@nuxtjs/color-mode` with cookie strategy | The SPA currently uses localStorage — causes FOUC on SSR. Cookie strategy required |
-| i18n FR/EN | Already a feature; SSR-safe version expected | Medium | `@nuxtjs/i18n` v9 (Nuxt 4 compatible) | Current vue-i18n with localStorage is not SSR-safe; cookie persistence required |
-| Language switch persisted across sessions | Users hate re-setting language on return | Low | `@nuxtjs/i18n` `detectBrowserLanguage` with `cookieSecure` | |
-| Responsive layout — mobile first | 60%+ of portfolio visitors on mobile | Low | Nuxt UI v3 + Tailwind v4 | All Nuxt UI components are mobile-first |
-| Project list with filters | Portfolio core feature; already built | Medium | `UInput` (search), `USelectMenu` or `UTabs` (filter), `UBadge` (category tags) | Current: custom `` + text ` `. Migrate to Nuxt UI |
-| Project detail page with gallery | Proves depth of work | Medium | `UModal` (lightbox), `UCarousel` (thumbnails) | Current GalleryModal.vue (custom) → replace with `UModal` + `UCarousel` |
-| Contact methods display | GitHub, LinkedIn, email, phone — visitors need this | Low | `UCard`, `UButton`, `ULink` | Current ContactPage.vue uses custom card design |
-| Navigation header with mobile menu | Standard expectation | Low | `UNavigationMenu` or `UHeader` (Nuxt UI Pro) | If not using Pro: compose with `UDrawer` for mobile nav overlay |
-| Footer with links | Standard; also helps SEO via internal links | Low | Custom with `ULink` | |
-| 404 page | Missing = 404 error shows server default | Trivial | `error.vue` in Nuxt root | |
-| Image optimization | Core Web Vitals; LCP often an image | Medium | `@nuxt/image` → `` | Hero image preload + lazy load for project thumbnails |
-| Local fonts (no Google Fonts FOUT) | Flash of unstyled text on SSR | Low | `@nuxtjs/google-fonts` with `download: true` or manual `public/fonts/` | Prefer manual: zero dependency |
-
----
-
-## Differentiators
-
-Features that elevate the portfolio above average. Not universally expected but add credibility.
-
-| Feature | Value Proposition | Complexity | Nuxt UI v3 Coverage | Notes |
-|---------|-------------------|------------|---------------------|-------|
-| Contact form with email delivery | Visitors can send a message directly — reduces friction vs email link only | Medium | `UForm` + `UFormField` + `UInput` + `UTextarea` + `UButton` | Backend-free via EmailJS. `UForm` handles validation schema (Zod/Valibot). Current SPA has NO form — this is new |
-| Testimonials section | Social proof — differentiates freelancer from agency | Low | `UCard` for testimonial cards, custom grid | Already has TestimonialsSection.vue — migrate design to Nuxt UI cards |
-| Services/pricing page (Fiverr landing) | Conversion-focused; makes offering concrete | Medium | `UCard` (service cards), `UBadge` (tags), `UAccordion` (FAQ) | Already exists as FiverrPage.vue — migrate FAQ to `UAccordion` |
-| Tech stack badges | Visual proof of skills without reading text | Low | `UBadge` with `color` and `variant` props | Current TechBadge.vue is custom — replace with `UBadge` |
-| Stats display (projects count, featured, etc.) | Builds credibility at a glance | Low | Custom with Tailwind / `UCard` | Already on Projects page and Contact page |
-| Formation/training page | Demonstrates continued learning | Low | `UCard`, `UBadge`, `UTimeline` (if available in v3) | Already exists — migrate |
-| Keyboard navigation in gallery | Accessibility + power-user UX | Low | `UModal` supports keyboard close (Escape) natively; add arrow key handler | Current GalleryModal.vue already handles keyboard — preserve in migration |
-| og:image per project | Rich previews when shared on LinkedIn/Twitter | Low | `useSeoMeta()` with `ogImage` per page | Already implemented in SPA — ensure NuxtImg doesn't break paths |
-| Preload hero image | LCP optimization — measurable Google ranking signal | Low | `useHead({ link: [{ rel: 'preload', as: 'image' }] })` | Single line addition |
-| Google Analytics 4 via nuxt-gtag | Current hardcoded GA in index.html is fragile | Low | `nuxt-gtag` module | Replace `index.html` script tag with proper module |
-
----
-
-## Anti-Features
-
-Things to deliberately NOT build in this migration.
-
-| Anti-Feature | Why Avoid | What to Do Instead |
-|--------------|-----------|-------------------|
-| Contact form with custom backend / API route | Adds infra complexity, auth, spam handling — out of scope per PROJECT.md | EmailJS from client — form submits directly, no Nuxt server route needed |
-| @nuxt/content for project data | CMS markdown adds indirection when data is already typed TS | Keep `src/data/` as `.ts` files imported by composables |
-| Blog / articles section | Not in scope; adds content maintenance burden | If needed later, add as a separate milestone |
-| Portfolio password protection | Friction for recruiters / clients browsing | Open portfolio is the point |
-| Infinite scroll on projects page | Premature — project count is small; adds complexity | Paginated list or full list is sufficient |
-| Animation library (GSAP, Motion One) | Heavy; Tailwind CSS animations + CSS transitions are sufficient | CSS `transition`, `@keyframes` via Tailwind |
-| Umami analytics self-hosted | Out of scope per PROJECT.md — requires infra | GA4 via nuxt-gtag |
-| Custom color theme picker | Dark/light binary is sufficient; theme builder adds JS weight and UX surface | `@nuxtjs/color-mode` toggle only |
-| CMS admin panel | No need for non-dev content editing | Static TS data files, update via code |
-| i18n for more than FR/EN | Scope creep; translation maintenance doubles for each language | FR/EN only |
-
----
-
-## Nuxt UI v3 Component Coverage Map
-
-Mapped against every portfolio pattern in this project. Confidence: MEDIUM (training data on v3 alpha/beta; verify against ui.nuxt.com before building).
-
-| Portfolio Pattern | Nuxt UI v3 Component(s) | Replaces (current) | Notes |
-|-------------------|------------------------|---------------------|-------|
-| Navigation menu desktop | `UNavigationMenu` | Custom `AppHeader.vue` nav links | Composable nav with active state |
-| Mobile menu drawer | `UDrawer` or `UModal` | Custom hamburger + overlay | `UDrawer` preferred for slide-in nav |
-| Dark/light toggle button | `UButton` with icon slot | `ThemeToggle.vue` (custom) | Toggle reads `useColorMode()` |
-| Language switcher dropdown | `UDropdownMenu` | `LanguageSwitcher.vue` (custom) | `UDropdownMenu` items = `[{ label: 'FR' }, { label: 'EN' }]` |
-| Project card | `UCard` | `ProjectCard.vue` (custom) | `UCard` header/footer/body slots |
-| Project filter search input | `UInput` with icon | Custom ` ` | Leading icon slot for magnifier |
-| Project category filter | `USelectMenu` or `UTabs` | Custom `` | `UTabs` better UX if <6 categories |
-| Project gallery modal/lightbox | `UModal` + `UCarousel` | `GalleryModal.vue` (custom) | `UModal` handles focus trap + Escape; `UCarousel` for image navigation with prev/next |
-| Contact form fields | `UForm` + `UFormField` + `UInput` + `UTextarea` | No form currently | `UForm` integrates with Zod/Valibot for schema validation |
-| Contact form submit button with loading | `UButton` with `loading` prop | N/A | `UButton loading` shows spinner during EmailJS send |
-| Social link items | `ULink` or `UButton` variant=link | Custom `` tags | |
-| Service/Fiverr service cards | `UCard` | `FiverrServiceCard.vue` (custom) | |
-| FAQ accordion | `UAccordion` | `ServiceFAQ.vue` (custom) | Built-in open/close, accessible |
-| Testimonial cards | `UCard` | `TestimonialCard.vue` (custom) | |
-| Tech skill badges | `UBadge` | `TechBadge.vue` (custom) | `color` and `variant` props cover current custom styles |
-| Section CTA buttons | `UButton` | `CTAButtons.vue` (custom) | `UButton` size/variant props handle all current btn variants |
-| 404 error page | Custom `error.vue` with `UButton` | N/A (SPA handled by router) | Nuxt `error.vue` gets `error` prop |
-| Toast / form feedback | `useToast()` + `UToastProvider` | None currently | Show success/error after EmailJS send |
-| Page loading indicator | `NuxtLoadingIndicator` (Nuxt built-in) | None in SPA | One-liner in `app.vue` |
-
----
-
-## Feature Dependencies
-
-```
-SSR (nuxt build)
- → i18n cookie persistence (@nuxtjs/i18n v9)
- → Language switcher UI (UDropdownMenu)
- → Dark mode cookie (@nuxtjs/color-mode)
- → Theme toggle UI (UButton + useColorMode)
- → Per-route SEO (useSeoMeta)
- → Sitemap (@nuxtjs/sitemap)
- → og:image per project
-
-Contact form (UForm + Zod)
- → EmailJS client send
- → UButton loading state
- → useToast() success/error feedback
-
-Project gallery (UModal + UCarousel)
- → Project detail page
- → Project data (TS static files)
- → useProjects() composable (useAsyncData wrapper)
-
-Image optimization (NuxtImg)
- → @nuxt/image module
- → Hero preload (useHead link preload)
-```
-
----
-
-## MVP Recommendation
-
-Phases should prioritize in this order to unblock everything else:
-
-1. **SSR foundation** — Nuxt 4 project scaffold, routing, layouts, @nuxtjs/color-mode, @nuxtjs/i18n. Without this nothing else works correctly.
-2. **Static data migration** — Port `src/data/` TS files + composables to Nuxt conventions. Unblocks all page content.
-3. **Page migrations** (Home, Projects, Project Detail, About, Contact, Fiverr, Formation) — Migrate one page at a time with Nuxt UI v3 components replacing custom ones.
-4. **Contact form** — New feature, not a migration. Add EmailJS + UForm + useToast after pages are stable.
-5. **SEO + sitemap** — Add after pages exist; useSeoMeta() per page, sitemap module, JSON-LD.
-6. **Performance polish** — NuxtImg, font preloads, GA4 via nuxt-gtag, Docker production build.
-
-Defer:
-- Formation page: low traffic value; migrate last
-- Fiverr page: secondary conversion path; migrate after core pages
-- Testimonials stats: nice-to-have; fold into About or Home as a section
-
----
-
-## Sources
-
-- Training knowledge: Nuxt UI v3 component API (alpha/beta period, up to Aug 2025) — MEDIUM confidence
-- Training knowledge: Nuxt 4 release features, `@nuxtjs/i18n` v9, `@nuxtjs/color-mode`, `@nuxt/image` — MEDIUM confidence
-- Direct codebase analysis: `src/views/`, `src/components/` in this repository — HIGH confidence
-- PROJECT.md constraints and out-of-scope declarations — HIGH confidence
-
-**Validate before building:** Confirm `UCarousel`, `UDrawer`, `UNavigationMenu`, `UAccordion`, `UForm`/`UFormField` names against current ui.nuxt.com/components — component names may have changed between v3 beta and v3 stable.
diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md
deleted file mode 100644
index ec597fd..0000000
--- a/.planning/research/PITFALLS.md
+++ /dev/null
@@ -1,285 +0,0 @@
-# Domain Pitfalls — Vue 3 SPA → Nuxt 4 SSR Migration
-
-**Domain:** Portfolio SSR migration (Nuxt 4 + Nuxt UI v3 + @nuxtjs/i18n + @nuxtjs/color-mode)
-**Researched:** 2026-04-07
-**Confidence:** MEDIUM (training data + ecosystem knowledge as of Aug 2025; web access unavailable for live verification)
-
----
-
-## Critical Pitfalls
-
-Mistakes that cause rewrites or block SSR from working correctly.
-
----
-
-### Pitfall 1: Hydration Mismatch from localStorage-based State
-
-**What goes wrong:** The existing SPA uses `localStorage` for both locale and theme persistence. During SSR, the server renders with the default locale/theme (server has no access to localStorage). The client then reads localStorage and switches — causing a visible flash and a Vue hydration warning (`[Vue warn]: Hydration mismatch`). In strict hydration mode (Nuxt 4 default), this can throw an error, not just a warning.
-
-**Why it happens:** `localStorage` is a browser-only API. The server renders `lang="fr"` but the client's stored preference is `lang="en"`. The DOM differs between server render and client mount.
-
-**Consequences:**
-- FOUC (Flash of Unstyled Content) for dark mode
-- Wrong locale briefly visible on first paint
-- Hydration errors that can fully break page interactivity in strict mode
-- SEO crawlers see the default locale/theme, not the user's preference (but this is acceptable for SEO)
-
-**Prevention:**
-- Replace ALL `localStorage` reads with cookie reads — cookies are sent with every HTTP request, so the server can read them via `useCookie()` during SSR
-- For locale: configure `@nuxtjs/i18n` with `detectBrowserLanguage: { useCookie: true, cookieKey: 'i18n_locale' }`
-- For theme: configure `@nuxtjs/color-mode` with `storageKey: 'color-mode'` and ensure it uses its built-in cookie strategy (it does by default in SSR mode)
-- Never call `localStorage` directly in composables that run during SSR — wrap in `if (import.meta.client)` or `onMounted()`
-
-**Detection:** Run `nuxt build && nuxt preview`, open DevTools Console — any `[Vue warn]: Hydration` message is a failure.
-
-**Phase:** Foundation setup (Phase 1 — before any page migration)
-
----
-
-### Pitfall 2: @nuxtjs/i18n v9 Breaking Config Changes
-
-**What goes wrong:** `@nuxtjs/i18n` v9 (required for Nuxt 4) has significant breaking changes from v8. The `vueI18n` config file path changed, `strategy: 'no_prefix'` behavior changed, and the `detectBrowserLanguage` defaults changed. Copying the old config causes silent failures where locale switching appears to work client-side but SSR always renders the default locale.
-
-**Why it happens:** i18n v9 moved to a Nuxt-native config approach; the old `vueI18n.config.ts` file requires an explicit `vueI18n` option pointing to it. Without this, messages load client-side only (from the bundle) but are missing during SSR rendering.
-
-**Consequences:**
-- Server renders untranslated keys (e.g. `"page.hero.title"`) instead of translated text
-- SEO crawlers index translation keys, not content
-- Locale cookies set but not respected on server
-
-**Prevention:**
-- Set `vueI18n: './i18n.config.ts'` explicitly in `nuxt.config.ts`
-- Use `lazy: true` with `langDir` for large translation files, but test SSR with `nuxt preview` (not `nuxt dev`) since lazy loading behaves differently
-- Set `strategy: 'no_prefix'` only if both FR and EN share the same URL structure — verify the SEO implication (Google prefers `hreflang` differentiation)
-- Test locale detection server-side: curl the deployed URL with `Cookie: i18n_locale=en` and verify English content is in the HTML response
-
-**Detection:** `curl -H "Cookie: i18n_locale=en" http://localhost:3000/ | grep -i "hero"` — if French text appears, SSR locale is broken.
-
-**Phase:** Foundation setup (Phase 1)
-
----
-
-### Pitfall 3: @nuxtjs/color-mode FOUC Despite Cookie Strategy
-
-**What goes wrong:** Even with `@nuxtjs/color-mode` configured for cookie storage, a FOUC (Flash of Unstyled Content / Flash of Wrong Theme) can still occur. This happens when Tailwind CSS v4 dark mode is configured as `class`-based but the `` class is added after hydration rather than during SSR.
-
-**Why it happens:** `@nuxtjs/color-mode` adds the color mode class to `` via a server-side plugin. If the plugin runs after the initial HTML render, or if Tailwind's dark variant is not using `darkMode: 'class'` correctly, the flash occurs. A common mistake is having `darkMode: 'media'` in Tailwind config while `@nuxtjs/color-mode` controls a class — these conflict.
-
-**Consequences:**
-- White flash when loading dark mode (or vice versa)
-- User sees theme switch on every page load
-- Poor perceived performance
-
-**Prevention:**
-- In `tailwind.config.ts` (or `@import "tailwindcss"` in CSS for v4), ensure dark mode variant matches `@nuxtjs/color-mode`'s `classSuffix: ''` and `classPrefix: ''` settings
-- In Nuxt UI v3 + Tailwind v4, dark mode is configured via CSS `@variant dark (.dark &)` — verify this aligns with the class `color-mode` adds (`dark` not `dark-mode`)
-- Set `colorMode.preference: 'system'` as fallback but ensure the cookie override takes precedence
-- Test with Network throttling (Slow 3G) in DevTools — FOUC is most visible on slow connections
-
-**Detection:** Record a screen capture of first page load with DevTools CPU 6x throttle. Any flash = FOUC present.
-
-**Phase:** Foundation setup (Phase 1) — must be validated before any page migration
-
----
-
-### Pitfall 4: Nuxt 4 `app/` Directory Structure — Component/Composable Resolution
-
-**What goes wrong:** Nuxt 4 changes the default directory layout. The `srcDir` default is now `app/` instead of the root. Components placed in `components/`, composables in `composables/`, and pages in `pages/` at the root level are NOT auto-imported in Nuxt 4 if you've opted into the new directory structure.
-
-**Why it happens:** Nuxt 4 introduces `app/` as the application root (analogous to how Next.js uses `app/`). Migrating without updating import paths or without setting `future.compatibilityVersion: 4` in nuxt.config causes either broken auto-imports or requires manual path updates.
-
-**Consequences:**
-- Components silently fail to auto-import → runtime errors
-- Composables work in dev (because Nuxt falls back) but fail in production build
-- Hours debugging "component not found" errors
-
-**Prevention:**
-- Decide upfront: use new `app/` structure (recommended) or stay at root with explicit `srcDir: '.'`
-- If using `app/`: move `components/`, `composables/`, `pages/`, `layouts/`, `middleware/`, `plugins/` into `app/`
-- `server/` stays at root (it's a Nitro convention, not a Nuxt app convention)
-- `public/` stays at root
-- `nuxt.config.ts`, `package.json` stay at root
-- Validate with `nuxt info` — it shows resolved directories
-
-**Detection:** Run `nuxt build` and check for "Auto-imported composable used but not resolved" warnings.
-
-**Phase:** Foundation setup (Phase 1 — structural decision before any files are created)
-
----
-
-### Pitfall 5: Nuxt UI v3 + Tailwind CSS v4 Configuration Conflicts
-
-**What goes wrong:** Nuxt UI v3 ships its own Tailwind CSS v4 preset and expects to control the Tailwind configuration via its module. Manually adding a `tailwind.config.ts` alongside Nuxt UI v3 can cause duplicate utility class generation, broken component styles, or the Nuxt UI design tokens being overridden.
-
-**Why it happens:** Tailwind CSS v4 moved from `tailwind.config.js` to CSS-based configuration (`@import "tailwindcss"` + `@theme`). Nuxt UI v3 injects its theme tokens via this CSS mechanism. If the developer also adds a separate Tailwind config file, the two configurations merge unpredictably.
-
-**Consequences:**
-- Nuxt UI components render without their default styles
-- Custom colors/spacing defined in config override Nuxt UI tokens instead of extending them
-- `@apply` directives fail silently in production (different behavior than dev)
-
-**Prevention:**
-- Do NOT create a standalone `tailwind.config.ts` with Nuxt UI v3 — extend via `app.config.ts` using Nuxt UI's theme API instead
-- For custom colors: use `ui.colors` in `app.config.ts`, not raw Tailwind config
-- For custom CSS utilities: add them to `assets/css/main.css` using Tailwind v4's `@layer utilities` syntax
-- Read Nuxt UI v3 theming docs before writing any custom styles
-
-**Detection:** After setup, run `nuxt dev` and inspect a `UButton` — if it renders unstyled, Tailwind integration is broken.
-
-**Phase:** Foundation setup (Phase 1)
-
----
-
-## Moderate Pitfalls
-
----
-
-### Pitfall 6: `useAsyncData` Key Collisions Across Pages
-
-**What goes wrong:** Multiple pages call `useAsyncData('projects', ...)` with the same key. In SSR, Nuxt caches `useAsyncData` results by key in the payload. If two different pages use the same key for different data shapes, one page gets the other's cached data.
-
-**Why it happens:** Copy-paste of composable calls without updating the key. This is a regression from SPA behavior — in a SPA, `useAsyncData` re-executes on every navigation; in SSR, the payload cache can serve stale data.
-
-**Prevention:**
-- Use route-scoped keys: `useAsyncData(\`project-\${slug}\`, ...)` for detail pages
-- For shared data (projects list): use a single composable (`useProjects`) that internally calls `useAsyncData('projects-list', ...)` — single definition, consistent key
-- Audit all `useAsyncData` calls and ensure every key is unique across the app
-
-**Detection:** Navigate between two pages that use the same key and check if data bleeds across.
-
-**Phase:** Data migration (composables phase)
-
----
-
-### Pitfall 7: EmailJS Client-Only Execution in SSR Context
-
-**What goes wrong:** EmailJS is a browser SDK (`emailjs-com` or `@emailjs/browser`). If the contact form composable calls `emailjs.sendForm()` in a context that can run server-side, the build will fail or throw a `window is not defined` error.
-
-**Why it happens:** SSR executes component setup code on the server. Any direct `import emailjs from '@emailjs/browser'` at module level that accesses browser globals during import causes the server render to crash.
-
-**Prevention:**
-- Use `import.meta.client` guard: only call emailjs inside `onMounted` or an event handler (not in `setup()` body)
-- Alternatively, use `nuxt-only` dynamic import: `const emailjs = await import('@emailjs/browser')` inside the submit handler
-- Never call `emailjs.init()` at the top level of a composable — defer to client-side execution
-
-**Detection:** Run `nuxt build && nuxt preview`, submit the contact form — if it throws, check server logs for `window is not defined`.
-
-**Phase:** Contact page migration
-
----
-
-### Pitfall 8: `useSeoMeta()` Duplicate Meta Tags
-
-**What goes wrong:** The `useSeoMeta()` composable in Nuxt merges meta tags reactively. If a base `useSeoMeta()` call is in `app.vue` AND a route-level call is in a page component, the page-level tags should override the base — but some tags (especially `og:image`) may render twice if not using the `key` deduplication mechanism.
-
-**Why it happens:** Nuxt uses Unhead under the hood. Without explicit `key` on duplicate meta entries, Unhead appends rather than replaces.
-
-**Prevention:**
-- Define global defaults in `app.vue` using `useHead()` or `useSeoMeta()` with low-priority defaults
-- Override at page level — Nuxt/Unhead will deduplicate by meta `name`/`property` automatically for standard tags
-- For `og:image`: provide absolute URLs (not relative paths) — relative paths resolve to `null` in SSR and crawlers see empty og:image
-- Test with `curl http://localhost:3000/ | grep -i "og:image"` — count occurrences
-
-**Detection:** View source of any page and check for duplicate ` ` tags.
-
-**Phase:** SEO implementation phase
-
----
-
-### Pitfall 9: NuxtImg with External/Dynamic Image Sources
-
-**What goes wrong:** `` with `provider: 'ipx'` (default) works fine for local images in `public/`. For images with dynamic URLs (e.g. project thumbnails loaded from a data array with paths like `/images/projects/foo.jpg`), the image optimization pipeline may fail silently in Docker if the IPX cache directory is not writable.
-
-**Why it happens:** IPX (the image optimization engine) writes optimized images to `.nuxt/image-cache/` or a configurable dir. In Docker containers running as non-root, this directory may not be writable. The request falls through to the original image without optimization — no error, just no optimization.
-
-**Prevention:**
-- In Dockerfile: ensure the working directory and `.nuxt/` subdirectories are owned by the runtime user
-- Or: use `sharp` provider with pre-optimized images at build time (simpler for static images)
-- For a portfolio with static project images: consider running `nuxt generate` (SSG) instead of SSR — eliminates the runtime IPX issue entirely
-- Test Docker image locally: `docker run --rm -p 3000:3000 portfolio-image` and verify images load optimized (check response headers for `Content-Type: image/webp`)
-
-**Detection:** After Docker deploy, check Network tab — if project images are served as `image/jpeg` instead of `image/webp`, IPX optimization is failing.
-
-**Phase:** Docker/deployment phase
-
----
-
-### Pitfall 10: SSG vs SSR Decision — Critical for Docker Strategy
-
-**What goes wrong:** The project currently deploys as static files served by nginx. A direct "lift and shift" to Nuxt 4 with `nuxt build` (SSR) requires a Node.js runtime in Docker — fundamentally different from the current nginx static serving. Teams often start with SSR, hit Docker complexity, then realize their portfolio (fully static content, no user-specific server responses) could just use `nuxt generate` (SSG).
-
-**Why it happens:** "SSR" is the stated goal, but for a portfolio with static content, SSG provides identical SEO benefits with zero runtime complexity. The decision is deferred until deployment, causing wasted work.
-
-**Consequences:**
-- Docker image is 10x larger (Node.js runtime vs static files)
-- Cold starts on container restarts
-- Memory management overhead
-- Unnecessary complexity for static content
-
-**Prevention:**
-- Decide SSR vs SSG in Phase 1, not Phase N
-- SSG (`nuxt generate`) is appropriate if: no user-specific server responses, no server-side auth, no real-time data
-- For this portfolio: SSG is almost certainly sufficient — locale/theme from cookies still works client-side after hydration, and pre-rendered HTML satisfies SEO
-- If SSG: keep nginx in Docker, only Nuxt is involved at build time, not runtime
-
-**Detection:** List every route — does any route return different HTML based on the authenticated user or real-time data? If no → SSG is viable.
-
-**Phase:** Phase 1 decision, before any implementation
-
----
-
-## Minor Pitfalls
-
----
-
-### Pitfall 11: `definePageMeta()` Not Respected in Certain Nuxt 4 Contexts
-
-**What goes wrong:** `definePageMeta({ layout: 'default' })` is a compile-time macro in Nuxt 4. Using it inside a `