diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md new file mode 100644 index 0000000..be85e46 --- /dev/null +++ b/.planning/research/ARCHITECTURE.md @@ -0,0 +1,230 @@ +# Architecture Patterns + +**Project:** Portfolio Killian Dalcin — Nuxt 4 SSR Migration +**Researched:** 2026-04-07 +**Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis) + +--- + +## Recommended Architecture + +``` +[ Browser ] + | + | HTTP request (SSR-rendered HTML on first load) + v +[ Nuxt 4 Server (Node 22) ] + | + |-- [ app/ ] + | |-- [ pages/ ] File-based routing + | |-- [ components/ ] Auto-imported UI components + | |-- [ composables/ ] Auto-imported reactive logic + | |-- [ layouts/ ] default.vue (header + footer) + | |-- [ assets/ ] Static assets (images, fonts) + | |-- [ plugins/ ] EmailJS init, gtag init + | + |-- [ server/ ] + | |-- (optional) api/ Not needed — no dynamic data + | + |-- [ data/ ] Static TS files (projects, testimonials, FAQ, techstack) + | + |-- nuxt.config.ts Modules, runtime config, i18n, color-mode +``` + +Nuxt 4 uses `app/` as the root source directory (replaces Nuxt 3's flat root layout). All pages, components, composables, layouts, and plugins live under `app/`. + +--- + +## Component Boundaries + +| Component | Responsibility | Communicates With | +|-----------|---------------|-------------------| +| `layouts/default.vue` | Shell: TheHeader + TheFooter + `` | All pages via slot | +| `TheHeader.vue` | Navigation + locale toggle + color-mode toggle | `useI18n()`, `useColorMode()` | +| `TheFooter.vue` | Links, copyright, social | `useI18n()` | +| `pages/index.vue` | Hero + featured projects + services + CTA | `useProjects()`, `useSeoMeta()` | +| `pages/projects.vue` | Project list + filters | `useProjects()` | +| `pages/project/[id].vue` | Project detail + image gallery modal | `useProjects()`, `UModal` (Nuxt UI) | +| `pages/about.vue` | Bio, tech stack | `useI18n()`, static `techstack.ts` | +| `pages/contact.vue` | UForm + EmailJS send | `useContactForm()`, EmailJS plugin | +| `pages/fiverr.vue` | Fiverr landing, service cards | `useI18n()`, static config | +| `pages/formation.vue` | Training/course landing | `useI18n()` | +| `components/ProjectCard.vue` | Reusable card (list + featured) | Props only, no store | +| `components/GalleryModal.vue` | UModal wrapper for project images | Emits only, props: images[] | +| `composables/useProjects.ts` | Filter/search logic over static data | Imports `data/projects.ts` | +| `composables/useSeoMeta.ts` | Per-route `useSeoMeta()` + JSON-LD | Nuxt built-in `useSeoMeta` | +| `data/*.ts` | Static typed data — single source of truth | Imported by composables only | + +**Rule:** Pages import composables. Composables import data files. Components receive props and emit events. No page imports another page. No component imports data files directly. + +--- + +## Data Flow + +``` +data/projects.ts (static TS, bilingual strings) + | + v +composables/useProjects.ts + - filter by category + - find by id + - expose featuredProjects, allProjects + | + v +pages/index.vue → featuredProjects → +pages/projects.vue → allProjects + filters → +pages/project/[id].vue → findById(route.params.id) → detail view + +``` + +``` +i18n locale (cookie, SSR-safe) + | + v +@nuxtjs/i18n module (strategy: 'prefix_except_default', defaultLocale: 'fr') + - /fr/* and /* (default) both work + - /en/* for English + | + v +All pages and composables via useI18n() (auto-imported by @nuxtjs/i18n) +``` + +``` +Color mode (cookie, SSR-safe) + | + v +@nuxtjs/color-mode (cookie strategy, no FOUC) + | + v +TheHeader.vue toggle → Tailwind dark: classes respond immediately +``` + +``` +Contact form + | + v +pages/contact.vue → UForm validation → composables/useContactForm.ts + | + v +EmailJS plugin (client-side send, no server route needed) +``` + +``` +SEO per route + | + v +Each page calls useSeoMeta() with i18n-translated values + + JSON-LD script tag on pages/index.vue only + | + v +@nuxtjs/sitemap generates sitemap.xml from route list at build time +``` + +--- + +## i18n Architecture Decision + +Use `@nuxtjs/i18n` v9 with `strategy: 'prefix_except_default'`: +- French (`fr`) is default, served at `/`, `/projects`, `/project/[id]`, etc. +- English served at `/en`, `/en/projects`, `/en/project/[id]`, etc. +- Locale detected from browser `Accept-Language` header on first visit (server-side), then persisted in cookie. +- **No redirect strategy** — prefix_except_default avoids redirect chains that hurt Core Web Vitals. +- Translation strings live in `app/i18n/locales/fr.ts` and `app/i18n/locales/en.ts` (migrated from existing `src/locales/`). + +The existing `useI18n.ts` composable wrapping vue-i18n is replaced entirely by the `useI18n()` auto-import provided by `@nuxtjs/i18n`. + +--- + +## Static Data Layer + +Decision: static TS files in `data/` (not `@nuxt/content`, not `server/api`). + +Rationale: +- All project data is known at build time and changes infrequently. +- `@nuxt/content` adds markdown parsing overhead and a file-system watcher not needed for typed data. +- `server/api` routes add network round-trips and cold-start latency for data that never changes. +- Static TS files are tree-shakeable, fully typed, and zero-overhead. + +Migration from `src/data/` is direct: copy files to `data/`, ensure bilingual structure is preserved (FR/EN fields in same object, selected by locale in composables). + +--- + +## Deployment: SSR vs SSG + +**Recommendation: `nuxt build` (SSR) not `nuxt generate` (SSG).** + +Rationale: +- i18n with cookie-based locale detection requires server execution to read the cookie and render the correct language on first request. SSG pre-renders all routes in one language only. +- `useSeoMeta()` with i18n-reactive values requires server-side execution per request. +- The Docker image runs `node server/index.mjs` (the Nuxt nitro server) — not nginx serving static files. +- SSR does not meaningfully increase operational complexity for a portfolio (low traffic, single container). + +Dockerfile pattern: multi-stage — build stage (`node:22-alpine` + `nuxt build`), production stage (copy `.output/` only, `CMD ["node", ".output/server/index.mjs"]`). No nginx layer needed. + +--- + +## Suggested Build Order (Phase Dependencies) + +``` +1. nuxt.config.ts + nuxt.config modules + Depends on: nothing + Blocks: everything — module config must exist before page/component work + +2. data/ migration (static TS files) + Depends on: nothing + Blocks: composables, all pages that display content + +3. composables/ migration + Depends on: data/ + Blocks: pages that use useProjects(), useSeoMeta() + +4. layouts/default.vue + TheHeader + TheFooter + Depends on: @nuxtjs/i18n working, @nuxtjs/color-mode working + Blocks: all page development (every page needs a shell) + +5. pages/ migration (one page at a time, start with index.vue) + Depends on: composables, layouts, Nuxt UI v3 components + Blocks: nothing else — pages are leaf nodes + +6. plugins/ (EmailJS, nuxt-gtag) + Depends on: contact page, nuxt.config + Blocks: contact form functionality, GA tracking + +7. Dockerfile + deployment + Depends on: all pages complete + Blocks: production ship +``` + +**Critical dependency:** `nuxt.config.ts` with `@nuxtjs/i18n`, `@nuxtjs/color-mode`, `@nuxt/ui`, and `@nuxtjs/sitemap` must be functional before any page/component work begins. All auto-imports, CSS variables, and the `useI18n()` composable availability depend on this configuration. + +--- + +## Anti-Patterns to Avoid + +### Anti-Pattern 1: localStorage in SSR Context +**What goes wrong:** `localStorage.setItem('locale', ...)` throws ReferenceError on server, causes hydration mismatch. +**Prevention:** Use only `useCookie()` (Nuxt built-in) for any client-persisted state (locale, color mode). Both `@nuxtjs/i18n` and `@nuxtjs/color-mode` handle this when configured with `cookieName`. + +### Anti-Pattern 2: `document.*` or `window.*` at module scope +**What goes wrong:** Runs during SSR, crashes server render. +**Prevention:** Wrap in `onMounted()` or use `import.meta.client` guard. The existing router `beforeEach` that calls `document.title` must move to `useSeoMeta()` calls inside each page. + +### Anti-Pattern 3: Flat root structure (Nuxt 3 pattern in Nuxt 4) +**What goes wrong:** Nuxt 4 expects `app/` as source directory. Files at project root are not auto-imported. +**Prevention:** All Vue files, composables, components go under `app/`. Configure `srcDir: 'app'` in `nuxt.config.ts` (or rely on Nuxt 4 default). + +### Anti-Pattern 4: useAsyncData for static data +**What goes wrong:** `useAsyncData` with a static import adds unnecessary async overhead and serialization. Static data does not need SSR serialization. +**Prevention:** Import static TS data directly in composables. Reserve `useAsyncData` for genuine async operations (external fetch, server routes). + +### Anti-Pattern 5: Per-page SEO via router.beforeEach +**What goes wrong:** `document.title` manipulation in router guards is SPA-only, invisible to crawlers. +**Prevention:** Each page calls `useSeoMeta({ title, description, ogTitle, ogDescription, ogImage })` at setup scope — Nuxt handles server-side `` injection. + +--- + +## Sources + +- Nuxt 4 source directory convention: official Nuxt 4 migration guide (app/ directory) +- Existing codebase analysis: `src/composables/`, `src/router/index.ts`, `src/locales/` +- PROJECT.md constraints: cookie-only persistence, EmailJS, static TS data, Docker SSR deployment +- Confidence: HIGH for Nuxt 4 file conventions; HIGH for SSR vs SSG decision given i18n cookie requirement; MEDIUM for @nuxtjs/i18n v9 prefix_except_default (verify exact config key names against current docs before implementing) diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md new file mode 100644 index 0000000..cb45d77 --- /dev/null +++ b/.planning/research/FEATURES.md @@ -0,0 +1,155 @@ +# Feature Landscape + +**Domain:** Freelance developer portfolio — Nuxt 4 SSR migration +**Researched:** 2026-04-07 +**Confidence:** MEDIUM — Nuxt UI v3 component coverage from training knowledge (cutoff Aug 2025); Nuxt 4 stable by then. Flag for validation against current ui.nuxt.com docs before implementation. + +--- + +## Table Stakes + +Features users and search engines expect. Missing = product feels incomplete or hurts SEO directly. + +| Feature | Why Expected | Complexity | Nuxt UI v3 Coverage | Notes | +|---------|--------------|------------|---------------------|-------| +| SSR on every route | Google crawls without JS; core migration reason | Low (Nuxt default) | N/A — framework concern | `nuxt build` gives SSR; `nuxt generate` gives SSG. SSR preferred for dynamic og:image | +| Per-route SEO meta | Each page needs unique title, description, og:image | Low | `useSeoMeta()` (Nuxt built-in) | Already implemented in SPA via custom `useSeo()` — replace with `useSeoMeta()` | +| JSON-LD structured data | Enables rich results in Google for Person, CreativeWork, ContactPage | Low | `useHead()` with script injection | Already on Home + Contact + Projects — migrate all pages | +| Sitemap.xml | Required for indexing; Google Search Console standard | Low | `@nuxtjs/sitemap` module | Out-of-the-box with i18n support | +| robots.txt | Crawl control; expected by all search engines | Trivial | `@nuxtjs/sitemap` handles it | | +| Dark/light mode — no FOUC | Flash of unstyled content = unprofessional | Medium | `@nuxtjs/color-mode` with cookie strategy | The SPA currently uses localStorage — causes FOUC on SSR. Cookie strategy required | +| i18n FR/EN | Already a feature; SSR-safe version expected | Medium | `@nuxtjs/i18n` v9 (Nuxt 4 compatible) | Current vue-i18n with localStorage is not SSR-safe; cookie persistence required | +| Language switch persisted across sessions | Users hate re-setting language on return | Low | `@nuxtjs/i18n` `detectBrowserLanguage` with `cookieSecure` | | +| Responsive layout — mobile first | 60%+ of portfolio visitors on mobile | Low | Nuxt UI v3 + Tailwind v4 | All Nuxt UI components are mobile-first | +| Project list with filters | Portfolio core feature; already built | Medium | `UInput` (search), `USelectMenu` or `UTabs` (filter), `UBadge` (category tags) | Current: custom ``. Migrate to Nuxt UI | +| Project detail page with gallery | Proves depth of work | Medium | `UModal` (lightbox), `UCarousel` (thumbnails) | Current GalleryModal.vue (custom) → replace with `UModal` + `UCarousel` | +| Contact methods display | GitHub, LinkedIn, email, phone — visitors need this | Low | `UCard`, `UButton`, `ULink` | Current ContactPage.vue uses custom card design | +| Navigation header with mobile menu | Standard expectation | Low | `UNavigationMenu` or `UHeader` (Nuxt UI Pro) | If not using Pro: compose with `UDrawer` for mobile nav overlay | +| Footer with links | Standard; also helps SEO via internal links | Low | Custom with `ULink` | | +| 404 page | Missing = 404 error shows server default | Trivial | `error.vue` in Nuxt root | | +| Image optimization | Core Web Vitals; LCP often an image | Medium | `@nuxt/image` → `` | Hero image preload + lazy load for project thumbnails | +| Local fonts (no Google Fonts FOUT) | Flash of unstyled text on SSR | Low | `@nuxtjs/google-fonts` with `download: true` or manual `public/fonts/` | Prefer manual: zero dependency | + +--- + +## Differentiators + +Features that elevate the portfolio above average. Not universally expected but add credibility. + +| Feature | Value Proposition | Complexity | Nuxt UI v3 Coverage | Notes | +|---------|-------------------|------------|---------------------|-------| +| Contact form with email delivery | Visitors can send a message directly — reduces friction vs email link only | Medium | `UForm` + `UFormField` + `UInput` + `UTextarea` + `UButton` | Backend-free via EmailJS. `UForm` handles validation schema (Zod/Valibot). Current SPA has NO form — this is new | +| Testimonials section | Social proof — differentiates freelancer from agency | Low | `UCard` for testimonial cards, custom grid | Already has TestimonialsSection.vue — migrate design to Nuxt UI cards | +| Services/pricing page (Fiverr landing) | Conversion-focused; makes offering concrete | Medium | `UCard` (service cards), `UBadge` (tags), `UAccordion` (FAQ) | Already exists as FiverrPage.vue — migrate FAQ to `UAccordion` | +| Tech stack badges | Visual proof of skills without reading text | Low | `UBadge` with `color` and `variant` props | Current TechBadge.vue is custom — replace with `UBadge` | +| Stats display (projects count, featured, etc.) | Builds credibility at a glance | Low | Custom with Tailwind / `UCard` | Already on Projects page and Contact page | +| Formation/training page | Demonstrates continued learning | Low | `UCard`, `UBadge`, `UTimeline` (if available in v3) | Already exists — migrate | +| Keyboard navigation in gallery | Accessibility + power-user UX | Low | `UModal` supports keyboard close (Escape) natively; add arrow key handler | Current GalleryModal.vue already handles keyboard — preserve in migration | +| og:image per project | Rich previews when shared on LinkedIn/Twitter | Low | `useSeoMeta()` with `ogImage` per page | Already implemented in SPA — ensure NuxtImg doesn't break paths | +| Preload hero image | LCP optimization — measurable Google ranking signal | Low | `useHead({ link: [{ rel: 'preload', as: 'image' }] })` | Single line addition | +| Google Analytics 4 via nuxt-gtag | Current hardcoded GA in index.html is fragile | Low | `nuxt-gtag` module | Replace `index.html` script tag with proper module | + +--- + +## Anti-Features + +Things to deliberately NOT build in this migration. + +| Anti-Feature | Why Avoid | What to Do Instead | +|--------------|-----------|-------------------| +| Contact form with custom backend / API route | Adds infra complexity, auth, spam handling — out of scope per PROJECT.md | EmailJS from client — form submits directly, no Nuxt server route needed | +| @nuxt/content for project data | CMS markdown adds indirection when data is already typed TS | Keep `src/data/` as `.ts` files imported by composables | +| Blog / articles section | Not in scope; adds content maintenance burden | If needed later, add as a separate milestone | +| Portfolio password protection | Friction for recruiters / clients browsing | Open portfolio is the point | +| Infinite scroll on projects page | Premature — project count is small; adds complexity | Paginated list or full list is sufficient | +| Animation library (GSAP, Motion One) | Heavy; Tailwind CSS animations + CSS transitions are sufficient | CSS `transition`, `@keyframes` via Tailwind | +| Umami analytics self-hosted | Out of scope per PROJECT.md — requires infra | GA4 via nuxt-gtag | +| Custom color theme picker | Dark/light binary is sufficient; theme builder adds JS weight and UX surface | `@nuxtjs/color-mode` toggle only | +| CMS admin panel | No need for non-dev content editing | Static TS data files, update via code | +| i18n for more than FR/EN | Scope creep; translation maintenance doubles for each language | FR/EN only | + +--- + +## Nuxt UI v3 Component Coverage Map + +Mapped against every portfolio pattern in this project. Confidence: MEDIUM (training data on v3 alpha/beta; verify against ui.nuxt.com before building). + +| Portfolio Pattern | Nuxt UI v3 Component(s) | Replaces (current) | Notes | +|-------------------|------------------------|---------------------|-------| +| Navigation menu desktop | `UNavigationMenu` | Custom `AppHeader.vue` nav links | Composable nav with active state | +| Mobile menu drawer | `UDrawer` or `UModal` | Custom hamburger + overlay | `UDrawer` preferred for slide-in nav | +| Dark/light toggle button | `UButton` with icon slot | `ThemeToggle.vue` (custom) | Toggle reads `useColorMode()` | +| Language switcher dropdown | `UDropdownMenu` | `LanguageSwitcher.vue` (custom) | `UDropdownMenu` items = `[{ label: 'FR' }, { label: 'EN' }]` | +| Project card | `UCard` | `ProjectCard.vue` (custom) | `UCard` header/footer/body slots | +| Project filter search input | `UInput` with icon | Custom `` | Leading icon slot for magnifier | +| Project category filter | `USelectMenu` or `UTabs` | Custom `