docs(03): create phase 3 plans — pages, components, Docker SSR
4 plans across 3 waves: shared components + deps (wave 1), pages landing/projects/detail + about/contact/fiverr/404 (wave 2), Dockerfile SSR + GA4 + docker-compose (wave 3).
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
---
|
||||
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} <${email}></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>
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
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>
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
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>
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
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
|
||||
- app/pages/formation.vue
|
||||
autonomous: true
|
||||
requirements:
|
||||
- INFRA-01
|
||||
- INFRA-04
|
||||
- PAGE-07
|
||||
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"
|
||||
- "Route /formation redirige vers / ou 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 + formation redirect + cleanup</name>
|
||||
<files>nuxt.config.ts, app/pages/formation.vue</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.
|
||||
|
||||
**Page Formation** (per D-19) : La page formation est supprimee. Si `app/pages/formation.vue` existe comme stub, le remplacer par une redirection :
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// D-19: Formation page removed from scope
|
||||
definePageMeta({
|
||||
middleware: () => navigateTo('/', { redirectCode: 301 })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
```
|
||||
Si le fichier n'existe pas, le creer quand meme pour eviter les 404 sur d'anciens liens.
|
||||
|
||||
**Nettoyage :** NE PAS supprimer le dossier `src/` dans cette phase — il sert de reference et sera nettoye dans une future phase de maintenance. Supprimer uniquement `nginx.conf` s'il existe (plus utile avec le Dockerfile SSR).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd C:/Users/minit/Desktop/portfolio/portfolio && grep -q "production" nuxt.config.ts && grep -q "navigateTo\|redirect" app/pages/formation.vue && echo "PASS"</automated>
|
||||
</verify>
|
||||
<done>GA4 nuxt-gtag actif en production via runtimeConfig, formation.vue redirige vers /, nginx.conf supprime si present</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` redirige vers `/` avec status 301
|
||||
</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.vue redirige vers / en 301 (per D-19)
|
||||
- .dockerignore exclut node_modules, .nuxt, .output, src, .git
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-pages-ship/03-04-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user