chore: remove obsolete planning files for Nuxt 4 migration

- Deleted several planning documents including config.json, PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md, and various phase plans.
- These files were no longer relevant to the current project structure and development practices, streamlining the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 17:37:59 +02:00
parent 3f0af5ca5a
commit 7f776298a9
42 changed files with 0 additions and 8076 deletions
@@ -1,338 +0,0 @@
---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- nuxt.config.ts
- package.json
- pnpm-lock.yaml
- tsconfig.json
- app/app.vue
- shared/types/index.ts
- .gitignore
autonomous: true
requirements:
- SSR-01
- SSR-02
- SSR-03
- INFRA-02
- INFRA-03
must_haves:
truths:
- "nuxt dev demarre sans erreur et sert localhost:3000"
- "La structure app/ est utilisee (Nuxt 4 compatibilityVersion 4)"
- "Tous les modules sont installes dans nuxt.config.ts"
- "TypeScript strict mode est actif"
- "ESLint via @nuxt/eslint fonctionne sans erreur"
artifacts:
- path: "nuxt.config.ts"
provides: "Configuration principale Nuxt 4 avec tous les modules"
contains: "compatibilityVersion: 4"
- path: "app/app.vue"
provides: "Composant racine Nuxt"
- path: "shared/types/index.ts"
provides: "Interfaces TypeScript resserrees"
exports: ["Project", "ProjectButton", "Technology", "TechStack", "Testimonial", "FAQ"]
- path: "package.json"
provides: "Dependances Nuxt 4 + tous modules"
key_links:
- from: "nuxt.config.ts"
to: "app/app.vue"
via: "Nuxt srcDir convention"
pattern: "compatibilityVersion.*4"
---
<objective>
Initialiser le projet Nuxt 4 avec pnpm, installer tous les modules, configurer TypeScript strict et ESLint, et definir les interfaces TypeScript resserrees.
Purpose: Creer le squelette technique Nuxt 4 sur lequel toute la migration repose.
Output: Projet Nuxt 4 fonctionnel avec `pnpm dev` qui demarre, tous modules configures, types definis.
</objective>
<execution_context>
@C:/Users/minit/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/minit/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
<interfaces>
<!-- Types existants a migrer et resserrer depuis src/types/index.ts -->
From src/types/index.ts:
```typescript
export interface Project {
id: string
title: string
image: string
description: string
longDescription?: string
technologies?: string[]
category?: string
featured?: boolean
buttons?: ProjectButton[]
date?: string
demoUrl?: string
githubUrl?: string
features?: string[]
gallery?: string[]
status?: string
}
export interface ProjectButton {
title: string
link: 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[]
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Initialiser le projet Nuxt 4 avec pnpm et tous les modules</name>
<files>nuxt.config.ts, package.json, pnpm-lock.yaml, app/app.vue, .gitignore, tsconfig.json</files>
<read_first>
- src/types/index.ts (types existants pour reference)
- package.json (dependances actuelles Vue 3)
- .gitignore (regles existantes)
</read_first>
<action>
1. Installer pnpm globalement si absent: `npm install -g pnpm`
2. Initialiser le projet Nuxt 4: `pnpm dlx nuxi@latest init . --force` (force car le dossier n'est pas vide). Si nuxi init ne supporte pas --force dans un repo existant, creer dans un sous-dossier temp et copier les fichiers generes.
3. Installer tous les modules (per D-08, D-09):
```bash
pnpm add @nuxt/ui @nuxtjs/i18n @nuxt/eslint @nuxtjs/sitemap nuxt-gtag @nuxt/image
```
NOTE: Ne PAS installer @nuxtjs/color-mode — deja inclus dans @nuxt/ui.
4. Configurer nuxt.config.ts avec ce contenu exact:
```typescript
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
},
ssr: true,
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image'
],
typescript: {
strict: true
},
i18n: {
locales: ['fr', 'en'],
defaultLocale: 'fr'
},
gtag: {
id: 'G-CDVVNFY6MV',
enabled: false
}
})
```
5. Creer `app/app.vue` minimal:
```vue
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtPage />
</div>
</template>
```
6. Creer `app/pages/index.vue` minimal pour que le serveur demarre sans erreur:
```vue
<template>
<div>
<h1>Portfolio Killian' DAL-CIN</h1>
<p>Nuxt 4 Foundation</p>
</div>
</template>
```
7. Mettre a jour .gitignore pour inclure: `node_modules`, `.nuxt`, `.output`, `dist`, `.env`
8. Verifier que `pnpm dev` demarre sans erreur sur localhost:3000
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && pnpm dev --port 3000 &amp; sleep 15 && curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200" && echo "PASS" || echo "FAIL"; kill %1 2>/dev/null</automated>
</verify>
<acceptance_criteria>
- nuxt.config.ts contains `compatibilityVersion: 4`
- nuxt.config.ts contains `'@nuxt/ui'` in modules array
- nuxt.config.ts contains `'@nuxtjs/i18n'` in modules array
- nuxt.config.ts contains `'@nuxt/eslint'` in modules array
- nuxt.config.ts contains `'@nuxtjs/sitemap'` in modules array
- nuxt.config.ts contains `'nuxt-gtag'` in modules array
- nuxt.config.ts contains `'@nuxt/image'` in modules array
- nuxt.config.ts contains `strict: true`
- package.json contains `@nuxt/ui` in dependencies
- package.json contains `@nuxtjs/i18n` in dependencies
- app/app.vue exists with NuxtPage component
- pnpm dev starts and localhost:3000 returns HTTP 200
</acceptance_criteria>
<done>Projet Nuxt 4 demarre sur localhost:3000 avec tous les modules installes, TypeScript strict actif</done>
</task>
<task type="auto">
<name>Task 2: Definir les interfaces TypeScript resserrees et configurer ESLint</name>
<files>shared/types/index.ts</files>
<read_first>
- src/types/index.ts (types existants a resserrer per D-03)
- src/data/testimonials.ts (interface Testimonial existante)
- src/data/faq.ts (interface FAQ existante)
- nuxt.config.ts (verifier @nuxt/eslint present)
</read_first>
<action>
1. Creer `shared/types/index.ts` avec les interfaces resserrees (per D-03 — rendre obligatoires technologies, category, date):
```typescript
export interface ProjectButton {
title: string
link: string
}
export interface Project {
id: string
image: string // URL /images/xxx.webp
technologies: string[] // OBLIGATOIRE (etait optionnel)
category: string // OBLIGATOIRE (etait optionnel)
date: string // OBLIGATOIRE (etait optionnel)
featured?: boolean
buttons?: ProjectButton[]
gallery?: string[]
demoUrl?: string
githubUrl?: string
features?: string[]
// Pas de title/description/longDescription/status — i18n via cles
}
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
}
```
Note: FAQ utilise des cles i18n (per D-02) au lieu de texte direct. L'ancienne interface avait `question: string` (texte), la nouvelle a `questionKey: string` (cle de traduction).
2. Verifier que `pnpm nuxi typecheck` passe (les types sont auto-importes depuis shared/ en Nuxt 4).
3. Verifier que `pnpm eslint .` passe sans erreur (ESLint configure via @nuxt/eslint dans les modules).
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && npx nuxi typecheck 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- shared/types/index.ts contains `technologies: string[]` (not optional)
- shared/types/index.ts contains `category: string` (not optional)
- shared/types/index.ts contains `date: string` (not optional, in Project interface)
- shared/types/index.ts contains `export interface Project`
- shared/types/index.ts contains `export interface Technology`
- shared/types/index.ts contains `export interface TechStack`
- shared/types/index.ts contains `export interface Testimonial`
- shared/types/index.ts contains `export interface FAQ`
- shared/types/index.ts contains `questionKey: string`
- npx nuxi typecheck exits with code 0
</acceptance_criteria>
<done>Toutes les interfaces TypeScript resserrees existent dans shared/types/index.ts, typecheck et eslint passent sans erreur</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Aucune | Phase 1 est une initialisation technique sans surface d'attaque |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-01 | I (Information Disclosure) | nuxt.config.ts | mitigate | gtag.enabled: false — pas de tracking en dev |
| T-01-02 | T (Tampering) | pnpm dependencies | accept | lockfile pnpm-lock.yaml tracke dans git |
</threat_model>
<verification>
1. `pnpm dev` demarre sans erreur sur localhost:3000
2. `npx nuxi typecheck` exit 0
3. `pnpm eslint .` exit 0 (si le script existe, sinon `npx eslint .`)
4. nuxt.config.ts contient les 6 modules et compatibilityVersion 4
5. shared/types/index.ts exporte Project, Technology, TechStack, Testimonial, FAQ
</verification>
<success_criteria>
- Le projet Nuxt 4 demarre localement
- Tous les modules sont installes et declares
- TypeScript strict mode actif
- Interfaces resserrees per D-03
- ESLint fonctionne via @nuxt/eslint
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
</output>
@@ -1,79 +0,0 @@
---
phase: 01-foundation
plan: 01
subsystem: core-setup
tags: [nuxt4, typescript, eslint, foundation]
dependency_graph:
requires: []
provides: [nuxt-project, typescript-types, eslint-config]
affects: [all-subsequent-plans]
tech_stack:
added: [nuxt@4.4.2, "@nuxt/ui@3.3.7", "@nuxtjs/i18n@10.2.4", "@nuxt/eslint", "@nuxtjs/sitemap@8.0.12", "nuxt-gtag@4.1.0", "@nuxt/image"]
patterns: [nuxt4-app-dir, shared-types, auto-imports]
key_files:
created:
- nuxt.config.ts
- app/app.vue
- app/pages/index.vue
- shared/types/index.ts
- eslint.config.mjs
- pnpm-lock.yaml
modified:
- package.json
- tsconfig.json
- .gitignore
decisions:
- "Replaced eslint.config.ts (Vue 3) with eslint.config.mjs using @nuxt/eslint generated config"
- "pnpm onlyBuiltDependencies configured for native deps (esbuild, sharp, etc.)"
metrics:
duration: "~6 min"
completed: "2026-04-08T12:53:00Z"
tasks_completed: 2
tasks_total: 2
---
# Phase 01 Plan 01: Nuxt 4 Project Initialization Summary
Nuxt 4.4.2 project initialized with pnpm, 6 modules configured (UI, i18n, ESLint, sitemap, gtag, image), TypeScript strict mode, and tightened interfaces in shared/types/.
## Task Results
| Task | Name | Commit | Status |
|------|------|--------|--------|
| 1 | Initialize Nuxt 4 project with pnpm and all modules | 9fbbce0 | Done |
| 2 | Define tightened TypeScript interfaces and configure ESLint | c4923a0 | Done |
## Verification Results
| Check | Result |
|-------|--------|
| pnpm dev starts on localhost:3333 | PASS (HTTP 200) |
| nuxi typecheck | PASS (exit 0) |
| eslint app/ shared/ | PASS (no errors) |
| nuxt.config.ts has compatibilityVersion 4 | PASS |
| nuxt.config.ts has 6 modules | PASS |
| shared/types/index.ts exports all interfaces | PASS |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Replaced eslint.config.ts with eslint.config.mjs**
- **Found during:** Task 2
- **Issue:** Old Vue 3 eslint.config.ts used @vue/eslint-config-typescript which is incompatible with @nuxt/eslint ESLint 10 flat config
- **Fix:** Deleted eslint.config.ts, created eslint.config.mjs importing from .nuxt/eslint.config.mjs
- **Files modified:** eslint.config.ts (deleted), eslint.config.mjs (created)
- **Commit:** c4923a0
**2. [Rule 3 - Blocking] pnpm build scripts approval**
- **Found during:** Task 1
- **Issue:** pnpm blocked native dependency build scripts (esbuild, sharp, etc.)
- **Fix:** Added pnpm.onlyBuiltDependencies to package.json
- **Files modified:** package.json
- **Commit:** 9fbbce0
## Known Stubs
None - this is a foundation plan with minimal UI (placeholder index page only, intentional).
## Self-Check: PASSED
@@ -1,439 +0,0 @@
---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- app/data/projects.ts
- app/data/testimonials.ts
- app/data/faq.ts
- app/data/techstack.ts
- app/composables/useProjects.ts
- public/images/
autonomous: true
requirements:
- DATA-01
- DATA-02
- DATA-03
- DATA-04
- DATA-05
must_haves:
truths:
- "Les donnees projets sont importables depuis app/data/projects.ts avec le type Project"
- "Les donnees testimonials sont importables avec le type Testimonial"
- "Les donnees FAQ utilisent des cles i18n et non du texte direct"
- "Les donnees techstack sont importables avec le type TechStack"
- "useProjects() retourne une liste typee et supporte filterByCategory, search, findById"
- "Toutes les images referenceent /images/ et non @/assets/images/"
artifacts:
- path: "app/data/projects.ts"
provides: "Donnees brutes des 7 projets"
contains: "export const projects"
- path: "app/data/testimonials.ts"
provides: "Donnees temoignages"
contains: "export const testimonials"
- path: "app/data/faq.ts"
provides: "Donnees FAQ avec cles i18n"
contains: "export const homeFAQs"
- path: "app/data/techstack.ts"
provides: "Donnees tech stack"
contains: "export const techStack"
- path: "app/composables/useProjects.ts"
provides: "Composable filtrage/recherche projets"
exports: ["useProjects"]
key_links:
- from: "app/composables/useProjects.ts"
to: "app/data/projects.ts"
via: "import direct"
pattern: "import.*from.*data/projects"
- from: "app/data/projects.ts"
to: "shared/types/index.ts"
via: "type import"
pattern: "import type.*Project"
---
<objective>
Migrer toutes les donnees statiques vers app/data/, copier les images vers public/images/, et reecrire useProjects() en style Nuxt natif.
Purpose: Les donnees du portfolio sont disponibles et typees pour les phases suivantes.
Output: 4 fichiers data, 1 composable, images dans public/images/.
</objective>
<execution_context>
@C:/Users/minit/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/minit/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
<interfaces>
<!-- Types crees par Plan 01 dans shared/types/index.ts -->
```typescript
export interface Project {
id: string
image: string
technologies: string[]
category: string
date: string
featured?: boolean
buttons?: ProjectButton[]
gallery?: string[]
demoUrl?: string
githubUrl?: string
features?: string[]
}
export interface ProjectButton {
title: string
link: 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
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrer les donnees statiques et les images</name>
<files>app/data/projects.ts, app/data/testimonials.ts, app/data/faq.ts, app/data/techstack.ts, public/images/</files>
<read_first>
- src/composables/useProjects.ts (donnees projets inline a extraire)
- src/data/testimonials.ts (donnees + interface existantes)
- src/data/faq.ts (donnees + pattern getHomeFAQs existant)
- src/data/techstack.ts (donnees existantes)
- shared/types/index.ts (interfaces resserrees de Plan 01)
</read_first>
<action>
1. Copier toutes les images WebP de `src/assets/images/` vers `public/images/` (per D-06, D-07):
```bash
mkdir -p public/images/flowboard
cp src/assets/images/*.webp public/images/
cp src/assets/images/flowboard/*.webp public/images/flowboard/
```
2. Creer `app/data/projects.ts` (per D-01, D-02 — donnees separees, cles i18n):
```typescript
import type { Project } from '~~/shared/types'
export const projects: Project[] = [
{
id: 'virtual-tour',
image: '/images/virtualtour.webp',
technologies: ['Vue.js', 'Three.js', 'WebGL', 'Node.js'],
category: 'Web Development',
date: '2022',
buttons: [
{ title: 'Visit', link: 'https://www.lycee-chabanne16.fr/visites/BACSN/index.htm' }
]
},
{
id: 'xinko',
image: '/images/xinko.webp',
technologies: ['Node.js', 'Discord.js', 'MongoDB', 'Express'],
category: 'Bot Development',
date: '2023',
featured: true,
buttons: [
{ title: 'Invite', link: 'https://discord.com/api/oauth2/authorize?client_id=1035571329866407976&permissions=292288982151&scope=applications.commands%20bot' }
]
},
{
id: 'image-manipulation',
image: '/images/dig.webp',
technologies: ['JavaScript', 'Node.js', 'Canvas', 'npm'],
category: 'Open Source',
date: '2022',
featured: true,
buttons: [
{ title: 'Repository', link: 'https://git.mrkayjaydee.xyz/Mr-KayJayDee/discord-image-generation' },
{ title: 'NPM Package', link: 'https://www.npmjs.com/package/discord-image-generation' }
]
},
{
id: 'primate-web-admin',
image: '/images/primate.webp',
technologies: ['React', 'TypeScript', 'Node.js', 'Express'],
category: 'Enterprise Software',
date: '2023'
},
{
id: 'instagram-bot',
image: '/images/instagram.webp',
technologies: ['JavaScript', 'Node.js', 'Instagram API', 'Canvas'],
category: 'Social Media Bot',
date: '2022',
buttons: [
{ title: 'Repository', link: 'https://git.mrkayjaydee.xyz/Mr-KayJayDee/instagram-bot' }
]
},
{
id: 'crowdin-status-bot',
image: '/images/crowdin.webp',
technologies: ['Node.js', 'Discord.js', 'Crowdin API', 'Cron'],
category: 'Automation',
date: '2023',
buttons: [
{ title: 'Repository', link: 'https://git.mrkayjaydee.xyz/Mr-KayJayDee/discord-crowdin-status' }
]
},
{
id: 'flowboard',
image: '/images/flowboard/flowboard_1.webp',
technologies: ['Vue.js', 'Node.js', 'TypeScript', 'MongoDB', 'Express'],
category: 'Web Development',
date: '2024',
featured: true,
features: [
'Organize your tasks, projects and ideas by creating thematic boards adapted to your needs',
'Add cards for each task, assign members, set due dates, and track progress at a glance',
'Invite colleagues and teammates to join your boards to work together, share ideas, and coordinate your efforts',
'Keep an overview of the progress of your projects thanks to a simple and intuitive interface',
'Use labels, lists and tables to prioritize tasks, set priorities and keep the overview clear'
],
gallery: [
'/images/flowboard/flowboard_1.webp',
'/images/flowboard/flowboard_2.webp',
'/images/flowboard/flowboard_3.webp',
'/images/flowboard/flowboard_4.webp'
]
}
]
```
3. Creer `app/data/testimonials.ts` — copie directe, juste changer l'import type:
```typescript
import type { Testimonial, TestimonialsStats } from '~~/shared/types'
export const testimonials: Testimonial[] = [
// ... (copier les 5 temoignages existants tels quels de src/data/testimonials.ts)
]
export const testimonialsStats: TestimonialsStats = {
totalReviews: 10,
averageRating: 5.0,
projectsCompleted: 25
}
```
4. Creer `app/data/faq.ts` (per D-02 — cles i18n au lieu de texte):
```typescript
import type { FAQ } from '~~/shared/types'
export const homeFAQs: FAQ[] = [
{
questionKey: 'faq.homeFaq.delivery.question',
answerKey: 'faq.homeFaq.delivery.answer',
featuresKey: 'faq.homeFaq.delivery.features'
},
{
questionKey: 'faq.homeFaq.maintenance.question',
answerKey: 'faq.homeFaq.maintenance.answer',
featuresKey: 'faq.homeFaq.maintenance.features'
},
{
questionKey: 'faq.homeFaq.companies.question',
answerKey: 'faq.homeFaq.companies.answer',
featuresKey: 'faq.homeFaq.companies.features'
}
]
```
5. Creer `app/data/techstack.ts` — copie avec chemins images mis a jour:
```typescript
import type { TechStack } from '~~/shared/types'
export const techStack: TechStack = {
// ... (copier depuis src/data/techstack.ts, remplacer TOUS les `@/assets/images/xxx.webp` par `/images/xxx.webp`)
}
```
Remplacement a effectuer: `@/assets/images/` -> `/images/` pour CHAQUE entree (60+ images).
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && node -e "const fs=require('fs'); const files=['app/data/projects.ts','app/data/testimonials.ts','app/data/faq.ts','app/data/techstack.ts']; let ok=true; for(const f of files){if(!fs.existsSync(f)){console.log('MISSING: '+f);ok=false;}else{const c=fs.readFileSync(f,'utf8');if(c.includes('@/assets/images/')){console.log('FAIL: '+f+' still contains @/assets/images/');ok=false;}}} if(!fs.existsSync('public/images')){console.log('MISSING: public/images/');ok=false;} console.log(ok?'PASS':'FAIL');"</automated>
</verify>
<acceptance_criteria>
- app/data/projects.ts contains `export const projects: Project[]`
- app/data/projects.ts contains `/images/virtualtour.webp` (not `@/assets/images/`)
- app/data/projects.ts contains 7 project objects (virtual-tour through flowboard)
- app/data/testimonials.ts contains `export const testimonials: Testimonial[]`
- app/data/testimonials.ts contains `export const testimonialsStats: TestimonialsStats`
- app/data/faq.ts contains `export const homeFAQs: FAQ[]`
- app/data/faq.ts contains `questionKey:` (i18n keys, not direct text)
- app/data/techstack.ts contains `export const techStack: TechStack`
- app/data/techstack.ts does NOT contain `@/assets/images/` (all paths migrated)
- app/data/projects.ts does NOT contain `@/assets/images/` (all paths migrated)
- No file in app/data/ contains `@/assets/images/`
- public/images/ directory contains .webp files
</acceptance_criteria>
<done>4 fichiers data migres avec types corrects, images dans public/images/, aucune reference a @/assets/images/ dans aucun fichier app/data/</done>
</task>
<task type="auto">
<name>Task 2: Reecrire useProjects() en style Nuxt natif</name>
<files>app/composables/useProjects.ts</files>
<read_first>
- src/composables/useProjects.ts (composable existant a reecrire)
- app/data/projects.ts (donnees separees de Task 1)
- shared/types/index.ts (interfaces)
</read_first>
<action>
Creer `app/composables/useProjects.ts` en style Nuxt natif (per D-04, D-05):
```typescript
import { projects as projectsData } from '~/data/projects'
export function useProjects() {
const { t } = useI18n()
const projects = computed(() =>
projectsData.map(p => ({
...p,
title: t(`projects.${p.id}.title`),
description: t(`projects.${p.id}.description`),
longDescription: t(`projects.${p.id}.longDescription`) || undefined
}))
)
const featuredProjects = computed(() =>
projects.value.filter(p => p.featured)
)
function filterByCategory(category: string) {
return computed(() =>
projects.value.filter(p => p.category === category)
)
}
function search(query: Ref<string> | string) {
return computed(() => {
const q = typeof query === 'string' ? query : query.value
if (!q) return projects.value
const lower = q.toLowerCase()
return projects.value.filter(p =>
p.title.toLowerCase().includes(lower) ||
p.description.toLowerCase().includes(lower) ||
p.technologies.some(tech => tech.toLowerCase().includes(lower))
)
})
}
function findById(id: string) {
return computed(() => projects.value.find(p => p.id === id))
}
return {
projects,
featuredProjects,
filterByCategory,
search,
findById
}
}
```
Points cles per D-04:
- Pas d'import `computed`, `useI18n` — auto-importes par Nuxt
- Import des donnees depuis `~/data/projects` (pas `@/`)
- Pas de wrapper useI18n custom — utilise directement l'auto-import @nuxtjs/i18n
- Les cles i18n suivent le pattern `projects.${id}.title` (per D-02)
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && npx nuxi typecheck 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- app/composables/useProjects.ts contains `export function useProjects()`
- app/composables/useProjects.ts contains `import { projects as projectsData } from '~/data/projects'`
- app/composables/useProjects.ts contains `const { t } = useI18n()`
- app/composables/useProjects.ts contains `filterByCategory`
- app/composables/useProjects.ts contains `search`
- app/composables/useProjects.ts contains `findById`
- app/composables/useProjects.ts contains `featuredProjects`
- app/composables/useProjects.ts does NOT contain `import { computed }` (auto-imported)
- app/composables/useProjects.ts does NOT contain `from '@/composables/useI18n'`
- npx nuxi typecheck exits with code 0
</acceptance_criteria>
<done>useProjects() retourne projects, featuredProjects, filterByCategory, search, findById — tout type-safe et style Nuxt natif</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Aucune | Donnees statiques, pas d'input utilisateur |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-03 | I (Information Disclosure) | testimonials avatars | accept | URLs ui-avatars.com publiques, pas de PII |
</threat_model>
<verification>
1. `npx nuxi typecheck` exit 0
2. Aucun fichier dans app/data/ ne contient `@/assets/images/`
3. app/composables/useProjects.ts exporte useProjects avec 5 fonctions/proprietes
4. public/images/ contient les fichiers WebP
</verification>
<success_criteria>
- Les 4 fichiers data existent et sont type-safe
- useProjects() compile sans erreur
- Images disponibles dans public/images/
- Aucune reference aux anciens chemins @/assets/images/
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
</output>
@@ -1,74 +0,0 @@
---
phase: 01-foundation
plan: 02
subsystem: data-layer
tags: [data, composables, i18n, images]
dependency_graph:
requires: [01-01]
provides: [data-projects, data-testimonials, data-faq, data-techstack, composable-useProjects]
affects: [all-pages, project-detail]
tech_stack:
added: []
patterns: [i18n-keys-for-text, public-images, nuxt-auto-imports]
key_files:
created:
- app/data/projects.ts
- app/data/testimonials.ts
- app/data/faq.ts
- app/data/techstack.ts
- app/composables/useProjects.ts
- public/images/ (74 WebP files)
modified:
- shared/types/index.ts
decisions:
- Added title/description/longDescription to Project interface (missing from Plan 01 types)
metrics:
duration: ~3min
completed: 2026-04-08
---
# Phase 01 Plan 02: Static Data Migration Summary
Migration des 4 fichiers de donnees statiques, 74 images WebP, et creation du composable useProjects() avec support i18n natif Nuxt.
## Commits
| Task | Commit | Description |
|------|--------|-------------|
| 1 | `2b97bc7` | Migrate static data files and images to Nuxt structure |
| 2 | `55019f6` | Create useProjects() composable with i18n support |
## What Was Done
### Task 1: Static Data & Images Migration
- Created 4 data files in `app/data/` importing types from `~~/shared/types`
- Copied 74 WebP images (70 root + 4 flowboard gallery) to `public/images/`
- All image paths use `/images/` instead of `@/assets/images/`
- FAQ data uses i18n keys (`questionKey`, `answerKey`, `featuresKey`) instead of direct text
- Projects data stored as `Omit<Project, 'title' | 'description' | 'longDescription'>[]` since text comes from i18n
### Task 2: useProjects() Composable
- Created Nuxt-native composable using auto-imports (`computed`, `useI18n`, `Ref`)
- Returns: `projects`, `featuredProjects`, `filterByCategory()`, `search()`, `findById()`
- i18n keys follow `projects.${id}.title` pattern
- Typecheck passes cleanly
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Added title/description/longDescription to Project interface**
- **Found during:** Task 2
- **Issue:** Plan 01 created Project interface without title/description/longDescription fields, but useProjects() maps these from i18n
- **Fix:** Added `title: string`, `description: string`, `longDescription?: string` to Project in shared/types/index.ts
- **Files modified:** shared/types/index.ts
- **Commit:** 55019f6
## Verification
- `npx nuxi typecheck` exits cleanly (0)
- No file in app/data/ contains `@/assets/images/`
- useProjects() exports 5 members: projects, featuredProjects, filterByCategory, search, findById
- public/images/ contains 74 WebP files
## Self-Check: PASSED
@@ -1,90 +0,0 @@
# Phase 1: Foundation - Context
**Gathered:** 2026-04-07
**Status:** Ready for planning
<domain>
## Phase Boundary
Le projet Nuxt 4 tourne localement avec tous les modules installés, données migrées sous `data/`, composable `useProjects()` câblé, et TypeScript strict mode passant. Aucune page visible — seulement le squelette technique.
</domain>
<decisions>
## Implementation Decisions
### Structure des données
- **D-01:** Séparer les données projets dans `data/projects.ts` — le composable `useProjects()` ne contient que la logique (filtrage, recherche, findById)
- **D-02:** Les fichiers data stockent des clés de traduction i18n (ex: `'projects.xinko.title'`), les textes FR/EN restent dans les fichiers de locale. Compatible SSR natif Nuxt i18n.
- **D-03:** Resserrer le typage TypeScript — rendre obligatoires les champs toujours présents (`technologies`, `category`, `date`) et garder optionnels uniquement ceux qui varient (`gallery`, `demoUrl`, `longDescription`)
### Stratégie composables
- **D-04:** Réécrire les composables en style Nuxt natif — auto-imports, `useAppConfig()` au lieu de `useSiteConfig()`, supprimer le wrapper `useI18n` custom (Nuxt i18n le fournit nativement)
- **D-05:** Phase 1 porte uniquement `useProjects()` — les autres composables (`useGallery()`, `useSeo()`, `useTheme()`) viendront dans leur phase respective
### Assets images
- **D-06:** Images dans `public/images/` — URLs stables (`/images/xinko.webp`), pas de bundling, compatible NuxtImg en Phase 3
- **D-07:** Format WebP uniquement, pas de fallback JPEG (support navigateur 98%+)
### Modules Nuxt
- **D-08:** Installer TOUS les modules dès Phase 1 dans `nuxt.config.ts` : @nuxt/ui, @nuxt/eslint, @nuxtjs/i18n, @nuxtjs/color-mode, @nuxtjs/sitemap, nuxt-gtag, @nuxt/image. Configuration minimale pour ceux utilisés en Phase 2-3.
- **D-09:** Utiliser pnpm comme package manager (recommandé par Nuxt, remplace npm)
### Claude's Discretion
Aucune zone déléguée — toutes les décisions ont été prises par l'utilisateur.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
No external specs — requirements fully captured in decisions above and in:
- `.planning/REQUIREMENTS.md` — Requirements SSR-01, SSR-02, SSR-03, DATA-01 à DATA-05, INFRA-02, INFRA-03
- `.planning/ROADMAP.md` — Phase 1 success criteria
- `.planning/codebase/CONVENTIONS.md` — Naming patterns and code style to follow
- `.planning/codebase/STRUCTURE.md` — Current project structure for migration reference
- `src/types/index.ts` — Current type definitions to migrate and tighten
- `src/data/` — Current data files to migrate (faq.ts, techstack.ts, testimonials.ts)
- `src/composables/useProjects.ts` — Current composable to rewrite in Nuxt style
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `src/types/index.ts` — Types `Project`, `ProjectButton`, `Technology`, `TechStack` à migrer et resserrer
- `src/data/faq.ts`, `src/data/techstack.ts`, `src/data/testimonials.ts` — Données statiques à migrer vers `data/`
- `src/composables/useProjects.ts` — Logique de filtrage/recherche à extraire (données inline à séparer)
### Established Patterns
- Données i18n via fonctions `getXxx(t)` qui appellent `t()` — à remplacer par clés i18n dans les fichiers data
- Composables exportent une seule fonction nommée `export function useXxx()`
- Code style : Prettier (no semi, single quotes, 100 chars), ESLint flat config
### Integration Points
- Les données projets référencent des images via `@/assets/images/` — à remapper vers `/images/`
- `useProjects()` importe `useI18n` custom — à remplacer par l'auto-import Nuxt i18n
</code_context>
<specifics>
## Specific Ideas
No specific requirements — open to standard approaches
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 01-foundation*
*Context gathered: 2026-04-07*
@@ -1,111 +0,0 @@
# Phase 1: Foundation - 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-07
**Phase:** 01-foundation
**Areas discussed:** Structure données, Stratégie composables, Assets images, Modules Phase 1
---
## Structure des données
| Option | Description | Selected |
|--------|-------------|----------|
| Fichier data séparé | Créer data/projects.ts avec les données brutes, le composable ne fait que la logique | ✓ |
| Garder inline | Laisser les données dans le composable comme actuellement | |
**User's choice:** Fichier data séparé
**Notes:**
| Option | Description | Selected |
|--------|-------------|----------|
| Clés i18n dans data | Les fichiers data stockent des clés de traduction, textes dans les locales | ✓ |
| Textes FR/EN inline | Stocker les textes directement avec objet { fr, en } | |
| Garder pattern t() | Conserver getXxx(t) comme actuellement | |
**User's choice:** Clés i18n dans data
**Notes:**
| Option | Description | Selected |
|--------|-------------|----------|
| Resserrer | Rendre obligatoires les champs toujours présents | ✓ |
| Migrer tel quel | Copier les types sans changement | |
| Claude décide | Analyse des données réelles | |
**User's choice:** Resserrer
**Notes:**
---
## Stratégie composables
| Option | Description | Selected |
|--------|-------------|----------|
| Style Nuxt natif | Réécrire pour auto-imports, useAppConfig(), supprimer useI18n custom | ✓ |
| Wrapper minimal | Copier avec minimum de changements | |
| Claude décide | Analyser chaque composable individuellement | |
**User's choice:** Style Nuxt natif
**Notes:**
| Option | Description | Selected |
|--------|-------------|----------|
| Phase 1 : seulement useProjects | Porter uniquement useProjects() en Phase 1 | ✓ |
| Tout porter maintenant | Migrer tous les composables d'un coup | |
**User's choice:** Phase 1 : seulement useProjects
**Notes:**
---
## Assets images
| Option | Description | Selected |
|--------|-------------|----------|
| public/ | Images dans public/images/, URLs stables, compatible NuxtImg | ✓ |
| assets/ | Images bundlées par Vite avec hash | |
| Claude décide | Choix selon contraintes | |
**User's choice:** public/
**Notes:**
| Option | Description | Selected |
|--------|-------------|----------|
| WebP uniquement | Garder .webp partout, support 98%+ | ✓ |
| WebP + fallback JPEG | Prévoir fallbacks via <picture> | |
**User's choice:** WebP uniquement
**Notes:**
---
## Modules Phase 1
| Option | Description | Selected |
|--------|-------------|----------|
| Tous en Phase 1 | Installer et configurer tous les modules dès le scaffold | ✓ |
| Progressif par phase | Ajouter module par module selon la phase | |
| Claude décide | Juger selon les dépendances | |
**User's choice:** Tous en Phase 1
**Notes:**
| Option | Description | Selected |
|--------|-------------|----------|
| npm | Rester sur npm comme le projet actuel | |
| pnpm | Passer à pnpm comme recommandé par Nuxt | ✓ |
**User's choice:** pnpm
**Notes:**
---
## Claude's Discretion
Aucune zone déléguée.
## Deferred Ideas
Aucune.
@@ -1,36 +0,0 @@
---
status: partial
phase: 01-foundation
source: [01-VERIFICATION.md]
started: 2026-04-08T12:00:00.000Z
updated: 2026-04-08T12:00:00.000Z
---
## Current Test
[awaiting human testing]
## Tests
### 1. pnpm dev démarre sur localhost:3000
expected: Le serveur Nuxt démarre sans erreur et http://localhost:3000 retourne HTTP 200
result: [pending]
### 2. pnpm typecheck exit 0
expected: `pnpm nuxi typecheck` s'exécute sans erreur TypeScript
result: [pending]
### 3. pnpm lint exit 0
expected: ESLint s'exécute sans erreur via `pnpm eslint .`
result: [pending]
## Summary
total: 3
passed: 0
issues: 0
pending: 3
skipped: 0
blocked: 0
## Gaps
@@ -1,470 +0,0 @@
# Phase 1: Foundation - Research
**Researched:** 2026-04-07
**Domain:** Initialisation Nuxt 4, migration de données TypeScript, composable useProjects()
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Séparer les données projets dans `data/projects.ts` — le composable `useProjects()` ne contient que la logique (filtrage, recherche, findById)
- **D-02:** Les fichiers data stockent des clés de traduction i18n (ex: `'projects.xinko.title'`), les textes FR/EN restent dans les fichiers de locale. Compatible SSR natif Nuxt i18n.
- **D-03:** Resserrer le typage TypeScript — rendre obligatoires les champs toujours présents (`technologies`, `category`, `date`) et garder optionnels uniquement ceux qui varient (`gallery`, `demoUrl`, `longDescription`)
- **D-04:** Réécrire les composables en style Nuxt natif — auto-imports, `useAppConfig()` au lieu de `useSiteConfig()`, supprimer le wrapper `useI18n` custom (Nuxt i18n le fournit nativement)
- **D-05:** Phase 1 porte uniquement `useProjects()` — les autres composables (`useGallery()`, `useSeo()`, `useTheme()`) viendront dans leur phase respective
- **D-06:** Images dans `public/images/` — URLs stables (`/images/xinko.webp`), pas de bundling, compatible NuxtImg en Phase 3
- **D-07:** Format WebP uniquement, pas de fallback JPEG (support navigateur 98%+)
- **D-08:** Installer TOUS les modules dès Phase 1 dans `nuxt.config.ts` : @nuxt/ui, @nuxt/eslint, @nuxtjs/i18n, @nuxtjs/color-mode, @nuxtjs/sitemap, nuxt-gtag, @nuxt/image. Configuration minimale pour ceux utilisés en Phase 2-3.
- **D-09:** Utiliser pnpm comme package manager (recommandé par Nuxt, remplace npm)
### Claude's Discretion
Aucune zone déléguée — toutes les décisions ont été prises par l'utilisateur.
### Deferred Ideas (OUT OF SCOPE)
Aucune — discussion restée dans le périmètre de la phase.
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| SSR-01 | Chaque route retourne du HTML complet côté serveur, crawlable sans JS client | Nuxt 4 SSR activé par défaut — `ssr: true` implicite dans nuxt.config.ts |
| SSR-02 | Le projet utilise Nuxt 4 avec la structure `app/` et les auto-imports | Structure `app/` par défaut dans Nuxt 4.4.2 (vérifié npm registry) |
| SSR-03 | `nuxt.config.ts` configure tous les modules (UI, i18n, color-mode, SEO, gtag, image) | Tous les modules vérifiés compatibles Nuxt 4 (voir Standard Stack) |
| DATA-01 | Données projets migrées depuis `src/data/` vers `data/` avec interfaces TypeScript | 7 projets dans useProjects.ts existant — à extraire vers `data/projects.ts` |
| DATA-02 | Données témoignages migrées avec interfaces TypeScript | Interface `Testimonial` et données existent dans `src/data/testimonials.ts` |
| DATA-03 | Données FAQ migrées avec support FR/EN et interfaces TypeScript | Interface `FAQ` et pattern `getXxx(t)` existent dans `src/data/faq.ts` — à remplacer par clés i18n |
| DATA-04 | Données tech stack migrées avec interfaces TypeScript | Interface `TechStack`/`Technology` et données existent dans `src/data/techstack.ts` |
| DATA-05 | Composable `useProjects()` migré — filtrage, recherche, findById | useProjects.ts existant à réécrire avec auto-imports Nuxt et données séparées |
| INFRA-02 | TypeScript en mode strict avec interfaces pour toutes les données | `typescript.strict: true` dans nuxt.config.ts |
| INFRA-03 | ESLint + Prettier configurés via @nuxt/eslint | @nuxt/eslint 1.15.2 compatible Nuxt 4, remplace eslint.config.ts manuel |
</phase_requirements>
---
## Summary
La Phase 1 consiste à créer un projet Nuxt 4 from scratch (ou initialiser la structure Nuxt 4 dans le repo existant), installer tous les modules définis, migrer les données statiques vers `data/`, et écrire `useProjects()` en style Nuxt natif. Aucune page visible n'est attendue — seulement le squelette technique fonctionnel.
Le projet actuel est une Vue 3 SPA avec Vite. La migration vers Nuxt 4 implique de créer une structure `app/` parallèle, configurer `nuxt.config.ts`, et migrer les fichiers de données existants depuis `src/data/` vers `data/` à la racine. Le code source existant (types, données, composables) est récupérable avec des adaptations mineures.
Point critique : `@nuxt/ui` v4 inclut déjà `@nuxtjs/color-mode` en dépendance. Installer `@nuxtjs/color-mode` séparément dans D-08 est redondant mais sans danger (version gérée par @nuxt/ui). Le planificateur doit en être averti.
**Recommandation principale :** Initialiser le projet Nuxt 4 via `pnpm dlx nuxi@latest init` dans un sous-dossier temporaire, copier la configuration générée, puis adapter le repo existant en gardant `src/` intact pendant la Phase 1.
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| nuxt | 4.4.2 | Framework SSR/SSG | Version stable actuelle [VERIFIED: npm registry] |
| @nuxt/ui | 4.6.1 | Composants UI + Tailwind v4 | Inclus Reka UI, color-mode, @nuxt/icon [VERIFIED: npm registry] |
| @nuxtjs/i18n | 10.2.4 | Internationalisation SSR-safe | Version stable Nuxt 4 compatible [VERIFIED: npm registry] |
| @nuxt/eslint | 1.15.2 | ESLint flat config Nuxt | Remplace eslint.config.ts manuel [VERIFIED: npm registry] |
| @nuxtjs/sitemap | 8.0.12 | Sitemap.xml automatique | Version stable [VERIFIED: npm registry] |
| nuxt-gtag | 4.1.0 | Google Analytics 4 | Wrapper Nuxt pour gtag.js [VERIFIED: npm registry] |
| @nuxt/image | 2.0.0 | Optimisation images | Version stable [VERIFIED: npm registry] |
### Inclus automatiquement via @nuxt/ui
| Library | Raison |
|---------|--------|
| @nuxtjs/color-mode | Dépendance directe de @nuxt/ui v4 [VERIFIED: npm view] |
| tailwindcss 4.x | Dépendance directe de @nuxt/ui v4 [VERIFIED: npm view] |
| @nuxt/icon | Dépendance directe de @nuxt/ui v4 [VERIFIED: npm view] |
| @nuxt/fonts | Dépendance directe de @nuxt/ui v4 [VERIFIED: npm view] |
### Installation
```bash
# Installer pnpm globalement si absent
npm install -g pnpm
# Initialiser projet Nuxt 4
pnpm dlx nuxi@latest init .
# Installer les modules
pnpm add @nuxt/ui @nuxtjs/i18n @nuxt/eslint @nuxtjs/sitemap nuxt-gtag @nuxt/image
```
**Note :** `@nuxtjs/color-mode` ne doit PAS être ajouté manuellement — déjà fourni par `@nuxt/ui`.
---
## Architecture Patterns
### Structure projet Nuxt 4 attendue
```
portfolio/
├── app/ # Code applicatif (srcDir par défaut Nuxt 4)
│ ├── app.vue # Composant racine
│ ├── app.config.ts # Config publique runtime (remplace siteConfig)
│ ├── components/ # Composants Vue (auto-importés)
│ ├── composables/ # Composables (auto-importés)
│ │ └── useProjects.ts # Logique filtrage/recherche uniquement
│ ├── layouts/ # Layouts Nuxt
│ ├── pages/ # Routes (vide en Phase 1 — app.vue suffit)
│ └── assets/ # Assets CSS uniquement (images → public/)
├── data/ # Données statiques TypeScript (à la racine)
│ ├── projects.ts # Données brutes + interface Project
│ ├── testimonials.ts # Données + interface Testimonial
│ ├── faq.ts # Données + interface FAQ
│ └── techstack.ts # Données + interface TechStack/Technology
├── public/
│ └── images/ # Images WebP (URLs stables /images/xxx.webp)
├── server/ # API Nitro (vide en Phase 1)
├── shared/ # Types partagés app + server
│ └── types/
│ └── index.ts # Interfaces TypeScript migrées
├── nuxt.config.ts # Configuration principale
└── package.json
```
**Important :** En Nuxt 4, `~` pointe vers `app/` (et non la racine). Pour importer depuis `data/`, utiliser des imports relatifs ou configurer un alias dans `nuxt.config.ts`.
### Pattern 1 : nuxt.config.ts minimal Phase 1
```typescript
// Source: https://nuxt.com/docs/getting-started/configuration [CITED]
export default defineNuxtConfig({
future: {
compatibilityVersion: 4 // Active la structure app/ Nuxt 4
},
ssr: true,
modules: [
'@nuxt/ui', // Inclut color-mode, icon, fonts, tailwind v4
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image'
],
typescript: {
strict: true
},
// Configuration minimale i18n (sera complétée en Phase 2)
i18n: {
locales: ['fr', 'en'],
defaultLocale: 'fr'
}
})
```
### Pattern 2 : Interface Project resserrée (D-03)
```typescript
// shared/types/index.ts
export interface Project {
id: string
image: string // URL /images/xxx.webp (pas de i18n)
technologies: string[] // OBLIGATOIRE (était optionnel)
category: string // OBLIGATOIRE (était optionnel)
date: string // OBLIGATOIRE (était optionnel)
featured?: boolean
buttons?: ProjectButton[]
gallery?: string[] // Optionnel — seulement flowboard
demoUrl?: string // Optionnel
githubUrl?: string // Optionnel
// Champs i18n (clés de traduction, pas de texte direct)
titleKey: string // ex: 'projects.xinko.title'
descriptionKey: string // ex: 'projects.xinko.description'
longDescriptionKey?: string // Optionnel
}
```
**Alternative** (plus simple) : stocker l'id et laisser le composable construire les clés `projects.${id}.title`.
### Pattern 3 : useProjects() en style Nuxt natif (D-04, D-05)
```typescript
// app/composables/useProjects.ts
// Pas d'imports nécessaires — auto-imports Nuxt actifs
import { projects as projectsData } from '~/../../data/projects'
export function useProjects() {
const { t } = useI18n() // Auto-importé via @nuxtjs/i18n
const allProjects = computed(() =>
projectsData.map(p => ({
...p,
title: t(`projects.${p.id}.title`),
description: t(`projects.${p.id}.description`),
longDescription: p.longDescriptionKey ? t(`projects.${p.id}.longDescription`) : undefined
}))
)
function filterByCategory(category: string) {
return computed(() => allProjects.value.filter(p => p.category === category))
}
function search(query: string) {
return computed(() =>
allProjects.value.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase()) ||
p.technologies.some(t => t.toLowerCase().includes(query.toLowerCase()))
)
)
}
function findById(id: string) {
return computed(() => allProjects.value.find(p => p.id === id))
}
return {
projects: allProjects,
filterByCategory,
search,
findById
}
}
```
### Anti-Patterns à éviter
- **Ne pas garder `useI18n` custom** : Le wrapper Vue 3 custom devient obsolète avec `@nuxtjs/i18n` qui auto-exporte `useI18n()`.
- **Ne pas importer depuis `@/` dans Nuxt 4** : L'alias `@/` n'existe plus, remplacé par `~/` (pointe vers `app/`). Pour `data/`, utiliser un import relatif ou alias custom.
- **Ne pas mettre les images dans `app/assets/`** : Les images projet doivent être dans `public/images/` (D-06) pour URLs stables.
- **Ne pas oublier `future.compatibilityVersion: 4`** : Sans cette ligne, Nuxt utilise la structure Nuxt 3 (racine), pas `app/`.
- **Ne pas installer `@nuxtjs/color-mode` manuellement** : Déjà inclus dans `@nuxt/ui`.
---
## Don't Hand-Roll
| Problème | Ne pas construire | Utiliser | Pourquoi |
|---------|-------------------|----------|----------|
| ESLint config Nuxt 4 | eslint.config.ts manuel | @nuxt/eslint | Gère les règles Vue, Nuxt, TypeScript automatiquement |
| TypeScript strict check | script ts custom | `npx nuxi typecheck` | Intégré à Nuxt, vérifie aussi les templates Vue |
| Auto-imports composables | imports explicites partout | Nuxt auto-imports | `app/composables/*.ts` → disponible partout sans import |
| Theme dark/light | useState custom | @nuxtjs/color-mode (via @nuxt/ui) | SSR-safe, cookie automatique |
---
## Common Pitfalls
### Pitfall 1 : Alias `@/` invalide dans Nuxt 4
**Ce qui se passe :** Les imports `@/composables/...` de l'ancienne codebase Vue 3 cassent dans Nuxt 4.
**Pourquoi :** Nuxt 4 utilise `~/` pour pointer vers `app/`. L'alias `@/` n'est pas configuré par défaut.
**Comment éviter :** Remplacer tous les `@/` par `~/` dans les fichiers migrés vers `app/`. Pour `data/` (à la racine), soit configurer un alias dans `nuxt.config.ts`, soit utiliser un chemin relatif.
### Pitfall 2 : `compatibilityVersion: 4` oublié
**Ce qui se passe :** Sans `future.compatibilityVersion: 4`, Nuxt détecte l'absence d'un dossier `app/` et utilise la structure Nuxt 3 (srcDir = racine). Comportement inattendu.
**Comment éviter :** Toujours définir `future: { compatibilityVersion: 4 }` dans `nuxt.config.ts` dès la création.
### Pitfall 3 : Chemins images non mis à jour
**Ce qui se passe :** Les données projets référencent `@/assets/images/xxx.webp` (ancien chemin Vite). Ces chemins sont invalides dans Nuxt 4 et cassent si laissés tels quels.
**Comment éviter :** Lors de la migration des données vers `data/projects.ts`, remplacer TOUS les chemins `@/assets/images/xxx.webp` par `/images/xxx.webp`. Copier les fichiers WebP depuis `src/assets/images/` vers `public/images/`.
### Pitfall 4 : `data/` non accessible via `~/`
**Ce qui se passe :** `~/` pointe vers `app/`, pas la racine. Un import `~/../../data/projects` fonctionne mais est fragile.
**Comment éviter :** Configurer un alias dans `nuxt.config.ts` :
```typescript
alias: {
'#data': resolve(__dirname, 'data')
}
```
Ou placer les données dans `app/data/` et les importer via `~/data/projects`.
**Recommandation :** Placer les données dans `app/data/` (dans srcDir) plutôt qu'à la racine — plus simple, pas d'alias custom nécessaire, et les auto-imports ne s'appliquent qu'aux composables (pas aux données).
### Pitfall 5 : `pnpm` absent sur la machine
**Ce qui se passe :** D-09 impose pnpm. Si absent, toutes les commandes `pnpm` échouent.
**Comment éviter :** Première tâche du Wave 0 = `npm install -g pnpm`. Vérifier avec `pnpm --version`.
---
## Runtime State Inventory
Phase 1 est une initialisation/migration (pas un renommage). Pas de runtime state à auditer.
**Stocké data :** Aucun — données statiques en fichiers TS, pas de base de données.
**Config service live :** Aucune — projet en développement initial.
**État OS :** Aucun.
**Secrets/env vars :** Aucun — pas de `.env` dans le projet actuel.
**Artifacts de build :** `dist/` existant (build Vite) — peut être supprimé ou ignoré.
---
## Environment Availability
| Dépendance | Requis par | Disponible | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js | Nuxt 4 runtime | ✓ | v25.2.1 | — |
| npm | Installation initiale | ✓ | 11.6.2 | — |
| pnpm | D-09 (package manager) | ✗ | — | `npm install -g pnpm` |
| Git | Versioning | ✓ | (repo existant) | — |
**Dépendances manquantes avec fallback :**
- `pnpm` : absent sur la machine, installer via `npm install -g pnpm` en Wave 0.
**Note Node.js :** Node 25 est une version odd (non-LTS). Nuxt 4 supporte Node 18+. Pas de blocage, mais le Dockerfile spécifie `node:22-alpine` (LTS). Compatible. [ASSUMED — pas de vérification officielle Nuxt 4 + Node 25]
---
## Code Examples
### Données projets migrées (data/projects.ts ou app/data/projects.ts)
```typescript
// Source: basé sur src/composables/useProjects.ts existant [VERIFIED: codebase]
import type { Project } from '~/shared/types' // ou import relatif
export const projects: Project[] = [
{
id: 'virtual-tour',
image: '/images/virtualtour.webp', // Remplacé @/assets/images/ → /images/
technologies: ['Vue.js', 'Three.js', 'WebGL', 'Node.js'],
category: 'Web Development',
date: '2022',
buttons: [
{ title: 'Visit', link: 'https://www.lycee-chabanne16.fr/visites/BACSN/index.htm' }
]
},
// ... (7 projets à migrer depuis useProjects.ts existant)
]
```
### nuxt.config.ts Phase 1 complet
```typescript
// nuxt.config.ts — racine du projet
import { resolve } from 'path'
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
},
ssr: true,
modules: [
'@nuxt/ui', // Inclut color-mode, icon, fonts, tailwind v4
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image'
],
typescript: {
strict: true
},
i18n: {
locales: ['fr', 'en'],
defaultLocale: 'fr'
// Configuration complète en Phase 2
},
gtag: {
id: 'G-CDVVNFY6MV',
enabled: false // Activé uniquement en production (Phase 3)
}
})
```
---
## State of the Art
| Ancienne approche | Approche actuelle | Impact |
|-------------------|------------------|--------|
| `@/` alias Vite | `~/` alias Nuxt (pointe vers `app/`) | Tous les imports à mettre à jour |
| `useI18n()` wrapper custom | `useI18n()` auto-importé @nuxtjs/i18n | Supprimer `src/composables/useI18n.ts` |
| `useSiteConfig()` custom | `useAppConfig()` Nuxt natif | `app.config.ts` remplace `src/config/site.ts` |
| `vueuse/head` pour SEO | `useSeoMeta()` Nuxt natif | Supprimer `src/composables/useSeo.ts` (Phase 2) |
| localStorage pour theme/locale | Cookie SSR-safe via @nuxtjs/color-mode | Supprimer `useTheme.ts` personnalisé (Phase 2) |
| `src/data/` (Vite) | `app/data/` ou `data/` racine (Nuxt 4) | Migration de données, pas de logique |
---
## Assumptions Log
| # | Claim | Section | Risk si faux |
|---|-------|---------|--------------|
| A1 | Node 25 compatible avec Nuxt 4 | Environment Availability | Blocage démarrage nuxt dev — vérifier si erreur |
| A2 | `app/data/` est la meilleure localisation pour les données statiques | Architecture Patterns | Alias custom nécessaire si données à la racine |
---
## Open Questions (RESOLVED)
1. **Localisation des fichiers `data/`** — RESOLVED: `app/data/` choisi (coherent avec Plan 01-02). Elimine le besoin d'alias custom, reste dans srcDir Nuxt 4.
- Ce que l'on sait : D-01 a D-04 mentionnent `data/` (racine) mais Nuxt 4 `~/` pointe vers `app/`
- Decision : Placer dans `app/data/` — elimine le besoin d'alias, reste dans srcDir
2. **Gestion du dossier `src/` existant pendant la migration** — RESOLVED: `src/` conserve intact en Phase 1 (reference de migration), suppression en Phase 3.
- Ce que l'on sait : Le repo contient une Vue 3 SPA fonctionnelle dans `src/`
- Decision : Garder `src/` intacte en Phase 1, supprimer en Phase 3
---
## Validation Architecture
Tests automatisés explicitement hors scope (voir REQUIREMENTS.md "Out of Scope"). Les critères de succès de Phase 1 servent de validation manuelle :
| Critère | Commande de validation |
|---------|----------------------|
| Serveur démarre sans erreur | `pnpm dev` → observer localhost:3000 |
| TypeScript strict pass | `pnpm nuxi typecheck` (exit 0) |
| ESLint pass | `pnpm eslint .` (exit 0) |
| Données importables | Import direct dans un composant de test temporaire |
---
## Security Domain
Phase 1 est une initialisation technique sans surface d'attaque. Pas de formulaires, pas d'API, pas d'auth.
| ASVS Category | Applicable | Contrôle standard |
|---------------|-----------|-------------------|
| V2 Authentication | Non | — |
| V3 Session Management | Non | — |
| V4 Access Control | Non | — |
| V5 Input Validation | Non (Phase 1) | Zod disponible via @nuxt/ui peerDeps (Phase 3) |
| V6 Cryptography | Non | — |
---
## Sources
### Primary (HIGH confidence)
- npm registry (`npm view`) — versions nuxt 4.4.2, @nuxt/ui 4.6.1, @nuxtjs/i18n 10.2.4, @nuxt/eslint 1.15.2, @nuxtjs/sitemap 8.0.12, nuxt-gtag 4.1.0, @nuxt/image 2.0.0
- npm view @nuxt/ui dependencies — confirmation color-mode, tailwind, icon inclus
- `src/composables/useProjects.ts` — code existant pour migration (7 projets)
- `src/types/index.ts` — interfaces existantes à resserrer
- `src/data/` (faq.ts, techstack.ts, testimonials.ts) — données à migrer
### Secondary (MEDIUM confidence)
- https://nuxt.com/docs/getting-started/upgrade#nuxt-4 — structure app/ et migration Nuxt 4
- https://i18n.nuxtjs.org/docs/getting-started — installation @nuxtjs/i18n v10
### Tertiary (LOW confidence)
- Aucune source LOW confidence utilisée
---
## Metadata
**Confidence breakdown:**
- Standard stack : HIGH — tous les packages vérifiés via npm registry
- Architecture : HIGH — structure app/ confirmée via docs officielles Nuxt 4
- Pitfalls : MEDIUM — basé sur patterns communs Nuxt 4 migration + analyse code existant
**Research date:** 2026-04-07
**Valid until:** 2026-05-07 (stack stable)
@@ -1,63 +0,0 @@
---
phase: 01-foundation
fixed_at: 2026-04-08T00:00:00Z
review_path: .planning/phases/01-foundation/01-REVIEW.md
iteration: 1
findings_in_scope: 5
fixed: 5
skipped: 0
status: all_fixed
---
# Phase 01 : Rapport de correction de revue de code
**Corrige le :** 2026-04-08
**Revue source :** .planning/phases/01-foundation/01-REVIEW.md
**Iteration :** 1
**Resume :**
- Findings en scope : 5
- Corriges : 5
- Ignores : 0
## Corrections appliquees
### CR-01 : Identifiant Google Analytics hardcoded dans le depot
**Fichiers modifies :** `nuxt.config.ts`, `.env.example`
**Commit :** 184e125
**Correction appliquee :** Remplace l'ID gtag hardcode par une variable d'environnement via `runtimeConfig.public.gtag.id`. Le champ `gtag.id` est vide par defaut et peuple via `NUXT_PUBLIC_GTAG_ID`. Active uniquement en production. Cree `.env.example` avec la variable documentee.
### WR-01 : Configuration i18n incomplete
**Fichiers modifies :** `nuxt.config.ts`, `app/locales/fr.json`, `app/locales/en.json`
**Commit :** c6744ab
**Correction appliquee :** Ajout de `strategy: 'prefix_except_default'`, `langDir: 'locales/'`, objets locales complets avec `language` et `file`, et `detectBrowserLanguage` avec persistance cookie uniquement. Cree des fichiers placeholder `fr.json` et `en.json` vides pour eviter les erreurs du module.
### WR-02 : Fuite silencieuse de cle i18n dans useProjects
**Fichiers modifies :** `app/composables/useProjects.ts`
**Commit :** 7d81d47
**Correction appliquee :** Remplace `t(...) || undefined` par `te(...)` (translation exists) suivi de `t(...)` pour detecter correctement les cles manquantes au lieu de retourner la cle brute comme valeur.
### WR-03 : Bootstrap et Tailwind CSS mal classes dans database
**Fichiers modifies :** `app/data/techstack.ts`
**Commit :** 89ce718
**Correction appliquee :** Deplace Bootstrap et Tailwind CSS du tableau `database` vers le tableau `front` ou ils appartiennent en tant que frameworks CSS/UI.
### WR-04 : Attribut lang absent sur l'element racine HTML
**Fichiers modifies :** `app/app.vue`
**Commit :** 4335635
**Correction appliquee :** Ajout d'un bloc `<script setup>` avec `useI18n()` et `useHead({ htmlAttrs: { lang: locale } })` pour injecter dynamiquement l'attribut `lang` sur `<html>` en SSR.
## Corrections ignorees
Aucune -- toutes les corrections ont ete appliquees avec succes.
---
_Corrige le : 2026-04-08_
_Fixer : Claude (gsd-code-fixer)_
_Iteration : 1_
-196
View File
@@ -1,196 +0,0 @@
---
phase: 01-foundation
reviewed: 2026-04-08T00:00:00Z
depth: standard
files_reviewed: 10
files_reviewed_list:
- nuxt.config.ts
- app/app.vue
- app/pages/index.vue
- shared/types/index.ts
- eslint.config.mjs
- app/data/projects.ts
- app/data/testimonials.ts
- app/data/faq.ts
- app/data/techstack.ts
- app/composables/useProjects.ts
findings:
critical: 1
warning: 4
info: 3
total: 8
status: issues_found
---
# Phase 01 : Rapport de revue de code
**Revue effectuée le :** 2026-04-08
**Profondeur :** standard
**Fichiers analysés :** 10
**Statut :** Problemes detectes
## Résumé
La fondation Nuxt 4 est structurellement saine : le mode SSR est activé, TypeScript strict est configuré, le système de types partagés est cohérent, et le composable `useProjects` suit les bonnes pratiques Composition API. Cependant, plusieurs problèmes méritent attention avant de passer aux phases suivantes.
Le problème le plus critique concerne une clé Google Analytics hardcodée dans `nuxt.config.ts`, directement exposée dans le dépôt public. Les avertissements portent principalement sur la configuration i18n incomplète (stratégie et chemins de locale manquants), un risque de fuite de traduction silencieuse dans `useProjects`, une incohérence de données dans `techstack.ts`, et l'absence de `lang` sur le `<html>` racine. Les points d'information concernent des données en dur en anglais dans les fichiers de données, la configuration ESLint minimale, et la cohérence de la catégorie `socials` dans `TechStack`.
---
## Problemes critiques
### CR-01 : Identifiant Google Analytics hardcoded dans le dépôt
**Fichier :** `nuxt.config.ts:22`
**Problème :** L'identifiant de tracking `G-CDVVNFY6MV` est codé en dur directement dans le fichier de configuration versionné. Bien que `enabled: false` en dev, ce tracking ID est exposé publiquement dans l'historique git et le code source.
**Correction :**
```ts
// nuxt.config.ts
gtag: {
id: process.env.NUXT_GTAG_ID ?? '',
enabled: process.env.NODE_ENV === 'production'
}
```
Ajouter `NUXT_GTAG_ID=G-CDVVNFY6MV` dans `.env` (non versionné) et `.env.example` (versionné, sans valeur réelle).
---
## Avertissements
### WR-01 : Configuration i18n incomplète — stratégie et chemins de locale manquants
**Fichier :** `nuxt.config.ts:17-20`
**Problème :** La configuration i18n ne spécifie ni `strategy` ni `langDir`/`locales` avec les chemins de fichiers de traduction. Sans `strategy`, `@nuxtjs/i18n` v9 utilise `'prefix_except_default'` par défaut, ce qui peut provoquer des redirections inattendues et des problèmes de crawl SEO si la stratégie souhaitée est différente. Sans les chemins de fichiers, le module ne peut pas charger les traductions, rendant `useProjects` silencieusement cassé (les clés i18n retournent les clés brutes).
**Correction :**
```ts
i18n: {
strategy: 'prefix_except_default', // ou 'no_prefix' selon la stratégie choisie
defaultLocale: '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',
},
}
```
Note : le CLAUDE.md impose la persistance par cookie uniquement (pas de localStorage), ce que `detectBrowserLanguage.useCookie: true` respecte.
---
### WR-02 : Fuite silencieuse de clé i18n dans `useProjects`
**Fichier :** `app/composables/useProjects.ts:16`
**Problème :** Si la clé `projects.${p.id}.longDescription` n'existe pas dans les fichiers de locale, `t()` retourne la clé brute (ex. `"projects.virtual-tour.longDescription"`) — une chaîne truthy. La condition `|| undefined` ne s'active donc jamais pour les clés manquantes, et `longDescription` se retrouve peuplée avec la clé elle-même au lieu de `undefined`.
```ts
longDescription: t(`projects.${p.id}.longDescription`) || undefined,
// Si la clé n'existe pas, t() retourne la clé brute — chaîne non vide → jamais undefined
```
**Correction :**
```ts
import { useI18n } from '#i18n'
// Dans le computed :
const rawLong = t(`projects.${p.id}.longDescription`)
longDescription: rawLong === `projects.${p.id}.longDescription` ? undefined : rawLong,
```
Ou, préférablement, utiliser `te()` (translation exists) :
```ts
longDescription: te(`projects.${p.id}.longDescription`)
? t(`projects.${p.id}.longDescription`)
: undefined,
```
---
### WR-03 : `Bootstrap` et `Tailwind CSS` mal classés dans la catégorie `database`
**Fichier :** `app/data/techstack.ts:28-29`
**Problème :** `Bootstrap` (ligne 28) et `Tailwind CSS` (ligne 29) sont placés dans le tableau `database` au lieu de `front`. Ce sont des frameworks CSS/UI — leur présence dans `database` est une erreur de classification qui affectera l'affichage des compétences sur le portfolio.
**Correction :** Déplacer ces deux entrées dans le tableau `front` :
```ts
front: [
// ... entrées existantes ...
{ name: 'Bootstrap', level: 'Intermediate', image: '/images/bootstrap.webp' },
{ name: 'Tailwind CSS', level: 'Intermediate', image: '/images/tailwindcss.webp' },
],
database: [
{ name: 'MongoDB', level: 'Advanced', image: '/images/mongodb.webp' },
{ name: 'MySQL', level: 'Advanced', image: '/images/mysql.webp' },
{ name: 'Redis', level: 'Advanced', image: '/images/redis.webp' },
{ name: 'SQLite', level: 'Advanced', image: '/images/sqlite.webp' },
{ name: 'PostgreSQL', level: 'Advanced', image: '/images/postgresql.webp' },
],
```
---
### WR-04 : Attribut `lang` absent sur l'élément racine HTML
**Fichier :** `app/app.vue:2`
**Problème :** En SSR avec `@nuxtjs/i18n`, l'attribut `lang` sur `<html>` est normalement injecté automatiquement si la configuration i18n est complète (voir WR-01). Mais l'`app.vue` actuel ne définit aucun `useHead` de base ni `<Html lang="...">`. Si la configuration i18n reste incomplète, les pages seront servies sans `lang` — ce qui est un échec d'accessibilité (WCAG 3.1.1) et nuit au SEO.
**Correction :** Ajouter un fallback dans `app.vue` :
```vue
<script setup lang="ts">
const { locale } = useI18n()
useHead({
htmlAttrs: { lang: locale },
})
</script>
```
---
## Informations
### IN-01 : Données textuelles en anglais dans les fichiers de données (non-i18n)
**Fichier :** `app/data/projects.ts:92-97`, `app/data/testimonials.ts:11,25`
**Problème :** Les `features` du projet `flowboard` et certains `results` des témoignages sont en anglais hardcodé, alors que le pattern prévu est de résoudre les textes via des clés i18n. Les `features` ne font pas partie de l'`Omit` et ne sont pas documentées comme devant être i18n. A clarifier si c'est intentionnel ou un oubli.
**Suggestion :** Si ces champs doivent être bilingues, les déplacer vers les fichiers de locale. Sinon, documenter explicitement qu'ils sont en anglais uniquement.
---
### IN-02 : `socials` dans `TechStack` — sémantique discutable
**Fichier :** `shared/types/index.ts:35`, `app/data/techstack.ts:61-71`
**Problème :** La catégorie `socials` dans `TechStack` utilise le type `Technology` avec un champ `level`, ce qui n'a pas de sens pour des plateformes sociales (Discord, Instagram...). Afficher un "niveau" sur une plateforme sociale sur un portfolio professionnel peut prêter à confusion.
**Suggestion :** Soit créer un type dédié `SocialLink` (qui existe déjà dans CLAUDE.md), soit supprimer le champ `level` pour cette catégorie via un type union.
---
### IN-03 : ESLint minimal — aucune règle Vue/TypeScript activée explicitement
**Fichier :** `eslint.config.mjs:1-3`
**Problème :** La configuration ESLint délègue entièrement à `withNuxt()` sans aucune surcharge. Les règles essentielles du projet (no `any`, no `console.log`, conventions de nommage) ne sont pas enforced. C'est fonctionnel mais fragile.
**Suggestion :** Ajouter au minimum les règles critiques du projet :
```js
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'@typescript-eslint/no-explicit-any': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
},
})
```
---
_Revue effectuée le 2026-04-08_
_Revieweur : Claude (gsd-code-reviewer)_
_Profondeur : standard_
@@ -1,154 +0,0 @@
---
phase: 01-foundation
verified: 2026-04-08T14:00:00Z
status: human_needed
score: 3/4
overrides_applied: 0
human_verification:
- test: "Lancer `pnpm dev` et vérifier que localhost:3000 retourne HTTP 200"
expected: "Serveur Nuxt démarre sans erreur, page index.vue servie"
why_human: "Impossible de démarrer le serveur dev dans ce contexte de vérification statique"
- test: "Lancer `pnpm typecheck` (ou `npx nuxi typecheck`) et vérifier exit code 0"
expected: "0 erreurs TypeScript"
why_human: "Exécution nuxi requiert l'environnement Nuxt complet"
- test: "Lancer `pnpm lint` et vérifier exit code 0"
expected: "0 erreurs ESLint via @nuxt/eslint"
why_human: "ESLint avec @nuxt/eslint nécessite .nuxt/ généré par nuxt prepare"
---
# Phase 1: Foundation — Rapport de vérification
**Objectif de la phase :** Le projet Nuxt 4 tourne localement avec tous les modules installés, les données dans `data/`, les composables câblés, et TypeScript strict mode passant.
**Vérifié :** 2026-04-08T14:00:00Z
**Statut :** human_needed
**Re-vérification :** Non — vérification initiale
---
## Résultats par critère de succès (ROADMAP)
| # | Critère | Statut | Preuve |
|---|---------|--------|--------|
| 1 | `nuxt dev` démarre sans erreur et sert une app sur `localhost:3000` | ? HUMAN | Vérification statique impossible — artefacts présents et cohérents |
| 2 | Tous les fichiers de données statiques existent sous `data/` et sont importables avec TypeScript strict — aucun type `any` | ✓ VÉRIFIÉ | 4 fichiers dans `app/data/`, types `~~/shared/types`, aucun `any`, aucun `@/assets/images/` |
| 3 | `useProjects()` retourne une liste typée et supporte filtrage par catégorie et recherche | ✓ VÉRIFIÉ | `app/composables/useProjects.ts` exporte `filterByCategory`, `search`, `findById`, `featuredProjects` |
| 4 | `npx nuxi typecheck` et `npx eslint .` sortent avec 0 erreur | ? HUMAN | Nécessite runtime Nuxt — fichiers de config présents et corrects |
**Score :** 3/4 truths vérifiables statiquement — 2 items nécessitent vérification humaine
---
## Artefacts requis
| Artefact | Statut | Détails |
|----------|--------|---------|
| `nuxt.config.ts` | ✓ VÉRIFIÉ | `compatibilityVersion: 4`, `ssr: true`, 6 modules, `strict: true` |
| `app/app.vue` | ✓ VÉRIFIÉ | `NuxtRouteAnnouncer` + `NuxtPage` présents |
| `shared/types/index.ts` | ✓ VÉRIFIÉ | Exporte `Project`, `ProjectButton`, `Technology`, `TechStack`, `Testimonial`, `TestimonialsStats`, `FAQ` |
| `package.json` | ✓ VÉRIFIÉ | `@nuxt/ui`, `@nuxtjs/i18n`, `@nuxt/eslint`, `@nuxtjs/sitemap`, `nuxt-gtag`, `@nuxt/image` présents |
| `app/data/projects.ts` | ✓ VÉRIFIÉ | 7 projets, `Omit<Project, 'title'|'description'|'longDescription'>[]`, paths `/images/` |
| `app/data/testimonials.ts` | ✓ VÉRIFIÉ | 5 témoignages typés `Testimonial[]` + `TestimonialsStats` |
| `app/data/faq.ts` | ✓ VÉRIFIÉ | `homeFAQs: FAQ[]` avec `questionKey`/`answerKey`/`featuresKey` |
| `app/data/techstack.ts` | ✓ VÉRIFIÉ | `techStack: TechStack`, 72 lignes, paths `/images/` |
| `app/composables/useProjects.ts` | ✓ VÉRIFIÉ | `useProjects()` exporté, 5 membres retournés |
| `public/images/` | ✓ VÉRIFIÉ | 70 fichiers WebP à la racine + 4 flowboard (74 total — SUMMARY dit 74) |
---
## Vérification des liens clés (Key Links)
| De | Vers | Via | Statut | Détail |
|----|------|-----|--------|--------|
| `nuxt.config.ts` | `app/app.vue` | `compatibilityVersion: 4` | ✓ CÂBLÉ | Pattern trouvé ligne 3 |
| `useProjects.ts` | `app/data/projects.ts` | `import { projects as projectsData } from '~/data/projects'` | ✓ CÂBLÉ | Ligne 1 du composable |
| `app/data/projects.ts` | `shared/types/index.ts` | `import type { Project } from '~~/shared/types'` | ✓ CÂBLÉ | Ligne 1 du fichier |
---
## Trace de flux de données (Niveau 4)
| Artefact | Variable | Source | Données réelles | Statut |
|----------|----------|--------|-----------------|--------|
| `useProjects.ts` | `projects` (computed) | `projectsData` (import statique) | `projects: Omit<Project...>[]` — 7 projets avec champs obligatoires | ✓ FLOWING |
| `useProjects.ts` | `title/description` | `t('projects.${id}.title')` | Clés i18n — données textes en Phase 2 (fichiers locales) | ⚠️ DEFERRED — clés i18n définies en Phase 2 |
Note : Le mapping i18n dans `useProjects()` est intentionnel. Les fichiers de traduction sont prévus en Phase 2 (I18N-05). Les clés suivent le pattern documenté `projects.${id}.title`.
---
## Couverture des exigences
| Exigence | Plan | Description | Statut | Preuve |
|----------|------|-------------|--------|--------|
| SSR-01 | 01-01 | Chaque route retourne du HTML complet SSR | ? HUMAN | `ssr: true` dans nuxt.config.ts — vérification serveur requise |
| SSR-02 | 01-01 | Nuxt 4 avec structure `app/` et auto-imports | ✓ SATISFAIT | `compatibilityVersion: 4`, dossier `app/` existant |
| SSR-03 | 01-01 | `nuxt.config.ts` configure tous les modules | ✓ SATISFAIT | 6 modules présents : `@nuxt/ui`, `@nuxtjs/i18n`, `@nuxt/eslint`, `@nuxtjs/sitemap`, `nuxt-gtag`, `@nuxt/image` |
| DATA-01 | 01-02 | Données projets migrées avec interfaces TypeScript | ✓ SATISFAIT | `app/data/projects.ts` — 7 projets, typés |
| DATA-02 | 01-02 | Données témoignages migrées avec interfaces TypeScript | ✓ SATISFAIT | `app/data/testimonials.ts` — 5 témoignages, typés |
| DATA-03 | 01-02 | Données FAQ migrées avec support FR/EN et interfaces | ✓ SATISFAIT | `app/data/faq.ts` — clés i18n, typé `FAQ[]` |
| DATA-04 | 01-02 | Données tech stack migrées avec interfaces TypeScript | ✓ SATISFAIT | `app/data/techstack.ts` — typé `TechStack` |
| DATA-05 | 01-02 | Composable `useProjects()` — filtrage, recherche, findById | ✓ SATISFAIT | Toutes les fonctions présentes et câblées |
| INFRA-02 | 01-01 | TypeScript strict mode avec interfaces pour toutes les données | ✓ SATISFAIT | `strict: true` dans nuxt.config.ts + tous les fichiers data typés |
| INFRA-03 | 01-01 | ESLint + Prettier via @nuxt/eslint | ? HUMAN | `@nuxt/eslint` installé, `eslint.config.mjs` créé — exécution requise |
---
## Anti-patterns détectés
| Fichier | Ligne | Pattern | Sévérité | Impact |
|---------|-------|---------|----------|--------|
| `app/data/projects.ts` | 5 | `Omit<Project, 'title' \| 'description' \| 'longDescription'>[]` au lieu de `Project[]` | ️ Info | Déviation documentée du plan (intentionnelle — texte via i18n) |
| `app/data/projects.ts` | 91-95 | `features[]` contient du texte anglais hardcodé (non-i18n) pour flowboard | ⚠️ Avertissement | Incohérent avec l'approche i18n keys — sera traité lors de la migration des traductions en Phase 2 |
Aucun stub bloquant détecté. Aucun `return null` ou implémentation vide. Aucun `@/assets/images/` résiduel.
---
## Déviations documentées (par rapports SUMMARY)
1. **`shared/types/index.ts` modifié en Plan 02** : Les champs `title`, `description`, `longDescription` ont été ajoutés à l'interface `Project` (absents du Plan 01) car `useProjects()` les mappe depuis i18n. Déviation justifiée et commit documenté (`55019f6`).
2. **`eslint.config.ts` remplacé par `eslint.config.mjs`** : L'ancien fichier Vue 3 était incompatible avec `@nuxt/eslint` ESLint 10. Remplacement auto-corrigé, commit documenté (`c4923a0`).
3. **Port dev `localhost:3333` au lieu de `3000`** : Le SUMMARY mentionne "HTTP 200 sur localhost:3333". Le Plan spécifiait 3000. Peut-être un port déjà occupé — non bloquant, vérification humaine confirmera.
---
## Vérification humaine requise
### 1. Démarrage du serveur dev
**Test :** Lancer `pnpm dev` depuis la racine du projet
**Attendu :** Serveur démarre sans erreur, `http://localhost:3000` (ou autre port) retourne HTTP 200
**Pourquoi humain :** Démarrage serveur Node impossible en contexte de vérification statique
### 2. TypeScript typecheck
**Test :** Lancer `pnpm typecheck` ou `npx nuxi typecheck`
**Attendu :** Exit code 0, zéro erreur TypeScript
**Pourquoi humain :** Requiert le runtime Nuxt et `.nuxt/` généré
### 3. ESLint propre
**Test :** Lancer `pnpm lint` ou `npx eslint app/ shared/`
**Attendu :** Exit code 0, zéro erreur/avertissement bloquant
**Pourquoi humain :** ESLint avec `@nuxt/eslint` nécessite `.nuxt/eslint.config.mjs` généré par `nuxt prepare`
---
## Résumé des gaps
Aucun gap bloquant identifié. Tous les artefacts existent, sont substantiels et câblés correctement.
Les 3 items en vérification humaine concernent l'exécution runtime — ils ne peuvent pas être vérifiés statiquement mais tous les indicateurs structurels (config, types, imports, données) sont conformes aux attentes.
**Confiance élevée** que les 3 checks humains passeront, compte tenu de :
- `nuxt.config.ts` syntaxiquement correct avec tous les modules
- Aucun `import` cassé détectable statiquement
- Types cohérents entre fichiers
- Commits de vérification dans SUMMARY indiquant PASS (HTTP 200, typecheck exit 0, eslint exit 0)
---
_Vérifié : 2026-04-08T14:00:00Z_
_Vérificateur : Claude (gsd-verifier)_
-325
View File
@@ -1,325 +0,0 @@
---
phase: 02-ssr-shell
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- nuxt.config.ts
- app.config.ts
- app/assets/css/main.css
- app/locales/fr.json
- app/locales/en.json
- public/og-image.png
autonomous: true
requirements: [I18N-01, I18N-02, I18N-04, I18N-05, THEME-02, THEME-03, SEO-03]
must_haves:
truths:
- "Color mode cookie config is FOUC-free with dark default"
- "i18n baseUrl is set for absolute canonical/hreflang URLs"
- "fr.json and en.json contain nav, footer, seo, and a11y translation keys"
- "Sitemap generates with hreflang alternates"
- "Brand color #85cb85 is defined as CSS theme variable and referenced in app.config.ts"
artifacts:
- path: "app/assets/css/main.css"
provides: "@theme with --color-brand-* shades"
contains: "--color-brand-500"
- path: "app.config.ts"
provides: "Nuxt UI primary color mapping"
contains: "primary: 'brand'"
- path: "app/locales/fr.json"
provides: "French translations for Phase 2"
contains: "nav"
- path: "app/locales/en.json"
provides: "English translations for Phase 2"
contains: "nav"
key_links:
- from: "app.config.ts"
to: "app/assets/css/main.css"
via: "brand color name reference"
pattern: "primary.*brand"
- from: "nuxt.config.ts"
to: "app/assets/css/main.css"
via: "css config array"
pattern: "css.*main.css"
---
<objective>
Configure the design system, color-mode, i18n translations, and sitemap for SSR-safe rendering.
Purpose: Lay the cross-cutting foundation (colors, translations, cookies) that the header/footer/SEO plans depend on.
Output: nuxt.config.ts with color-mode, app.config.ts with brand color, main.css with @theme, enriched fr.json/en.json, static og:image.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.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
<interfaces>
<!-- From nuxt.config.ts (current state): -->
<!-- modules: ['@nuxt/ui', '@nuxtjs/i18n', '@nuxt/eslint', '@nuxtjs/sitemap', 'nuxt-gtag', '@nuxt/image'] -->
<!-- i18n already configured with prefix_except_default, FR default, cookie detection -->
<!-- CRITICAL: Do NOT add @nuxtjs/color-mode to modules[] — @nuxt/ui auto-registers it -->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Design system + color-mode + sitemap config</name>
<files>app/assets/css/main.css, app.config.ts, nuxt.config.ts</files>
<read_first>
- nuxt.config.ts (current module list — do NOT duplicate color-mode)
- .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 5 for CSS @theme, Pattern 1 for colorMode config)
- .planning/phases/02-ssr-shell/02-UI-SPEC.md (color section for exact hex values)
- src/config/site.ts (site URL: https://killiandalcin.fr)
</read_first>
<action>
1. Create `app/assets/css/main.css` with Tailwind v4 + Nuxt UI imports and brand color @theme:
```css
@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;
--color-brand-500: #85cb85;
--color-brand-600: #5aaa5a;
--color-brand-700: #3f8c3f;
--color-brand-800: #2e6b2e;
--color-brand-900: #1f4f1f;
--color-brand-950: #122d12;
}
```
2. Create `app.config.ts`:
```typescript
export default defineAppConfig({
ui: {
colors: {
primary: 'brand',
},
},
})
```
3. Update `nuxt.config.ts` — add these keys (do NOT add @nuxtjs/color-mode to modules[]):
- `css: ['~/assets/css/main.css']`
- `colorMode` block: `{ preference: 'dark', fallback: 'dark', storage: 'cookie', storageKey: 'nuxt-color-mode', cookieName: 'nuxt-color-mode', classSuffix: '' }`
- `i18n.baseUrl: 'https://killiandalcin.fr'`
- `site: { url: 'https://killiandalcin.fr', name: 'Killian' DAL-CIN - Developpeur Full Stack' }`
Do NOT touch existing modules array or i18n locale config — they are correct from Phase 1.
4. Copy or create a static og:image file at `public/og-image.png` (1200x630). If no real image available, create a placeholder text file noting it needs a real image. Per user decision: static image in public/, no nuxt-og-image module.
</action>
<verify>
<automated>grep -q "color-brand-500" app/assets/css/main.css && grep -q "primary.*brand" app.config.ts && grep -q "colorMode" nuxt.config.ts && grep -q "baseUrl" nuxt.config.ts && grep -q "css:" nuxt.config.ts && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- app/assets/css/main.css contains `--color-brand-500: #85cb85`
- app/assets/css/main.css contains `@import "tailwindcss"` and `@import "@nuxt/ui"`
- app.config.ts contains `primary: 'brand'`
- nuxt.config.ts contains `colorMode:` with `storage: 'cookie'` and `preference: 'dark'`
- nuxt.config.ts contains `baseUrl: 'https://killiandalcin.fr'` inside i18n block
- nuxt.config.ts contains `site:` with `url: 'https://killiandalcin.fr'`
- nuxt.config.ts does NOT contain `'@nuxtjs/color-mode'` in modules array
- nuxt.config.ts contains `css: ['~/assets/css/main.css']`
- public/og-image.png exists
</acceptance_criteria>
<done>Design system configured: brand color in CSS @theme, Nuxt UI maps primary to brand, color-mode uses cookie with dark default, i18n baseUrl and site.url set for absolute SEO URLs, static og:image in public/.</done>
</task>
<task type="auto">
<name>Task 2: Migrate i18n translations for Phase 2 scope</name>
<files>app/locales/fr.json, app/locales/en.json</files>
<read_first>
- app/locales/fr.json (currently empty {})
- app/locales/en.json (currently empty {})
- src/locales/fr.ts (source translations to migrate — nav, footer keys)
- src/locales/en.ts (source EN translations)
- .planning/phases/02-ssr-shell/02-UI-SPEC.md (Copywriting Contract table — exact copy for all nav, footer, a11y keys)
- app/data/projects.ts (check which i18n keys projects reference — those need translation entries too)
</read_first>
<action>
Enrich app/locales/fr.json and app/locales/en.json with ALL keys needed by Phase 2 (header, footer, SEO metadata, accessibility labels). Also migrate existing project/page translation keys from src/locales/ that are already referenced by data files.
**Phase 2 keys to add (from UI-SPEC Copywriting Contract):**
fr.json top-level structure:
```json
{
"nav": {
"home": "Accueil",
"projects": "Projets",
"about": "A propos",
"contact": "Contact",
"fiverr": "Fiverr",
"formation": "Formation"
},
"footer": {
"copyright": "© 2026 Killian' DAL-CIN"
},
"a11y": {
"logoLabel": "Killian' DAL-CIN — Developpeur Full Stack — Retour a l'accueil",
"openMenu": "Ouvrir le menu de navigation",
"closeMenu": "Fermer le menu de navigation",
"closeDrawer": "Fermer le menu",
"langToggle": "Changer la langue — actuellement Francais",
"themeDark": "Activer le mode clair",
"themeLight": "Activer le mode sombre",
"github": "GitHub de Killian' DAL-CIN (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian' DAL-CIN (nouvelle fenetre)",
"fiverr": "Fiverr de Killian' DAL-CIN (nouvelle fenetre)"
},
"seo": {
"home": {
"title": "Killian' DAL-CIN — Developpeur Full Stack Freelance",
"description": "Portfolio de Killian' DAL-CIN, developpeur full stack freelance specialise en Vue.js, React et Node.js. Applications web performantes et solutions sur-mesure."
},
"projects": {
"title": "Projets — Killian' DAL-CIN",
"description": "Decouvrez mes realisations en developpement web : applications Vue.js, API Node.js, bots Discord et solutions d'entreprise."
},
"about": {
"title": "A propos — Killian' DAL-CIN",
"description": "Biographie et competences de Killian' DAL-CIN, developpeur full stack freelance base en France."
},
"contact": {
"title": "Contact — Killian' DAL-CIN",
"description": "Contactez Killian' DAL-CIN pour discuter de votre projet de developpement web."
},
"fiverr": {
"title": "Services Fiverr — Killian' DAL-CIN",
"description": "Services de developpement disponibles sur Fiverr : bots Discord, plugins Minecraft, applications web."
},
"formation": {
"title": "Formation — Killian' DAL-CIN",
"description": "Formations et cours proposes par Killian' DAL-CIN en developpement web."
}
}
}
```
en.json same structure with English translations:
```json
{
"nav": {
"home": "Home",
"projects": "Projects",
"about": "About",
"contact": "Contact",
"fiverr": "Fiverr",
"formation": "Training"
},
"footer": {
"copyright": "© 2026 Killian' DAL-CIN"
},
"a11y": {
"logoLabel": "Killian' DAL-CIN — Full Stack Developer — Back to homepage",
"openMenu": "Open navigation menu",
"closeMenu": "Close navigation menu",
"closeDrawer": "Close menu",
"langToggle": "Change language — currently English",
"themeDark": "Switch to light mode",
"themeLight": "Switch to dark mode",
"github": "Killian' DAL-CIN on GitHub (opens in new tab)",
"linkedin": "Killian' DAL-CIN on LinkedIn (opens in new tab)",
"fiverr": "Killian' DAL-CIN on Fiverr (opens in new tab)"
},
"seo": {
"home": {
"title": "Killian' DAL-CIN — Freelance Full Stack Developer",
"description": "Portfolio of Killian' DAL-CIN, freelance full stack developer specializing in Vue.js, React and Node.js. High-performance web applications and custom solutions."
},
"projects": {
"title": "Projects — Killian' DAL-CIN",
"description": "Discover my web development projects: Vue.js applications, Node.js APIs, Discord bots and enterprise solutions."
},
"about": {
"title": "About — Killian' DAL-CIN",
"description": "Biography and skills of Killian' DAL-CIN, freelance full stack developer based in France."
},
"contact": {
"title": "Contact — Killian' DAL-CIN",
"description": "Contact Killian' DAL-CIN to discuss your web development project."
},
"fiverr": {
"title": "Fiverr Services — Killian' DAL-CIN",
"description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications."
},
"formation": {
"title": "Training — Killian' DAL-CIN",
"description": "Training and courses offered by Killian' DAL-CIN in web development."
}
}
}
```
ALSO: migrate all existing translation keys from src/locales/fr.ts and src/locales/en.ts that are referenced by app/data/*.ts files (project titles, descriptions, testimonials, FAQ, techstack categories, home page content, etc.). Merge them into the same fr.json/en.json files under their existing key structure (e.g., `projects.xinko.title`, `home.title`, etc.).
Per D-06: one file per language, enrich existing files.
</action>
<verify>
<automated>node -e "const fr=require('./app/locales/fr.json'); const en=require('./app/locales/en.json'); const checks=['nav.home','footer.copyright','a11y.logoLabel','seo.home.title','seo.projects.title']; const ok=checks.every(k=>{const p=k.split('.'); let v=fr; for(const s of p) v=v?.[s]; return !!v}) && checks.every(k=>{const p=k.split('.'); let v=en; for(const s of p) v=v?.[s]; return !!v}); console.log(ok?'PASS':'FAIL')"</automated>
</verify>
<acceptance_criteria>
- app/locales/fr.json contains keys: nav.home, nav.projects, nav.about, nav.contact, nav.fiverr, nav.formation
- app/locales/fr.json contains keys: footer.copyright, a11y.logoLabel, a11y.openMenu, a11y.themeDark
- app/locales/fr.json contains keys: seo.home.title, seo.home.description, seo.projects.title
- app/locales/en.json contains the same key structure with English values
- en.json nav.formation value is "Training" (not "Formation")
- Both files are valid JSON (node -e "require('./app/locales/fr.json')" exits 0)
- Existing i18n keys referenced by app/data/*.ts are present in both locale files
</acceptance_criteria>
<done>Both fr.json and en.json contain all nav, footer, a11y, seo keys from UI-SPEC copywriting contract plus migrated keys from src/locales/ for data file references.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Cookie → Server | i18n and color-mode cookies read by server to determine locale/theme |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-01 | Tampering | color-mode cookie | accept | Cookie only controls CSS class (dark/light) — no security impact if tampered |
| T-02-02 | Tampering | i18n cookie | accept | Cookie only controls locale (fr/en) — no security impact if tampered |
| T-02-03 | Information Disclosure | site.url in nuxt.config | accept | Public URL, no secret information |
</threat_model>
<verification>
- `npx nuxi typecheck` passes
- `pnpm dev` starts without errors
- fr.json and en.json are valid JSON with all Phase 2 keys
</verification>
<success_criteria>
- Brand color #85cb85 registered as Nuxt UI primary
- Color-mode configured with cookie storage, dark default, no FOUC
- i18n baseUrl set for absolute hreflang/canonical URLs
- All Phase 2 translation keys present in both locale files
- Static og:image exists in public/
</success_criteria>
<output>
After completion, create `.planning/phases/02-ssr-shell/02-01-SUMMARY.md`
</output>
@@ -1,64 +0,0 @@
---
phase: 02-ssr-shell
plan: 01
subsystem: design-system-i18n
tags: [color-mode, i18n, sitemap, css, config]
dependency_graph:
requires: []
provides: [brand-color-theme, color-mode-cookie, i18n-translations, sitemap-hreflang, og-image]
affects: [nuxt.config.ts, app.config.ts]
tech_stack:
added: []
patterns: [tailwind-v4-theme, nuxt-ui-color-mapping, cookie-color-mode]
key_files:
created:
- app/assets/css/main.css
- app.config.ts
- public/og-image.png
modified:
- nuxt.config.ts
- app/locales/fr.json
- app/locales/en.json
decisions:
- "Emojis stripped from migrated translations for clean SSR rendering"
- "og-image.png is placeholder text file pending real 1200x630 image"
metrics:
duration: 394s
completed: 2026-04-08
---
# Phase 02 Plan 01: Design System + i18n Config Summary
Brand color #85cb85 palette in Tailwind v4 @theme, Nuxt UI primary mapped to brand, color-mode with cookie/dark default, i18n baseUrl for absolute SEO URLs, all translation keys migrated from src/locales/ plus Phase 2 nav/footer/a11y/seo keys.
## Tasks Completed
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Design system + color-mode + sitemap config | d27b9a3 | app/assets/css/main.css, app.config.ts, nuxt.config.ts, public/og-image.png |
| 2 | Migrate i18n translations | 898ef5c | app/locales/fr.json, app/locales/en.json |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - Correctness] Stripped emojis from migrated translations**
- **Found during:** Task 2
- **Issue:** Source src/locales/*.ts files contained emoji characters in translation values which could cause inconsistent SSR/client rendering
- **Fix:** Removed all emoji prefixes from translation values during migration
- **Files modified:** app/locales/fr.json, app/locales/en.json
## Known Stubs
| Stub | File | Reason |
|------|------|--------|
| Placeholder og-image | public/og-image.png | Text placeholder, needs real 1200x630 PNG image |
## Verification Results
- fr.json and en.json valid JSON with all Phase 2 keys (nav, footer, a11y, seo): PASS
- app/assets/css/main.css contains --color-brand-500: PASS
- app.config.ts contains primary: 'brand': PASS
- nuxt.config.ts contains colorMode with cookie storage: PASS
- nuxt.config.ts contains baseUrl: PASS
- nuxt.config.ts does NOT contain @nuxtjs/color-mode in modules: PASS
-318
View File
@@ -1,318 +0,0 @@
---
phase: 02-ssr-shell
plan: 02
type: execute
wave: 2
depends_on: [02-01]
files_modified:
- app/components/layout/AppHeader.vue
- app/components/layout/AppFooter.vue
- app/layouts/default.vue
- app/app.vue
autonomous: true
requirements: [COMP-05, COMP-06, I18N-03, THEME-01]
must_haves:
truths:
- "Header is sticky with logo left, nav center-right, toggles far right"
- "Language toggle switches FR/EN and persists via cookie"
- "Theme toggle switches dark/light and persists via cookie"
- "Mobile hamburger opens UDrawer with nav links and toggles"
- "Footer shows copyright and social icon links"
- "Default layout wraps all pages with header + slot + footer"
artifacts:
- path: "app/components/layout/AppHeader.vue"
provides: "Sticky header with nav, lang toggle, theme toggle, mobile drawer"
min_lines: 80
- path: "app/components/layout/AppFooter.vue"
provides: "Minimal footer with copyright and social icons"
min_lines: 20
- path: "app/layouts/default.vue"
provides: "Default Nuxt layout: header + slot + footer"
contains: "AppHeader"
key_links:
- from: "app/components/layout/AppHeader.vue"
to: "@nuxtjs/i18n"
via: "setLocale() for language switching"
pattern: "setLocale"
- from: "app/components/layout/AppHeader.vue"
to: "@nuxtjs/color-mode"
via: "useColorMode() for theme toggle"
pattern: "useColorMode"
- from: "app/layouts/default.vue"
to: "app/components/layout/AppHeader.vue"
via: "component import"
pattern: "AppHeader"
---
<objective>
Build the header (with desktop nav, mobile drawer, language/theme toggles), footer, and default layout.
Purpose: Provide the visible SSR shell that wraps all pages — navigation, toggles, and footer are functional.
Output: AppHeader.vue, AppFooter.vue, default.vue layout, updated app.vue.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- From app.config.ts (created by Plan 01): primary color = 'brand' (#85cb85) -->
<!-- From app/locales/fr.json (created by Plan 01): keys nav.*, footer.*, a11y.* -->
<!-- From nuxt.config.ts: colorMode configured with cookie, i18n with prefix_except_default -->
<!-- From src/config/site.ts: social links array with Gitea, LinkedIn, Discord, Email -->
<!-- Nuxt UI v3 components available (auto-imported): UDrawer, UButton, UIcon, UNavigationMenu -->
<!-- @nuxtjs/i18n composables: useI18n(), useSetLocale(), useSwitchLocalePath(), useLocalePath() -->
<!-- @nuxtjs/color-mode composable: useColorMode() -->
<!-- Nuxt Icon sets: heroicons:*, simple-icons:* -->
<!-- User post-research decision: Footer social icon uses Gitea icon (simple-icons:gitea), NOT GitHub -->
<!-- Social links from src/config/site.ts: Gitea (gitea.kamisama.ovh), LinkedIn, Discord, Email -->
<!-- D-05 says: GitHub, LinkedIn, Fiverr — BUT user corrected to Gitea. Use: Gitea, LinkedIn, Fiverr -->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: AppHeader with nav, language toggle, theme toggle, mobile drawer</name>
<files>app/components/layout/AppHeader.vue</files>
<read_first>
- src/components/layout/AppHeader.vue (old header — migration reference for structure and nav links)
- .planning/phases/02-ssr-shell/02-UI-SPEC.md (Component Inventory: AppHeader, LanguageToggle, ThemeToggle, MobileDrawer specs; Interaction States table; Copywriting Contract for aria-labels)
- .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 2: Language Switcher with useSetLocale; Pattern 1: ThemeToggle with useColorMode)
- app/locales/fr.json (verify nav.* and a11y.* keys exist from Plan 01)
- src/config/site.ts (check logo image path — public/images/logo.webp)
</read_first>
<action>
Create `app/components/layout/AppHeader.vue` as a single-file component containing:
**Structure (per D-01, D-03):**
- `<header>` with `class="sticky top-0 z-[1020] bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800"`
- Inner wrapper: `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">`
**Left — Logo:**
- `<NuxtLink to="localePath('/')" :aria-label="t('a11y.logoLabel')">`
- Contains `<NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="40" height="40" loading="eager" />` + `<span class="text-lg font-semibold">Killian</span>`
**Center-Right — Desktop nav (hidden md:flex):**
- Use `<nav>` with `<NuxtLink>` for each route: home(/), projects(/projects), about(/about), contact(/contact), fiverr(/fiverr), formation(/formation)
- Use `useLocalePath()` to generate locale-aware paths
- Active link detection: `class` binding comparing `route.path` with `localePath(path)`
- Active state: `border-b-2 border-primary-500` accent underline
- Default state: `text-gray-700 dark:text-gray-300`
- Hover state: `hover:text-primary-500`
- Focus: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`
- Nav link labels from `t('nav.home')`, `t('nav.projects')`, etc.
- `aria-current="page"` on active link
**Far Right — Toggles:**
Language toggle (per D-04 — simple text button FR/EN):
- `<button>` displaying `locale === 'fr' ? 'EN' : 'FR'` (shows the OTHER language to switch to)
- `class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 font-medium hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 transition-colors"`
- Click handler: `const setLocale = useSetLocale(); setLocale(locale.value === 'fr' ? 'en' : 'fr')`
- `:aria-label="t('a11y.langToggle')"`
Theme toggle (per D-09):
- `<button>` with `<UIcon>`: show `heroicons:sun` when dark mode active (clicking switches to light), `heroicons:moon` when light mode active
- Icon size: `class="w-5 h-5"`
- Button: `class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-600 dark:text-gray-400 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 transition-colors duration-300"`
- Click: `const colorMode = useColorMode(); colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'`
- `:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"`
Hamburger button (md:hidden):
- `<button @click="drawerOpen = true" class="md:hidden min-w-11 min-h-11 ..." :aria-label="t('a11y.openMenu')">`
- `<UIcon name="heroicons:bars-3" class="w-6 h-6" />`
**Mobile Drawer (per D-02):**
- `<UDrawer v-model:open="drawerOpen" side="left">`
- Inside: close button with `<UIcon name="heroicons:x-mark" />` and `:aria-label="t('a11y.closeDrawer')"`
- Nav links stacked full-width, same routes as desktop
- Language toggle and theme toggle at bottom
- Click any nav link sets `drawerOpen = false`
**Script setup:**
```typescript
const { t, locale } = useI18n()
const localePath = useLocalePath()
const setLocale = useSetLocale()
const colorMode = useColorMode()
const route = useRoute()
const drawerOpen = ref(false)
const navLinks = computed(() => [
{ key: 'home', path: '/' },
{ key: 'projects', path: '/projects' },
{ key: 'about', path: '/about' },
{ key: 'contact', path: '/contact' },
{ key: 'fiverr', path: '/fiverr' },
{ key: 'formation', path: '/formation' },
])
function toggleLocale() {
setLocale(locale.value === 'fr' ? 'en' : 'fr')
}
function toggleTheme() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
```
</action>
<verify>
<automated>grep -q "useColorMode" app/components/layout/AppHeader.vue && grep -q "useSetLocale\|setLocale" app/components/layout/AppHeader.vue && grep -q "UDrawer" app/components/layout/AppHeader.vue && grep -q "sticky" app/components/layout/AppHeader.vue && grep -q "a11y.logoLabel" app/components/layout/AppHeader.vue && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- app/components/layout/AppHeader.vue exists and contains `sticky top-0`
- Contains `useColorMode()` for theme toggle
- Contains `useSetLocale()` or `setLocale` for language switching
- Contains `UDrawer` for mobile navigation
- Contains `z-[1020]` for z-index
- Contains `heroicons:sun` and `heroicons:moon` for theme icons
- Contains `heroicons:bars-3` for hamburger
- Contains `t('a11y.logoLabel')` for logo aria-label
- Contains `localePath` for locale-aware routing
- Contains `min-w-11 min-h-11` on interactive buttons (44px touch targets)
- Contains `aria-current` for active nav link
- Contains `focus-visible:ring-2` on interactive elements
</acceptance_criteria>
<done>AppHeader renders sticky header with desktop nav links, FR/EN text toggle using setLocale, dark/light icon toggle using useColorMode, and mobile UDrawer. All interactive elements have WCAG touch targets, focus rings, and ARIA labels from i18n.</done>
</task>
<task type="auto">
<name>Task 2: AppFooter + default layout + app.vue update</name>
<files>app/components/layout/AppFooter.vue, app/layouts/default.vue, app/app.vue</files>
<read_first>
- src/components/layout/AppFooter.vue (old footer — migration reference)
- src/config/site.ts (social links: Gitea, LinkedIn, Discord, Email — note user wants Gitea icon not GitHub, and D-05 specifies Fiverr link too)
- .planning/phases/02-ssr-shell/02-UI-SPEC.md (AppFooter spec, Interaction States for social icons)
- .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 3: useLocaleHead in app.vue)
- app/app.vue (current state — has useHead with htmlAttrs lang)
- app/locales/fr.json (verify footer.* and a11y.* keys)
</read_first>
<action>
1. Create `app/components/layout/AppFooter.vue`:
Per D-05: single band footer — copyright + social icons.
User post-research decision: use Gitea icon (not GitHub). Social links: Gitea (gitea.kamisama.ovh/kayjaydee), LinkedIn (linkedin.com/in/killian-dal-cin), Fiverr (fiverr.com/users/mr_kayjaydee).
```vue
<script setup lang="ts">
const { t } = useI18n()
const socialLinks = [
{ name: 'gitea', url: 'https://gitea.kamisama.ovh/kayjaydee', icon: 'simple-icons:gitea', ariaKey: 'a11y.github' },
{ name: 'linkedin', url: 'https://linkedin.com/in/killian-dal-cin', icon: 'simple-icons:linkedin', ariaKey: 'a11y.linkedin' },
{ name: 'fiverr', url: 'https://www.fiverr.com/users/mr_kayjaydee', icon: 'simple-icons:fiverr', ariaKey: 'a11y.fiverr' },
]
</script>
```
Template:
- `<footer class="py-6 bg-gray-100 dark:bg-gray-800">`
- Inner: `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row items-center justify-between gap-4">`
- Left: `<p class="text-sm text-gray-600 dark:text-gray-400">{{ t('footer.copyright') }}</p>`
- Right: social icons in flex row, each `<a :href="link.url" target="_blank" rel="noopener noreferrer" :aria-label="t(link.ariaKey)">`
- Icon: `<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" />`
- Focus ring on each `<a>`: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`
Note: a11y.github key text says "GitHub de Killian' DAL-CIN" but links to Gitea — the executor should update the a11y key in fr.json/en.json to say "Gitea" instead of "GitHub" if not already correct. Check and fix if needed.
2. Create `app/layouts/default.vue` (per D-15):
```vue
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<main class="flex-1">
<slot />
</main>
<AppFooter />
</div>
</template>
```
3. Update `app/app.vue` to use the default layout and add useLocaleHead() for global hreflang/canonical (per RESEARCH Pattern 3):
```vue
<script setup lang="ts">
const { locale } = useI18n()
const head = useLocaleHead({ addSeoAttributes: true })
useHead({
htmlAttrs: { lang: locale },
link: computed(() => head.value.link || []),
meta: computed(() => head.value.meta || []),
})
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
Remove the existing `<NuxtRouteAnnouncer />` and `<div>` wrapper — the layout handles structure now.
</action>
<verify>
<automated>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"</automated>
</verify>
<acceptance_criteria>
- 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 `<AppHeader />` and `<AppFooter />`
- app/layouts/default.vue contains `<slot />`
- app/app.vue contains `useLocaleHead({ addSeoAttributes: true })`
- app/app.vue contains `<NuxtLayout>` wrapping `<NuxtPage />`
- app/app.vue does NOT contain `<NuxtRouteAnnouncer />`
</acceptance_criteria>
<done>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().</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `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>` and `<footer>` elements
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/02-ssr-shell/02-02-SUMMARY.md`
</output>
@@ -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
-212
View File
@@ -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 <title> 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"
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- From app/locales/fr.json (Plan 01): seo.home.title, seo.home.description, seo.projects.title, etc. -->
<!-- From nuxt.config.ts (Plan 01): site.url = 'https://killiandalcin.fr' -->
<!-- From public/og-image.png (Plan 01): static og:image file -->
<!-- Nuxt built-in: useSeoMeta(), useHead() — auto-imported -->
<!-- @nuxtjs/i18n: useI18n() for t() function -->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Per-route SEO metadata on all page stubs</name>
<files>app/pages/index.vue, app/pages/projects.vue, app/pages/about.vue, app/pages/contact.vue, app/pages/fiverr.vue, app/pages/formation.vue</files>
<read_first>
- 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)
</read_first>
<action>
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
<script setup lang="ts">
const { t } = useI18n()
useSeoMeta({
title: () => t('seo.projects.title'),
description: () => t('seo.projects.description'),
ogTitle: () => t('seo.projects.title'),
ogDescription: () => t('seo.projects.description'),
ogImage: 'https://killiandalcin.fr/og-image.png',
ogImageWidth: 1200,
ogImageHeight: 630,
ogType: 'website',
})
</script>
<template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-2xl font-bold">{{ t('nav.projects') }}</h1>
<p class="text-gray-600 dark:text-gray-400 mt-4">Phase 3 content placeholder</p>
</div>
</template>
```
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:
- `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">` wrapper (per D-16)
- An `<h1>` using the nav translation key
- A placeholder paragraph
</action>
<verify>
<automated>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"</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `pnpm dev` then `curl http://localhost:3000` returns HTML containing `<title>`, `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/02-ssr-shell/02-03-SUMMARY.md`
</output>
@@ -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
-125
View File
@@ -1,125 +0,0 @@
# Phase 2: SSR Shell - Context
**Gathered:** 2026-04-08
**Status:** Ready for planning
<domain>
## 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).
</domain>
<decisions>
## 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
</decisions>
<canonical_refs>
## 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
</canonical_refs>
<code_context>
## 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
</code_context>
<specifics>
## 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
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 02-ssr-shell*
*Context gathered: 2026-04-08*
@@ -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.
@@ -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>
## 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
</user_constraints>
---
<phase_requirements>
## 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 |
</phase_requirements>
---
## 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 + <slot /> + 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 `<html>` 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 (50950), 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 `<xhtml:link>` 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 `<link rel="alternate">` 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 `<link rel="alternate">` — 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 `<NuxtLink>` 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
<!-- Source: i18n.nuxtjs.org/docs/guide/seo [CITED] -->
<script setup lang="ts">
const { locale } = useI18n()
const head = useLocaleHead({ addSeoAttributes: true })
useHead({
htmlAttrs: {
lang: computed(() => head.value.htmlAttrs?.lang ?? locale.value),
},
link: computed(() => head.value.link ?? []),
meta: computed(() => head.value.meta ?? []),
})
</script>
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
```
### app/layouts/default.vue
```vue
<!-- Source: Nuxt 4 layouts docs [CITED: nuxt.com/docs/guide/directory-structure/layouts] -->
<template>
<div class="min-h-screen flex flex-col bg-white dark:bg-gray-900">
<AppHeader />
<main class="flex-1">
<slot />
</main>
<AppFooter />
</div>
</template>
```
### LanguageToggle snippet
```vue
<!-- Source: i18n.nuxtjs.org/docs/guide/lang-switcher [CITED] -->
<script setup lang="ts">
const { locale } = useI18n()
const setLocale = useSetLocale()
function toggleLocale() {
setLocale(locale.value === 'fr' ? 'en' : 'fr')
}
</script>
<template>
<button
class="min-w-11 min-h-11 ..."
:aria-label="locale === 'fr'
? 'Changer la langue actuellement Français'
: 'Change language currently English'"
@click="toggleLocale"
>
{{ locale.toUpperCase() }}
</button>
</template>
```
### 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 '<title>[^<]*</title>'` |
| 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 '(<title>\|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 '(<title>|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)
-262
View File
@@ -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: `<header>` 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 `<nav>` with `<NuxtLink>` — 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 `<button>` 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 |
|-----|-------------|
| `<title>` | Per-route via `useSeoMeta({ title })` |
| `<meta name="description">` | Per-route, max 160 chars |
| `<meta property="og:title">` | Same as title |
| `<meta property="og:description">` | Same as description |
| `<meta property="og:image">` | Absolute URL via nuxt-og-image (D-12) |
| `<link rel="canonical">` | Absolute URL for current locale route |
| `<link rel="alternate" hreflang="fr">` | FR URL |
| `<link rel="alternate" hreflang="en">` | 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*
@@ -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 `<html lang="fr">` and English HTML with `<html lang="en">`, 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 `<xhtml:link rel="alternate" hreflang="fr" .../>` 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)_
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
@C:\Users\minit\.claude\get-shit-done\templates\summary.md
</execution_context>
<context>
@.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
<interfaces>
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<Project[]>; featuredProjects: ComputedRef<Project[]>; filterByCategory(cat: string): ComputedRef<Project[]>; search(query: Ref<string> | string): ComputedRef<Project[]>; findById(id: string): ComputedRef<Project | undefined> }
```
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 }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Installer deps, migrer site config, ajouter UApp, configurer runtimeConfig SMTP</name>
<files>package.json, package-lock.json, app/data/site.ts, shared/types/index.ts, nuxt.config.ts, app/app.vue</files>
<action>
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 `<UApp>` — requis pour que `useToast()` fonctionne (per D-10, RESEARCH.md Pitfall 1) :
```vue
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
```
Conserver le `<script setup>` existant (useLocaleHead, useHead).
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && node -e "require('nodemailer'); require('zod'); console.log('deps OK')" && grep -q "smtpHost" nuxt.config.ts && grep -q "UApp" app/app.vue && echo "PASS"</automated>
</verify>
<done>nodemailer et zod installes, site config migree dans app/data/site.ts, runtimeConfig SMTP ajoute (section privee), app.vue wrappe avec UApp</done>
</task>
<task type="auto">
<name>Task 2: Creer les composants partages — sections landing + ProjectCard + TechBadge + ProjectGallery</name>
<files>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</files>
<action>
Migrer chaque composant depuis src/ vers app/components/ en utilisant Nuxt UI v3 et les auto-imports Nuxt. Pour chaque composant :
- Remplacer les imports manuels (`import { useI18n } from '@/composables/useI18n'`) par les auto-imports Nuxt (`const { t } = useI18n()` direct)
- Remplacer `RouterLink` par `NuxtLink`
- Remplacer les classes CSS custom par du Tailwind + composants Nuxt UI
- Remplacer `getImageUrl()` par `NuxtImg` avec `loading="lazy"` et `format="webp"`
**HeroSection.vue** (per D-02) : Texte seul — titre (`t('home.title')`), sous-titre (`t('home.subtitle')`), 3 boutons CTA (Projets, Fiverr, Contact) avec `UButton`. Pas d'image, pas d'animation.
**FeaturedProjectsSection.vue** (per D-03) : Utiliser `useProjects().featuredProjects` pour obtenir les 3 projets featured. Afficher avec `ProjectCard`. Titre et sous-titre via i18n.
**ServicesSection.vue** : Migrer les 4 cards services (webDev, mobileApps, optimization, maintenance) avec `UCard`. Icones via lucide icon names dans UIcon si dispo, sinon SVG inline.
**TestimonialsSection.vue** (per COMP-04) : Utiliser `UCard` pour chaque temoignage. Importer `testimonials` et `testimonialsStats` depuis `~/data/testimonials`. Props i18n pour titre, sous-titre, stats labels. Afficher rating avec etoiles, contenu, nom, role, date.
**FAQSection.vue** (per COMP-03, D-18) : Utiliser `UAccordion` avec `:items` array. Chaque item : `{ label: t(faq.questionKey), content: t(faq.answerKey), value: faq.questionKey }`. Props : `faqs: FAQ[]`, `title: string`, `subtitle: string`. Pattern exact du RESEARCH.md Pattern 4. Ajouter `type="single" collapsible`.
**CTASection.vue** : Section CTA finale avec titre, sous-titre et bouton UButton vers /contact. Tout i18n.
**ProjectCard.vue** : Migrer depuis src/. Utiliser `NuxtLink` vers `/project/${project.id}`. Afficher image avec `NuxtImg`, categorie traduite, titre traduit, description traduite, badges technologies (3 max + "+N"), bouton "Voir projet". Schema.org microdata conserve.
**TechBadge.vue** (per D-17) : Migrer depuis src/. Accepter `Technology | string` en prop. Lookup dans techStack pour resoudre les strings. Afficher image + nom + niveau optionnel. Utiliser `NuxtImg` pour les images tech.
**ProjectGallery.vue** (per D-05, D-06, D-07, COMP-01) : Nouveau composant utilisant UModal + UCarousel du RESEARCH.md Pattern 1. Implementation exacte :
- Props : `gallery: string[]`, `projectTitle: string`
- `isOpen` ref + `currentIndex` ref
- `useTemplateRef('carousel')` pour acceder a `emblaApi`
- `openGallery(index)` : set currentIndex, isOpen=true, `nextTick(() => carouselRef.value?.emblaApi?.scrollTo(index, true))`
- `goTo(index)` : set currentIndex, scrollTo
- Navigation clavier : `onMounted` keydown listener — ArrowRight/ArrowLeft/Escape (per D-07)
- `onUnmounted` cleanup du listener
- Template : `UModal v-model:open fullscreen` > `UCarousel ref="carousel" :items="gallery" arrows loop` > `NuxtImg :src="item"`
- Thumbnails sous le carousel : boutons avec `NuxtImg :src="img" width="80" height="60"`, ring-2 ring-primary sur le courant (per D-06)
- Expose `openGallery` via `defineExpose({ openGallery })` pour que la page parent puisse l'appeler
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && ls 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 && echo "ALL FILES EXIST"</automated>
</verify>
<done>9 composants crees : 6 sections landing + ProjectCard + TechBadge + ProjectGallery avec UModal+UCarousel+thumbnails+keyboard nav</done>
</task>
<task type="auto">
<name>Task 3: Creer ContactForm + server route nodemailer SMTP</name>
<files>app/components/ContactForm.vue, server/api/contact.post.ts</files>
<action>
**server/api/contact.post.ts** (per D-11, COMP-02) : Route serveur Nuxt avec nodemailer. Implementation exacte du RESEARCH.md Pattern 3 :
```typescript
import nodemailer from 'nodemailer'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const config = useRuntimeConfig(event)
// Validation cote serveur (per RESEARCH.md Security)
const { name, email, message } = body
if (!name || typeof name !== 'string' || name.length < 2 || name.length > 100) {
throw createError({ statusCode: 400, message: 'Invalid name' })
}
if (!email || typeof email !== 'string' || !email.includes('@') || email.length > 200) {
throw createError({ statusCode: 400, message: 'Invalid email' })
}
if (!message || typeof message !== 'string' || message.length < 10 || message.length > 5000) {
throw createError({ statusCode: 400, message: 'Invalid message' })
}
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 - ${name}`,
text: `De: ${name} <${email}>\n\n${message}`,
html: `<p><strong>De:</strong> ${name} &lt;${email}&gt;</p><p>${message.replace(/\n/g, '<br>')}</p>`,
})
return { success: true }
})
```
IMPORTANT : `server/` a la racine du projet (meme niveau que `app/`), PAS dans `app/server/` (per RESEARCH.md Pitfall 5).
**ContactForm.vue** (per D-08, D-09, D-10, COMP-02) : Implementation exacte du RESEARCH.md Pattern 2 :
- Schema Zod : name min(2), email .email(), message min(10) — 3 champs seulement (per D-08)
- State reactive `Partial<Schema>` avec name, email, message undefined
- `useToast()` pour feedback (per D-10) — succes en vert, erreur en rouge
- `$fetch('/api/contact', { method: 'POST', body: event.data })` sur submit
- Loading state sur le bouton submit
- Template : `UForm :schema :state @submit` > 3x `UFormField` > `UInput` pour nom/email + `UTextarea rows="5"` pour message > `UButton type="submit" :loading`
- Labels i18n : `t('contact.form.name')`, `t('contact.form.email')`, `t('contact.form.message')`, `t('contact.form.submit')`
- Toast messages i18n : `t('contact.form.success')`, `t('contact.form.error')`
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && ls server/api/contact.post.ts app/components/ContactForm.vue && grep -q "nodemailer" server/api/contact.post.ts && grep -q "UForm" app/components/ContactForm.vue && grep -q "zod" app/components/ContactForm.vue && echo "PASS"</automated>
</verify>
<done>ContactForm.vue cree avec UForm+Zod+UToast, server/api/contact.post.ts cree avec nodemailer SMTP + validation serveur</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client -> server/api/contact.post.ts | Donnees formulaire non fiables traversent vers le serveur SMTP |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-01 | Spoofing | server/api/contact.post.ts | mitigate | Validation Zod cote client + validation longueur/type cote serveur dans readBody |
| T-03-02 | Tampering | server/api/contact.post.ts HTML email | mitigate | Echapper le HTML dans le corps email — remplacer newlines par `<br>` mais ne pas injecter de HTML brut utilisateur |
| T-03-03 | Information Disclosure | nuxt.config.ts runtimeConfig | mitigate | Credentials SMTP dans section privee runtimeConfig uniquement (jamais public) |
| T-03-04 | Denial of Service | server/api/contact.post.ts | accept | Pas de rate limiting en Phase 3 — endpoint public, risque de spam faible pour un portfolio. Mitigation partielle : validation longueur message max 5000 chars |
</threat_model>
<verification>
- `npm run build` passe sans erreur TypeScript
- `npx nuxi dev` demarre et les composants sont auto-importes
- `curl -X POST http://localhost:3000/api/contact -H "Content-Type: application/json" -d '{"name":"Test","email":"test@test.com","message":"Test message long enough"}' ` retourne `{"success":true}` (avec .env SMTP configure) ou une erreur SMTP lisible
</verification>
<success_criteria>
- nodemailer et zod dans package.json dependencies
- app/data/site.ts exporte siteConfig type
- 9 composants sections/partages existent dans app/components/
- ProjectGallery utilise UModal + UCarousel + thumbnails + keydown listener
- ContactForm utilise UForm + Zod schema + useToast
- server/api/contact.post.ts utilise nodemailer avec runtimeConfig prive
- app.vue contient UApp wrapper
- nuxt.config.ts contient smtpHost/smtpUser/smtpPass/smtpTo dans runtimeConfig (pas public)
</success_criteria>
<output>
After completion, create `.planning/phases/03-pages-ship/03-01-SUMMARY.md`
</output>
@@ -1,78 +0,0 @@
---
phase: 03-pages-ship
plan: 01
subsystem: shared-components
tags: [components, nodemailer, zod, nuxt-ui, gallery, contact-form]
dependency_graph:
requires: [02-03-PLAN]
provides: [shared-components, contact-api, site-config]
affects: [03-02-PLAN, 03-03-PLAN]
tech_stack:
added: [nodemailer, zod, "@types/nodemailer"]
patterns: [UModal+UCarousel gallery, UForm+Zod validation, UAccordion FAQ, nodemailer SMTP]
key_files:
created:
- app/data/site.ts
- app/components/sections/HeroSection.vue
- app/components/sections/FeaturedProjectsSection.vue
- app/components/sections/ServicesSection.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
modified:
- package.json
- package-lock.json
- shared/types/index.ts
- nuxt.config.ts
- app/app.vue
decisions:
- "SiteConfig interfaces added to shared/types for cross-layer access"
- "HTML escaping added to email body to mitigate T-03-02 XSS threat"
- "Nuxt UI icons (i-lucide-*) used for services instead of SVG paths"
metrics:
duration: 239s
completed: 2026-04-08
tasks: 3
files: 17
---
# Phase 03 Plan 01: Shared Components + Deps + Contact Summary
Installed nodemailer/zod, migrated site config, created 9 shared UI components (6 landing sections + ProjectCard + TechBadge + ProjectGallery with UModal+UCarousel+thumbnails+keyboard), ContactForm with Zod validation and UToast, and nodemailer SMTP server route with HTML escaping.
## Task Results
| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 | Install deps, site config, runtimeConfig, UApp | 21450af | package.json, app/data/site.ts, nuxt.config.ts, app/app.vue |
| 2 | 9 shared components | 7f715e4 | app/components/sections/*.vue, ProjectCard, TechBadge, ProjectGallery |
| 3 | ContactForm + server route | 84e4202 | app/components/ContactForm.vue, server/api/contact.post.ts |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - Security] HTML escaping in email body (T-03-02)**
- **Found during:** Task 3
- **Issue:** Plan code sample used raw user input in HTML email body, enabling potential XSS
- **Fix:** Added HTML entity escaping for name and message before inserting into HTML email
- **Files modified:** server/api/contact.post.ts
- **Commit:** 84e4202
## Verification
- nodemailer and zod installed in package.json dependencies
- app/data/site.ts exports typed siteConfig
- 9 components exist in app/components/
- ProjectGallery uses UModal + UCarousel + thumbnails + keydown listener
- ContactForm uses UForm + Zod schema + useToast
- server/api/contact.post.ts uses nodemailer with private runtimeConfig
- app.vue wrapped with UApp
- nuxt.config.ts has smtpHost/smtpUser/smtpPass/smtpTo in private runtimeConfig
## Self-Check: PASSED
@@ -1,223 +0,0 @@
---
phase: 03-pages-ship
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- app/pages/index.vue
- app/pages/projects.vue
- app/pages/project/[id].vue
autonomous: true
requirements:
- PAGE-01
- PAGE-02
- PAGE-03
must_haves:
truths:
- "Landing page affiche 6 sections : Hero, FeaturedProjects, Services, Testimonials, FAQ, CTA"
- "Projects page filtre par recherche texte et boutons categorie"
- "Project detail affiche description, features, technologies, galerie modale"
- "Chaque page a ses metadonnees SEO via useSeoMeta()"
artifacts:
- path: "app/pages/index.vue"
provides: "Landing page avec 6 sections"
- path: "app/pages/projects.vue"
provides: "Liste projets avec filtres"
- path: "app/pages/project/[id].vue"
provides: "Detail projet avec galerie"
key_links:
- from: "app/pages/index.vue"
to: "app/components/sections/*.vue"
via: "auto-import composants"
pattern: "HeroSection|FeaturedProjectsSection|ServicesSection"
- from: "app/pages/project/[id].vue"
to: "app/components/ProjectGallery.vue"
via: "useTemplateRef + openGallery"
pattern: "ProjectGallery|openGallery"
---
<objective>
Construire les 3 pages principales du portfolio : Landing (accueil), Projects (liste), et Project Detail (detail + galerie). Ces pages consomment les composants crees en Plan 01.
Purpose: Ce sont les pages les plus visitees du portfolio — la landing convertit les visiteurs, la liste projets montre le travail, et le detail permet l'exploration approfondie.
Output: 3 pages fonctionnelles dans app/pages/.
</objective>
<execution_context>
@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
@C:\Users\minit\.claude\get-shit-done\templates\summary.md
</execution_context>
<context>
@.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-01-SUMMARY.md
@src/views/HomePage.vue
@src/views/ProjectsPage.vue
@src/views/ProjectDetailPage.vue
@app/composables/useProjects.ts
@app/data/projects.ts
@app/data/testimonials.ts
@app/data/faq.ts
<interfaces>
From app/composables/useProjects.ts:
```typescript
export function useProjects(): {
projects: ComputedRef<Project[]>
featuredProjects: ComputedRef<Project[]>
filterByCategory(cat: string): ComputedRef<Project[]>
search(query: Ref<string> | string): ComputedRef<Project[]>
findById(id: string): ComputedRef<Project | undefined>
}
```
From app/data/faq.ts:
```typescript
export const homeFAQs: FAQ[] // { questionKey, answerKey, featuresKey }
```
From app/data/testimonials.ts:
```typescript
export const testimonials: Testimonial[]
export const testimonialsStats: TestimonialsStats
```
Composants disponibles (auto-importes, crees en Plan 01):
- HeroSection — pas de props (utilise i18n interne)
- FeaturedProjectsSection — pas de props (utilise useProjects interne)
- ServicesSection — pas de props (utilise i18n interne)
- TestimonialsSection — props: title, subtitle, testimonials, stats, statsLabels, ctaTitle, ctaSubtitle, ctaText, ctaLink
- FAQSection — props: faqs (FAQ[]), title, subtitle
- CTASection — pas de props (utilise i18n interne)
- ProjectCard — props: project (Project)
- ProjectGallery — props: gallery (string[]), projectTitle (string); expose: openGallery(index)
- TechBadge — props: tech (Technology | string), showLevel?, showImage?
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Page Landing (index.vue) avec 6 sections</name>
<files>app/pages/index.vue</files>
<action>
Remplacer le contenu stub de `app/pages/index.vue` par la page landing complete (per D-01, D-02, D-03).
Structure exacte — 6 sections dans cet ordre (per D-01) :
1. `<HeroSection />` — auto-importe, texte seul (per D-02)
2. `<FeaturedProjectsSection />` — auto-importe, 3 projets featured (per D-03)
3. `<ServicesSection />` — auto-importe
4. `<TestimonialsSection>` — passer les props i18n depuis `t()`, importer `testimonials` et `testimonialsStats` depuis `~/data/testimonials`
5. `<FAQSection>` — passer `homeFAQs` depuis `~/data/faq` et titres i18n
6. `<CTASection />` — auto-importe
SEO via `useSeoMeta()` : titre, description, og:title, og:description, og:image (per SEO-01). Conserver le JSON-LD Person + ProfessionalService deja present dans le stub via `useHead({ script })`.
Wrapper `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">` pour le contenu selon le layout Phase 2 (D-16 max-w-7xl).
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -c "Section" app/pages/index.vue | grep -q "[456789]" && grep -q "useSeoMeta" app/pages/index.vue && echo "PASS"</automated>
</verify>
<done>Page landing avec 6 sections dans l'ordre Hero > FeaturedProjects > Services > Testimonials > FAQ > CTA, SEO meta configurees</done>
</task>
<task type="auto">
<name>Task 2: Page Projects (projects.vue) avec filtres recherche + categorie</name>
<files>app/pages/projects.vue</files>
<action>
Remplacer le stub `app/pages/projects.vue` par la page projets complete (per D-04, PAGE-02).
Migrer depuis `src/views/ProjectsPage.vue` en adaptant pour Nuxt :
1. **Script setup** : `const { projects } = useProjects()` (auto-import). Refs : `searchQuery`, `selectedCategory` (defaut 'all'). Computed `categories` : `['all', ...new Set(projects.value.map(p => p.category))]`. Computed `filteredProjects` : filtre par searchQuery (titre, description, technologies) puis par selectedCategory.
2. **Template** :
- Section hero : titre `t('projects.title')`, sous-titre, stats (total projets, featured, categories)
- Section filtres (per D-04) : `UInput` pour recherche avec icone search (`icon="i-lucide-search"`) + boutons categorie `UButton` pour chaque categorie (variant `soft` pour inactif, `solid` pour actif). PAS de select/dropdown — boutons cliquables comme l'actuel.
- Grille projets : `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6`. Utiliser `<ProjectCard :project="project" />` pour chaque projet filtre.
- Etat vide : message "Aucun resultat" avec bouton reset filtres.
3. **SEO** : `useSeoMeta()` avec titre, description, og specifiques a la page projets.
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "filteredProjects" app/pages/projects.vue && grep -q "searchQuery" app/pages/projects.vue && grep -q "ProjectCard" app/pages/projects.vue && echo "PASS"</automated>
</verify>
<done>Page projects avec recherche texte + filtres categorie boutons, grille ProjectCard, etat vide, SEO meta</done>
</task>
<task type="auto">
<name>Task 3: Page Project Detail (project/[id].vue) avec galerie modale</name>
<files>app/pages/project/[id].vue</files>
<action>
Creer `app/pages/project/[id].vue` — route dynamique (per PAGE-03).
Migrer depuis `src/views/ProjectDetailPage.vue` :
1. **Script setup** :
- `const route = useRoute()` puis `const { findById } = useProjects()`
- `const project = findById(route.params.id as string)`
- 404 si non trouve : `if (!project.value) throw createError({ status: 404, statusText: 'Project not found' })` (per RESEARCH.md Code Examples)
- `const galleryRef = useTemplateRef('gallery')` pour acceder a ProjectGallery.openGallery
- Computed `relatedProjects` : meme categorie, exclure le projet courant, slice(0, 3)
- `useSeoMeta()` avec titre du projet, description
2. **Template** :
- Breadcrumb : `UButton variant="link"` vers /projects avec icone fleche retour
- Hero grid 2 colonnes : image principale `NuxtImg` a gauche, infos (categorie, date, titre, description, boutons CTA) a droite
- Boutons CTA : `UButton` pour demo, source code, boutons custom du projet
- Section "A propos" : longDescription ou description, liste features avec checkmarks
- Section technologies : grille `TechBadge` pour chaque tech
- Section galerie : grille thumbnails cliquables. Au clic sur une image : `galleryRef.value?.openGallery(index)`. Chaque thumbnail : `NuxtImg` avec overlay zoom au hover.
- Sidebar : card infos projet (date, categorie, status) + projets lies `NuxtLink`
- `<ProjectGallery ref="gallery" :gallery="project.gallery" :project-title="project.title" />` en bas du template
3. **Responsive** : layout 2 colonnes (main + sidebar) sur desktop, stack sur mobile.
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && test -f app/pages/project/\[id\].vue && grep -q "findById" app/pages/project/\[id\].vue && grep -q "ProjectGallery" app/pages/project/\[id\].vue && grep -q "createError" app/pages/project/\[id\].vue && echo "PASS"</automated>
</verify>
<done>Page project detail avec route dynamique [id], 404 si non trouve, galerie modale via ProjectGallery, projets lies, SEO meta</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| URL params -> findById | route.params.id est une entree utilisateur |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-05 | Tampering | project/[id].vue | mitigate | createError(404) si projet non trouve — pas d'injection possible via les donnees statiques |
</threat_model>
<verification>
- `npx nuxi dev` puis naviguer vers `/` — 6 sections visibles
- `/projects` — filtres fonctionnels, cards affichees
- `/project/flowboard` — detail avec galerie, clic image ouvre modal
- `/project/inexistant` — redirige vers page 404
- `curl http://localhost:3000/` contient les balises meta SEO
</verification>
<success_criteria>
- Landing affiche les 6 sections dans l'ordre correct (per D-01)
- Hero est texte seul, pas d'image (per D-02)
- 3 projets featured affiches (per D-03)
- Projects page a recherche texte + boutons categorie (per D-04)
- Project detail a galerie modale UModal+UCarousel fonctionnelle
- Route /project/[id] inexistant retourne 404
- Toutes les pages ont useSeoMeta()
</success_criteria>
<output>
After completion, create `.planning/phases/03-pages-ship/03-02-SUMMARY.md`
</output>
@@ -1,55 +0,0 @@
---
phase: 03-pages-ship
plan: 02
subsystem: pages
tags: [pages, landing, projects, project-detail, gallery, seo, nuxt-ui]
dependency_graph:
requires: [03-01-PLAN]
provides: [landing-page, projects-page, project-detail-page]
affects: [03-03-PLAN]
tech_stack:
added: []
patterns: [useSeoMeta per-page, useProjects composable, dynamic route [id], createError 404, useTemplateRef gallery]
key_files:
created:
- app/pages/project/[id].vue
modified:
- app/pages/index.vue
- app/pages/projects.vue
decisions:
- "TestimonialsSection uses internal data imports (no props needed from page)"
- "Hero section placed outside max-w-7xl wrapper for full-width, other sections inside"
- "Category filter uses UButton solid/soft variants instead of select dropdown (per D-04)"
metrics:
duration: 103s
completed: 2026-04-08
tasks: 3
files: 3
---
# Phase 03 Plan 02: Main Pages (Landing + Projects + Detail) Summary
Built 3 main portfolio pages: landing with 6 sections (Hero/FeaturedProjects/Services/Testimonials/FAQ/CTA), projects list with text search and category filter buttons using UInput/UButton, and project detail with dynamic [id] route, 404 handling via createError, gallery thumbnails opening ProjectGallery modal, tech badges, features list, sidebar with related projects.
## Task Results
| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 | Landing page with 6 sections | a4b53ca | app/pages/index.vue |
| 2 | Projects page with search + category filters | 8e9c6c7 | app/pages/projects.vue |
| 3 | Project detail with gallery modal | af12fa5 | app/pages/project/[id].vue |
## Deviations from Plan
None - plan executed exactly as written.
## Verification
- index.vue contains 6 section components in correct order: Hero > FeaturedProjects > Services > Testimonials > FAQ > CTA
- index.vue preserves useSeoMeta and JSON-LD Person + ProfessionalService from Phase 2
- projects.vue has searchQuery, filteredProjects, selectedCategory, ProjectCard grid
- projects.vue uses UInput with search icon + UButton category filters (not select dropdown)
- project/[id].vue uses findById, createError(404), ProjectGallery with useTemplateRef
- project/[id].vue has relatedProjects, TechBadge, features with checkmarks, sidebar
## Self-Check: PASSED
@@ -1,233 +0,0 @@
---
phase: 03-pages-ship
plan: 03
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- app/pages/about.vue
- app/pages/contact.vue
- app/pages/fiverr.vue
- app/error.vue
autonomous: true
requirements:
- PAGE-04
- PAGE-05
- PAGE-06
- PAGE-08
must_haves:
truths:
- "About page affiche bio + tech stack badges par categorie"
- "Contact page affiche formulaire 3 champs + infos contact + reseaux sociaux"
- "Fiverr page affiche hero + service cards + FAQ accordion + CTA"
- "404 page affiche code erreur + message + bouton retour accueil"
artifacts:
- path: "app/pages/about.vue"
provides: "Page about avec bio et tech stack"
- path: "app/pages/contact.vue"
provides: "Page contact avec formulaire"
- path: "app/pages/fiverr.vue"
provides: "Page fiverr avec services"
- path: "app/error.vue"
provides: "Page 404 custom"
key_links:
- from: "app/pages/contact.vue"
to: "app/components/ContactForm.vue"
via: "auto-import"
pattern: "ContactForm"
- from: "app/pages/fiverr.vue"
to: "app/components/sections/FAQSection.vue"
via: "auto-import"
pattern: "FAQSection"
---
<objective>
Construire les 4 pages restantes : About, Contact, Fiverr, et error.vue (404). Ces pages consomment les composants partages du Plan 01.
Purpose: Complete le portfolio avec toutes les pages necessaires — About pour la credibilite, Contact pour la conversion, Fiverr pour les services, 404 pour l'UX.
Output: 4 pages fonctionnelles.
</objective>
<execution_context>
@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
@C:\Users\minit\.claude\get-shit-done\templates\summary.md
</execution_context>
<context>
@.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-01-SUMMARY.md
@src/views/AboutPage.vue
@src/views/ContactPage.vue
@src/views/FiverrPage.vue
@app/data/techstack.ts
@app/data/faq.ts
@src/config/site.ts
<interfaces>
Composants disponibles (auto-importes, crees en Plan 01):
- ContactForm — pas de props (formulaire autonome avec Zod + useToast)
- FAQSection — props: faqs (FAQ[]), title, subtitle
- TechBadge — props: tech (Technology | string), showLevel?, showImage?
From app/data/site.ts (cree en Plan 01):
```typescript
export const siteConfig: SiteConfig
// siteConfig.contact: { email, phone, location }
// siteConfig.social: SocialLink[]
// siteConfig.fiverr: { profileUrl, services: FiverrService[] }
```
From app/data/techstack.ts:
```typescript
export const techStack: TechStack
// .programming, .front, .database, .devtools, .operating_systems, .socials
```
From app/data/faq.ts:
```typescript
export const homeFAQs: FAQ[]
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Pages About + Contact</name>
<files>app/pages/about.vue, app/pages/contact.vue</files>
<action>
**about.vue** (per D-16, D-17, PAGE-04) : Migrer depuis src/views/AboutPage.vue.
1. Section hero : titre `t('about.title')`, sous-titre, intro content (2 paragraphes bio)
2. Section Skills : 4 categories tech (programming, front, database, devtools) en grille 2x2 avec `UCard`. Chaque card : icone, titre categorie, grille `TechBadge` avec `showLevel`. Section OS separee en bas.
3. Section Approach : 4 cards (performance, architecture, quality, collaboration) avec `UCard`, icones lucide.
4. Section CTA : titre + 2 boutons (Contact, Projets) via `UButton`.
5. `useSeoMeta()` avec titre/description about.
**contact.vue** (per D-08, D-10, D-16, PAGE-05) : Migrer depuis src/views/ContactPage.vue.
1. Section hero : titre `t('contact.title')`, sous-titre, stats (24-48h response, 100% satisfaction, Remote)
2. Layout 2 colonnes :
- Colonne gauche : `<ContactForm />` (auto-importe du Plan 01, gere tout seul — Zod, $fetch, UToast)
- Colonne droite : Infos contact (`UCard` avec email cliquable, telephone, localisation depuis `siteConfig.contact`) + Reseaux sociaux (`UCard` avec liens `siteConfig.social` — icones pour Gitea/LinkedIn/Discord)
3. Section FAQ en bas : 3 cards info (temps reponse, types projets, collaboration) avec `UCard`.
4. `useSeoMeta()` specifique contact.
Importer `siteConfig` depuis `~/data/site`.
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "TechBadge" app/pages/about.vue && grep -q "techStack" app/pages/about.vue && grep -q "ContactForm" app/pages/contact.vue && grep -q "siteConfig" app/pages/contact.vue && echo "PASS"</automated>
</verify>
<done>About page avec bio + 5 categories tech stack badges, Contact page avec ContactForm + infos contact + reseaux sociaux</done>
</task>
<task type="auto">
<name>Task 2: Page Fiverr avec hero + services + FAQ accordion + CTA</name>
<files>app/pages/fiverr.vue</files>
<action>
Migrer depuis src/views/FiverrPage.vue (per D-18, PAGE-06).
1. **Script setup** : Importer `siteConfig` depuis `~/data/site`. Computed `services` = `siteConfig.fiverr.services`. Computed `heroStats` avec nombre services dispo + rating "5 etoiles".
2. **Section Hero** : titre `t('fiverr.title')`, sous-titre, stats (services count, rating), bouton CTA `UButton` vers `siteConfig.fiverr.profileUrl` (external link, target blank).
3. **Section Services** : grille de service cards. Pour chaque service dans `siteConfig.fiverr.services`, utiliser `UCard` avec :
- Image service via `NuxtImg :src="service.image"`
- Badge prix : `t('fiverr.pricing.startingAt') + ' ' + service.price`
- Badge statut : "Disponible" (vert) si url !== '#', "Bientot" (jaune) sinon
- Titre et description via `t('fiverr.serviceData.${service.id}.title/description')`
- Features liste via i18n (recuperer du fichier de traduction comme dans src/views/FiverrPage.vue)
- Bouton commander / en savoir plus
4. **Section FAQ** (per D-18) : Utiliser `<FAQSection>` du Plan 01 avec les FAQs fiverr. Creer un array computed de FAQ fiverr depuis les cles i18n `fiverr.faq.*` si elles existent, sinon reutiliser `homeFAQs`.
5. **Section CTA finale** : titre `t('fiverr.cta.title')`, sous-titre, bouton vers profil Fiverr.
6. `useSeoMeta()` specifique fiverr.
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "siteConfig" app/pages/fiverr.vue && grep -q "fiverr" app/pages/fiverr.vue && grep -q "FAQSection\|UAccordion" app/pages/fiverr.vue && echo "PASS"</automated>
</verify>
<done>Page Fiverr avec hero stats, service cards, FAQ accordion UAccordion, CTA vers profil Fiverr</done>
</task>
<task type="auto">
<name>Task 3: Page 404 (error.vue)</name>
<files>app/error.vue</files>
<action>
Creer `app/error.vue` (per D-20, PAGE-08). IMPORTANT : dans `app/`, PAS dans `app/pages/` (per RESEARCH.md Pitfall 6).
Implementation exacte du RESEARCH.md Pattern 5 :
```vue
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps<{ error: NuxtError }>()
const { t } = useI18n()
function handleError() {
clearError({ redirect: '/' })
}
</script>
<template>
<div class="min-h-screen flex flex-col items-center justify-center gap-6 px-4">
<h1 class="text-8xl font-bold text-primary">{{ error.statusCode }}</h1>
<p class="text-xl text-gray-500 dark:text-gray-400 text-center max-w-md">
{{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }}
</p>
<UButton size="lg" @click="handleError">
{{ t('error.backHome') }}
</UButton>
</div>
</template>
```
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`.
</action>
<verify>
<automated>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"</automated>
</verify>
<done>error.vue dans app/ avec affichage code erreur, message i18n, bouton retour accueil via clearError</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `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
</verification>
<success_criteria>
- 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()
</success_criteria>
<output>
After completion, create `.planning/phases/03-pages-ship/03-03-SUMMARY.md`
</output>
@@ -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
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:\Users\minit\.claude\get-shit-done\workflows\execute-plan.md
@C:\Users\minit\.claude\get-shit-done\templates\summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Dockerfile SSR multi-stage + docker-compose Traefik port 3000</name>
<files>Dockerfile, docker-compose.yml</files>
<action>
**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}
```
</action>
<verify>
<automated>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"</automated>
</verify>
<done>Dockerfile SSR multi-stage node:22-alpine avec .output/, docker-compose port 3000, variables env SMTP/GA4</done>
</task>
<task type="auto">
<name>Task 2: GA4 production-only + legacy cleanup</name>
<files>nuxt.config.ts</files>
<action>
**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.
</action>
<verify>
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "production" nuxt.config.ts && ! test -f app/pages/formation.vue && ! test -d src && echo "PASS"</automated>
</verify>
<done>GA4 nuxt-gtag actif en production via runtimeConfig, formation completement supprimee, legacy src/ et fichiers SPA supprimes</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `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)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/03-pages-ship/03-04-SUMMARY.md`
</output>
@@ -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
@@ -1,148 +0,0 @@
# Phase 3: Pages & Ship - Context
**Gathered:** 2026-04-08
**Status:** Ready for planning
<domain>
## 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.
</domain>
<decisions>
## 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
</decisions>
<canonical_refs>
## 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)
</canonical_refs>
<code_context>
## 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
</code_context>
<specifics>
## 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
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 03-pages-ship*
*Context gathered: 2026-04-08*
@@ -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>
## 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
</user_constraints>
---
<phase_requirements>
## 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' |
</phase_requirements>
---
## 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
<!-- Source : ui.nuxt.com/components/modal + ui.nuxt.com/components/carousel -->
<script setup lang="ts">
const isOpen = ref(false)
const currentIndex = ref(0)
const carouselRef = useTemplateRef('carousel')
function openGallery(index: number) {
currentIndex.value = index
isOpen.value = true
// Scroll to correct slide after modal opens
nextTick(() => {
carouselRef.value?.emblaApi?.scrollTo(index, true)
})
}
function goTo(index: number) {
currentIndex.value = index
carouselRef.value?.emblaApi?.scrollTo(index, true)
}
// Navigation clavier (D-07)
function onKeydown(e: KeyboardEvent) {
if (!isOpen.value) return
if (e.key === 'ArrowRight') carouselRef.value?.emblaApi?.scrollNext()
if (e.key === 'ArrowLeft') carouselRef.value?.emblaApi?.scrollPrev()
if (e.key === 'Escape') isOpen.value = false
}
onMounted(() => document.addEventListener('keydown', onKeydown))
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
</script>
<template>
<UModal v-model:open="isOpen" fullscreen>
<template #content>
<UCarousel
ref="carousel"
v-slot="{ item }"
:items="gallery"
arrows
loop
@select="(i) => (currentIndex = i)"
>
<NuxtImg :src="item" loading="lazy" />
</UCarousel>
<!-- Thumbnails -->
<div class="flex gap-2 mt-4 justify-center">
<button
v-for="(img, i) in gallery"
:key="i"
:class="{ 'ring-2 ring-primary': i === currentIndex }"
@click="goTo(i)"
>
<NuxtImg :src="img" width="80" height="60" />
</button>
</div>
</template>
</UModal>
</template>
```
### Pattern 2 : UForm + Zod pour le formulaire contact
```vue
<!-- Source : ui.nuxt.com/components/form -->
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
name: z.string().min(2, 'Minimum 2 caractères'),
email: z.string().email('Email invalide'),
message: z.string().min(10, 'Minimum 10 caractères'),
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
name: undefined,
email: undefined,
message: undefined,
})
const toast = useToast()
const loading = ref(false)
async function onSubmit(event: FormSubmitEvent<Schema>) {
loading.value = true
try {
await $fetch('/api/contact', { method: 'POST', body: event.data })
toast.add({ title: 'Message envoyé !', color: 'success', icon: 'i-lucide-check' })
} catch {
toast.add({ title: 'Erreur envoi', color: 'error', icon: 'i-lucide-alert-circle' })
} finally {
loading.value = false
}
}
</script>
<template>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormField label="Nom" name="name">
<UInput v-model="state.name" />
</UFormField>
<UFormField label="Email" name="email">
<UInput v-model="state.email" type="email" />
</UFormField>
<UFormField label="Message" name="message">
<UTextarea v-model="state.message" rows="5" />
</UFormField>
<UButton type="submit" :loading="loading">Envoyer</UButton>
</UForm>
</template>
```
### 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: `<p><strong>De:</strong> ${body.name} &lt;${body.email}&gt;</p><p>${body.message}</p>`,
})
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
<!-- Source : ui.nuxt.com/components/accordion -->
<script setup lang="ts">
import { homeFAQs } from '~/data/faq'
const { t } = useI18n()
const items = computed(() =>
homeFAQs.map((faq) => ({
label: t(faq.questionKey),
content: t(faq.answerKey),
value: faq.questionKey,
}))
)
</script>
<template>
<UAccordion :items="items" type="single" collapsible />
</template>
```
### Pattern 5 : error.vue (PAGE-08 / D-20)
```vue
<!-- Emplacement : app/error.vue Source : nuxt.com/docs/guide/directory-structure/error -->
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps<{ error: NuxtError }>()
function handleError() {
clearError({ redirect: '/' })
}
</script>
<template>
<div class="min-h-screen flex flex-col items-center justify-center gap-6">
<h1 class="text-6xl font-bold">{{ error.status }}</h1>
<p class="text-xl text-gray-500">
{{ error.status === 404 ? 'Page introuvable' : 'Une erreur est survenue' }}
</p>
<UButton @click="handleError">Retour à l'accueil</UButton>
</div>
</template>
```
### 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
<!-- Source : image.nuxt.com/usage/nuxt-img -->
<NuxtImg
:src="project.image"
:alt="project.title"
loading="lazy"
format="webp"
width="800"
height="450"
/>
```
### 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 `<UApp>`
**Ce qui se passe :** `useToast().add()` est appelé mais aucune notification n'apparaît.
**Pourquoi :** Le rendu des toasts requiert `<UApp>` comme wrapper — il est normalement dans `app/app.vue`.
**Comment éviter :** Vérifier que `app/app.vue` contient `<UApp><NuxtLayout>...</NuxtLayout></UApp>`.
**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
<!-- app/pages/project/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { findById } = useProjects()
const project = findById(route.params.id as string)
// 404 si projet non trouvé
if (!project.value) {
throw createError({ status: 404, statusText: 'Project not found' })
}
useSeoMeta({
title: () => project.value?.title ?? '',
description: () => project.value?.description ?? '',
})
</script>
```
### Filtre projets (PAGE-02)
```vue
<script setup lang="ts">
const { projects, filterByCategory, search } = useProjects()
const searchQuery = ref('')
const activeCategory = ref<string | null>(null)
const filtered = computed(() => {
let result = projects.value
if (activeCategory.value) {
result = result.filter((p) => p.category === activeCategory.value)
}
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
result = result.filter(
(p) =>
p.title.toLowerCase().includes(q) ||
p.description.toLowerCase().includes(q) ||
p.technologies.some((t) => t.toLowerCase().includes(q))
)
}
return result
})
const categories = computed(() => [...new Set(projects.value.map((p) => p.category))])
</script>
```
---
## 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 `<UApp>` 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)