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:
@@ -1,230 +0,0 @@
|
||||
# Architecture Patterns
|
||||
|
||||
**Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
|
||||
**Researched:** 2026-04-07
|
||||
**Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
```
|
||||
[ Browser ]
|
||||
|
|
||||
| HTTP request (SSR-rendered HTML on first load)
|
||||
v
|
||||
[ Nuxt 4 Server (Node 22) ]
|
||||
|
|
||||
|-- [ app/ ]
|
||||
| |-- [ pages/ ] File-based routing
|
||||
| |-- [ components/ ] Auto-imported UI components
|
||||
| |-- [ composables/ ] Auto-imported reactive logic
|
||||
| |-- [ layouts/ ] default.vue (header + footer)
|
||||
| |-- [ assets/ ] Static assets (images, fonts)
|
||||
| |-- [ plugins/ ] EmailJS init, gtag init
|
||||
|
|
||||
|-- [ server/ ]
|
||||
| |-- (optional) api/ Not needed — no dynamic data
|
||||
|
|
||||
|-- [ data/ ] Static TS files (projects, testimonials, FAQ, techstack)
|
||||
|
|
||||
|-- nuxt.config.ts Modules, runtime config, i18n, color-mode
|
||||
```
|
||||
|
||||
Nuxt 4 uses `app/` as the root source directory (replaces Nuxt 3's flat root layout). All pages, components, composables, layouts, and plugins live under `app/`.
|
||||
|
||||
---
|
||||
|
||||
## Component Boundaries
|
||||
|
||||
| Component | Responsibility | Communicates With |
|
||||
|-----------|---------------|-------------------|
|
||||
| `layouts/default.vue` | Shell: TheHeader + TheFooter + `<slot />` | 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 → <ProjectCard />
|
||||
pages/projects.vue → allProjects + filters → <ProjectCard />
|
||||
pages/project/[id].vue → findById(route.params.id) → detail view + <GalleryModal />
|
||||
```
|
||||
|
||||
```
|
||||
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 `<head>` 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)
|
||||
@@ -1,155 +0,0 @@
|
||||
# Feature Landscape
|
||||
|
||||
**Domain:** Freelance developer portfolio — Nuxt 4 SSR migration
|
||||
**Researched:** 2026-04-07
|
||||
**Confidence:** MEDIUM — Nuxt UI v3 component coverage from training knowledge (cutoff Aug 2025); Nuxt 4 stable by then. Flag for validation against current ui.nuxt.com docs before implementation.
|
||||
|
||||
---
|
||||
|
||||
## Table Stakes
|
||||
|
||||
Features users and search engines expect. Missing = product feels incomplete or hurts SEO directly.
|
||||
|
||||
| Feature | Why Expected | Complexity | Nuxt UI v3 Coverage | Notes |
|
||||
|---------|--------------|------------|---------------------|-------|
|
||||
| SSR on every route | Google crawls without JS; core migration reason | Low (Nuxt default) | N/A — framework concern | `nuxt build` gives SSR; `nuxt generate` gives SSG. SSR preferred for dynamic og:image |
|
||||
| Per-route SEO meta | Each page needs unique title, description, og:image | Low | `useSeoMeta()` (Nuxt built-in) | Already implemented in SPA via custom `useSeo()` — replace with `useSeoMeta()` |
|
||||
| JSON-LD structured data | Enables rich results in Google for Person, CreativeWork, ContactPage | Low | `useHead()` with script injection | Already on Home + Contact + Projects — migrate all pages |
|
||||
| Sitemap.xml | Required for indexing; Google Search Console standard | Low | `@nuxtjs/sitemap` module | Out-of-the-box with i18n support |
|
||||
| robots.txt | Crawl control; expected by all search engines | Trivial | `@nuxtjs/sitemap` handles it | |
|
||||
| Dark/light mode — no FOUC | Flash of unstyled content = unprofessional | Medium | `@nuxtjs/color-mode` with cookie strategy | The SPA currently uses localStorage — causes FOUC on SSR. Cookie strategy required |
|
||||
| i18n FR/EN | Already a feature; SSR-safe version expected | Medium | `@nuxtjs/i18n` v9 (Nuxt 4 compatible) | Current vue-i18n with localStorage is not SSR-safe; cookie persistence required |
|
||||
| Language switch persisted across sessions | Users hate re-setting language on return | Low | `@nuxtjs/i18n` `detectBrowserLanguage` with `cookieSecure` | |
|
||||
| Responsive layout — mobile first | 60%+ of portfolio visitors on mobile | Low | Nuxt UI v3 + Tailwind v4 | All Nuxt UI components are mobile-first |
|
||||
| Project list with filters | Portfolio core feature; already built | Medium | `UInput` (search), `USelectMenu` or `UTabs` (filter), `UBadge` (category tags) | Current: custom `<select>` + text `<input>`. 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` → `<NuxtImg>` | 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 `<input>` | Leading icon slot for magnifier |
|
||||
| Project category filter | `USelectMenu` or `UTabs` | Custom `<select>` | `UTabs` better UX if <6 categories |
|
||||
| Project gallery modal/lightbox | `UModal` + `UCarousel` | `GalleryModal.vue` (custom) | `UModal` handles focus trap + Escape; `UCarousel` for image navigation with prev/next |
|
||||
| Contact form fields | `UForm` + `UFormField` + `UInput` + `UTextarea` | No form currently | `UForm` integrates with Zod/Valibot for schema validation |
|
||||
| Contact form submit button with loading | `UButton` with `loading` prop | N/A | `UButton loading` shows spinner during EmailJS send |
|
||||
| Social link items | `ULink` or `UButton` variant=link | Custom `<a>` tags | |
|
||||
| Service/Fiverr service cards | `UCard` | `FiverrServiceCard.vue` (custom) | |
|
||||
| FAQ accordion | `UAccordion` | `ServiceFAQ.vue` (custom) | Built-in open/close, accessible |
|
||||
| Testimonial cards | `UCard` | `TestimonialCard.vue` (custom) | |
|
||||
| Tech skill badges | `UBadge` | `TechBadge.vue` (custom) | `color` and `variant` props cover current custom styles |
|
||||
| Section CTA buttons | `UButton` | `CTAButtons.vue` (custom) | `UButton` size/variant props handle all current btn variants |
|
||||
| 404 error page | Custom `error.vue` with `UButton` | N/A (SPA handled by router) | Nuxt `error.vue` gets `error` prop |
|
||||
| Toast / form feedback | `useToast()` + `UToastProvider` | None currently | Show success/error after EmailJS send |
|
||||
| Page loading indicator | `NuxtLoadingIndicator` (Nuxt built-in) | None in SPA | One-liner in `app.vue` |
|
||||
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
SSR (nuxt build)
|
||||
→ i18n cookie persistence (@nuxtjs/i18n v9)
|
||||
→ Language switcher UI (UDropdownMenu)
|
||||
→ Dark mode cookie (@nuxtjs/color-mode)
|
||||
→ Theme toggle UI (UButton + useColorMode)
|
||||
→ Per-route SEO (useSeoMeta)
|
||||
→ Sitemap (@nuxtjs/sitemap)
|
||||
→ og:image per project
|
||||
|
||||
Contact form (UForm + Zod)
|
||||
→ EmailJS client send
|
||||
→ UButton loading state
|
||||
→ useToast() success/error feedback
|
||||
|
||||
Project gallery (UModal + UCarousel)
|
||||
→ Project detail page
|
||||
→ Project data (TS static files)
|
||||
→ useProjects() composable (useAsyncData wrapper)
|
||||
|
||||
Image optimization (NuxtImg)
|
||||
→ @nuxt/image module
|
||||
→ Hero preload (useHead link preload)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP Recommendation
|
||||
|
||||
Phases should prioritize in this order to unblock everything else:
|
||||
|
||||
1. **SSR foundation** — Nuxt 4 project scaffold, routing, layouts, @nuxtjs/color-mode, @nuxtjs/i18n. Without this nothing else works correctly.
|
||||
2. **Static data migration** — Port `src/data/` TS files + composables to Nuxt conventions. Unblocks all page content.
|
||||
3. **Page migrations** (Home, Projects, Project Detail, About, Contact, Fiverr, Formation) — Migrate one page at a time with Nuxt UI v3 components replacing custom ones.
|
||||
4. **Contact form** — New feature, not a migration. Add EmailJS + UForm + useToast after pages are stable.
|
||||
5. **SEO + sitemap** — Add after pages exist; useSeoMeta() per page, sitemap module, JSON-LD.
|
||||
6. **Performance polish** — NuxtImg, font preloads, GA4 via nuxt-gtag, Docker production build.
|
||||
|
||||
Defer:
|
||||
- Formation page: low traffic value; migrate last
|
||||
- Fiverr page: secondary conversion path; migrate after core pages
|
||||
- Testimonials stats: nice-to-have; fold into About or Home as a section
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Training knowledge: Nuxt UI v3 component API (alpha/beta period, up to Aug 2025) — MEDIUM confidence
|
||||
- Training knowledge: Nuxt 4 release features, `@nuxtjs/i18n` v9, `@nuxtjs/color-mode`, `@nuxt/image` — MEDIUM confidence
|
||||
- Direct codebase analysis: `src/views/`, `src/components/` in this repository — HIGH confidence
|
||||
- PROJECT.md constraints and out-of-scope declarations — HIGH confidence
|
||||
|
||||
**Validate before building:** Confirm `UCarousel`, `UDrawer`, `UNavigationMenu`, `UAccordion`, `UForm`/`UFormField` names against current ui.nuxt.com/components — component names may have changed between v3 beta and v3 stable.
|
||||
@@ -1,285 +0,0 @@
|
||||
# Domain Pitfalls — Vue 3 SPA → Nuxt 4 SSR Migration
|
||||
|
||||
**Domain:** Portfolio SSR migration (Nuxt 4 + Nuxt UI v3 + @nuxtjs/i18n + @nuxtjs/color-mode)
|
||||
**Researched:** 2026-04-07
|
||||
**Confidence:** MEDIUM (training data + ecosystem knowledge as of Aug 2025; web access unavailable for live verification)
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
Mistakes that cause rewrites or block SSR from working correctly.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 1: Hydration Mismatch from localStorage-based State
|
||||
|
||||
**What goes wrong:** The existing SPA uses `localStorage` for both locale and theme persistence. During SSR, the server renders with the default locale/theme (server has no access to localStorage). The client then reads localStorage and switches — causing a visible flash and a Vue hydration warning (`[Vue warn]: Hydration mismatch`). In strict hydration mode (Nuxt 4 default), this can throw an error, not just a warning.
|
||||
|
||||
**Why it happens:** `localStorage` is a browser-only API. The server renders `lang="fr"` but the client's stored preference is `lang="en"`. The DOM differs between server render and client mount.
|
||||
|
||||
**Consequences:**
|
||||
- FOUC (Flash of Unstyled Content) for dark mode
|
||||
- Wrong locale briefly visible on first paint
|
||||
- Hydration errors that can fully break page interactivity in strict mode
|
||||
- SEO crawlers see the default locale/theme, not the user's preference (but this is acceptable for SEO)
|
||||
|
||||
**Prevention:**
|
||||
- Replace ALL `localStorage` reads with cookie reads — cookies are sent with every HTTP request, so the server can read them via `useCookie()` during SSR
|
||||
- For locale: configure `@nuxtjs/i18n` with `detectBrowserLanguage: { useCookie: true, cookieKey: 'i18n_locale' }`
|
||||
- For theme: configure `@nuxtjs/color-mode` with `storageKey: 'color-mode'` and ensure it uses its built-in cookie strategy (it does by default in SSR mode)
|
||||
- Never call `localStorage` directly in composables that run during SSR — wrap in `if (import.meta.client)` or `onMounted()`
|
||||
|
||||
**Detection:** Run `nuxt build && nuxt preview`, open DevTools Console — any `[Vue warn]: Hydration` message is a failure.
|
||||
|
||||
**Phase:** Foundation setup (Phase 1 — before any page migration)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: @nuxtjs/i18n v9 Breaking Config Changes
|
||||
|
||||
**What goes wrong:** `@nuxtjs/i18n` v9 (required for Nuxt 4) has significant breaking changes from v8. The `vueI18n` config file path changed, `strategy: 'no_prefix'` behavior changed, and the `detectBrowserLanguage` defaults changed. Copying the old config causes silent failures where locale switching appears to work client-side but SSR always renders the default locale.
|
||||
|
||||
**Why it happens:** i18n v9 moved to a Nuxt-native config approach; the old `vueI18n.config.ts` file requires an explicit `vueI18n` option pointing to it. Without this, messages load client-side only (from the bundle) but are missing during SSR rendering.
|
||||
|
||||
**Consequences:**
|
||||
- Server renders untranslated keys (e.g. `"page.hero.title"`) instead of translated text
|
||||
- SEO crawlers index translation keys, not content
|
||||
- Locale cookies set but not respected on server
|
||||
|
||||
**Prevention:**
|
||||
- Set `vueI18n: './i18n.config.ts'` explicitly in `nuxt.config.ts`
|
||||
- Use `lazy: true` with `langDir` for large translation files, but test SSR with `nuxt preview` (not `nuxt dev`) since lazy loading behaves differently
|
||||
- Set `strategy: 'no_prefix'` only if both FR and EN share the same URL structure — verify the SEO implication (Google prefers `hreflang` differentiation)
|
||||
- Test locale detection server-side: curl the deployed URL with `Cookie: i18n_locale=en` and verify English content is in the HTML response
|
||||
|
||||
**Detection:** `curl -H "Cookie: i18n_locale=en" http://localhost:3000/ | grep -i "hero"` — if French text appears, SSR locale is broken.
|
||||
|
||||
**Phase:** Foundation setup (Phase 1)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: @nuxtjs/color-mode FOUC Despite Cookie Strategy
|
||||
|
||||
**What goes wrong:** Even with `@nuxtjs/color-mode` configured for cookie storage, a FOUC (Flash of Unstyled Content / Flash of Wrong Theme) can still occur. This happens when Tailwind CSS v4 dark mode is configured as `class`-based but the `<html>` class is added after hydration rather than during SSR.
|
||||
|
||||
**Why it happens:** `@nuxtjs/color-mode` adds the color mode class to `<html>` via a server-side plugin. If the plugin runs after the initial HTML render, or if Tailwind's dark variant is not using `darkMode: 'class'` correctly, the flash occurs. A common mistake is having `darkMode: 'media'` in Tailwind config while `@nuxtjs/color-mode` controls a class — these conflict.
|
||||
|
||||
**Consequences:**
|
||||
- White flash when loading dark mode (or vice versa)
|
||||
- User sees theme switch on every page load
|
||||
- Poor perceived performance
|
||||
|
||||
**Prevention:**
|
||||
- In `tailwind.config.ts` (or `@import "tailwindcss"` in CSS for v4), ensure dark mode variant matches `@nuxtjs/color-mode`'s `classSuffix: ''` and `classPrefix: ''` settings
|
||||
- In Nuxt UI v3 + Tailwind v4, dark mode is configured via CSS `@variant dark (.dark &)` — verify this aligns with the class `color-mode` adds (`dark` not `dark-mode`)
|
||||
- Set `colorMode.preference: 'system'` as fallback but ensure the cookie override takes precedence
|
||||
- Test with Network throttling (Slow 3G) in DevTools — FOUC is most visible on slow connections
|
||||
|
||||
**Detection:** Record a screen capture of first page load with DevTools CPU 6x throttle. Any flash = FOUC present.
|
||||
|
||||
**Phase:** Foundation setup (Phase 1) — must be validated before any page migration
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Nuxt 4 `app/` Directory Structure — Component/Composable Resolution
|
||||
|
||||
**What goes wrong:** Nuxt 4 changes the default directory layout. The `srcDir` default is now `app/` instead of the root. Components placed in `components/`, composables in `composables/`, and pages in `pages/` at the root level are NOT auto-imported in Nuxt 4 if you've opted into the new directory structure.
|
||||
|
||||
**Why it happens:** Nuxt 4 introduces `app/` as the application root (analogous to how Next.js uses `app/`). Migrating without updating import paths or without setting `future.compatibilityVersion: 4` in nuxt.config causes either broken auto-imports or requires manual path updates.
|
||||
|
||||
**Consequences:**
|
||||
- Components silently fail to auto-import → runtime errors
|
||||
- Composables work in dev (because Nuxt falls back) but fail in production build
|
||||
- Hours debugging "component not found" errors
|
||||
|
||||
**Prevention:**
|
||||
- Decide upfront: use new `app/` structure (recommended) or stay at root with explicit `srcDir: '.'`
|
||||
- If using `app/`: move `components/`, `composables/`, `pages/`, `layouts/`, `middleware/`, `plugins/` into `app/`
|
||||
- `server/` stays at root (it's a Nitro convention, not a Nuxt app convention)
|
||||
- `public/` stays at root
|
||||
- `nuxt.config.ts`, `package.json` stay at root
|
||||
- Validate with `nuxt info` — it shows resolved directories
|
||||
|
||||
**Detection:** Run `nuxt build` and check for "Auto-imported composable used but not resolved" warnings.
|
||||
|
||||
**Phase:** Foundation setup (Phase 1 — structural decision before any files are created)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: Nuxt UI v3 + Tailwind CSS v4 Configuration Conflicts
|
||||
|
||||
**What goes wrong:** Nuxt UI v3 ships its own Tailwind CSS v4 preset and expects to control the Tailwind configuration via its module. Manually adding a `tailwind.config.ts` alongside Nuxt UI v3 can cause duplicate utility class generation, broken component styles, or the Nuxt UI design tokens being overridden.
|
||||
|
||||
**Why it happens:** Tailwind CSS v4 moved from `tailwind.config.js` to CSS-based configuration (`@import "tailwindcss"` + `@theme`). Nuxt UI v3 injects its theme tokens via this CSS mechanism. If the developer also adds a separate Tailwind config file, the two configurations merge unpredictably.
|
||||
|
||||
**Consequences:**
|
||||
- Nuxt UI components render without their default styles
|
||||
- Custom colors/spacing defined in config override Nuxt UI tokens instead of extending them
|
||||
- `@apply` directives fail silently in production (different behavior than dev)
|
||||
|
||||
**Prevention:**
|
||||
- Do NOT create a standalone `tailwind.config.ts` with Nuxt UI v3 — extend via `app.config.ts` using Nuxt UI's theme API instead
|
||||
- For custom colors: use `ui.colors` in `app.config.ts`, not raw Tailwind config
|
||||
- For custom CSS utilities: add them to `assets/css/main.css` using Tailwind v4's `@layer utilities` syntax
|
||||
- Read Nuxt UI v3 theming docs before writing any custom styles
|
||||
|
||||
**Detection:** After setup, run `nuxt dev` and inspect a `UButton` — if it renders unstyled, Tailwind integration is broken.
|
||||
|
||||
**Phase:** Foundation setup (Phase 1)
|
||||
|
||||
---
|
||||
|
||||
## Moderate Pitfalls
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 6: `useAsyncData` Key Collisions Across Pages
|
||||
|
||||
**What goes wrong:** Multiple pages call `useAsyncData('projects', ...)` with the same key. In SSR, Nuxt caches `useAsyncData` results by key in the payload. If two different pages use the same key for different data shapes, one page gets the other's cached data.
|
||||
|
||||
**Why it happens:** Copy-paste of composable calls without updating the key. This is a regression from SPA behavior — in a SPA, `useAsyncData` re-executes on every navigation; in SSR, the payload cache can serve stale data.
|
||||
|
||||
**Prevention:**
|
||||
- Use route-scoped keys: `useAsyncData(\`project-\${slug}\`, ...)` for detail pages
|
||||
- For shared data (projects list): use a single composable (`useProjects`) that internally calls `useAsyncData('projects-list', ...)` — single definition, consistent key
|
||||
- Audit all `useAsyncData` calls and ensure every key is unique across the app
|
||||
|
||||
**Detection:** Navigate between two pages that use the same key and check if data bleeds across.
|
||||
|
||||
**Phase:** Data migration (composables phase)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 7: EmailJS Client-Only Execution in SSR Context
|
||||
|
||||
**What goes wrong:** EmailJS is a browser SDK (`emailjs-com` or `@emailjs/browser`). If the contact form composable calls `emailjs.sendForm()` in a context that can run server-side, the build will fail or throw a `window is not defined` error.
|
||||
|
||||
**Why it happens:** SSR executes component setup code on the server. Any direct `import emailjs from '@emailjs/browser'` at module level that accesses browser globals during import causes the server render to crash.
|
||||
|
||||
**Prevention:**
|
||||
- Use `import.meta.client` guard: only call emailjs inside `onMounted` or an event handler (not in `setup()` body)
|
||||
- Alternatively, use `nuxt-only` dynamic import: `const emailjs = await import('@emailjs/browser')` inside the submit handler
|
||||
- Never call `emailjs.init()` at the top level of a composable — defer to client-side execution
|
||||
|
||||
**Detection:** Run `nuxt build && nuxt preview`, submit the contact form — if it throws, check server logs for `window is not defined`.
|
||||
|
||||
**Phase:** Contact page migration
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 8: `useSeoMeta()` Duplicate Meta Tags
|
||||
|
||||
**What goes wrong:** The `useSeoMeta()` composable in Nuxt merges meta tags reactively. If a base `useSeoMeta()` call is in `app.vue` AND a route-level call is in a page component, the page-level tags should override the base — but some tags (especially `og:image`) may render twice if not using the `key` deduplication mechanism.
|
||||
|
||||
**Why it happens:** Nuxt uses Unhead under the hood. Without explicit `key` on duplicate meta entries, Unhead appends rather than replaces.
|
||||
|
||||
**Prevention:**
|
||||
- Define global defaults in `app.vue` using `useHead()` or `useSeoMeta()` with low-priority defaults
|
||||
- Override at page level — Nuxt/Unhead will deduplicate by meta `name`/`property` automatically for standard tags
|
||||
- For `og:image`: provide absolute URLs (not relative paths) — relative paths resolve to `null` in SSR and crawlers see empty og:image
|
||||
- Test with `curl http://localhost:3000/ | grep -i "og:image"` — count occurrences
|
||||
|
||||
**Detection:** View source of any page and check for duplicate `<meta property="og:image">` tags.
|
||||
|
||||
**Phase:** SEO implementation phase
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 9: NuxtImg with External/Dynamic Image Sources
|
||||
|
||||
**What goes wrong:** `<NuxtImg>` with `provider: 'ipx'` (default) works fine for local images in `public/`. For images with dynamic URLs (e.g. project thumbnails loaded from a data array with paths like `/images/projects/foo.jpg`), the image optimization pipeline may fail silently in Docker if the IPX cache directory is not writable.
|
||||
|
||||
**Why it happens:** IPX (the image optimization engine) writes optimized images to `.nuxt/image-cache/` or a configurable dir. In Docker containers running as non-root, this directory may not be writable. The request falls through to the original image without optimization — no error, just no optimization.
|
||||
|
||||
**Prevention:**
|
||||
- In Dockerfile: ensure the working directory and `.nuxt/` subdirectories are owned by the runtime user
|
||||
- Or: use `sharp` provider with pre-optimized images at build time (simpler for static images)
|
||||
- For a portfolio with static project images: consider running `nuxt generate` (SSG) instead of SSR — eliminates the runtime IPX issue entirely
|
||||
- Test Docker image locally: `docker run --rm -p 3000:3000 portfolio-image` and verify images load optimized (check response headers for `Content-Type: image/webp`)
|
||||
|
||||
**Detection:** After Docker deploy, check Network tab — if project images are served as `image/jpeg` instead of `image/webp`, IPX optimization is failing.
|
||||
|
||||
**Phase:** Docker/deployment phase
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 10: SSG vs SSR Decision — Critical for Docker Strategy
|
||||
|
||||
**What goes wrong:** The project currently deploys as static files served by nginx. A direct "lift and shift" to Nuxt 4 with `nuxt build` (SSR) requires a Node.js runtime in Docker — fundamentally different from the current nginx static serving. Teams often start with SSR, hit Docker complexity, then realize their portfolio (fully static content, no user-specific server responses) could just use `nuxt generate` (SSG).
|
||||
|
||||
**Why it happens:** "SSR" is the stated goal, but for a portfolio with static content, SSG provides identical SEO benefits with zero runtime complexity. The decision is deferred until deployment, causing wasted work.
|
||||
|
||||
**Consequences:**
|
||||
- Docker image is 10x larger (Node.js runtime vs static files)
|
||||
- Cold starts on container restarts
|
||||
- Memory management overhead
|
||||
- Unnecessary complexity for static content
|
||||
|
||||
**Prevention:**
|
||||
- Decide SSR vs SSG in Phase 1, not Phase N
|
||||
- SSG (`nuxt generate`) is appropriate if: no user-specific server responses, no server-side auth, no real-time data
|
||||
- For this portfolio: SSG is almost certainly sufficient — locale/theme from cookies still works client-side after hydration, and pre-rendered HTML satisfies SEO
|
||||
- If SSG: keep nginx in Docker, only Nuxt is involved at build time, not runtime
|
||||
|
||||
**Detection:** List every route — does any route return different HTML based on the authenticated user or real-time data? If no → SSG is viable.
|
||||
|
||||
**Phase:** Phase 1 decision, before any implementation
|
||||
|
||||
---
|
||||
|
||||
## Minor Pitfalls
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 11: `definePageMeta()` Not Respected in Certain Nuxt 4 Contexts
|
||||
|
||||
**What goes wrong:** `definePageMeta({ layout: 'default' })` is a compile-time macro in Nuxt 4. Using it inside a `<script setup lang="ts">` that also contains conditional logic or computed values causes the macro extraction to fail silently — the layout falls back to default without error.
|
||||
|
||||
**Prevention:** Keep `definePageMeta()` at the top of `<script setup>`, with only static values. Never wrap it in conditionals.
|
||||
|
||||
**Phase:** Page migration
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 12: Google Analytics / nuxt-gtag Firing Twice in Dev
|
||||
|
||||
**What goes wrong:** `nuxt-gtag` (or `nuxt-google-analytics`) sends a pageview event on every route change. In development with HMR, events can fire multiple times. This pollutes GA4 debug data.
|
||||
|
||||
**Prevention:** Configure `gtag: { enabled: process.env.NODE_ENV === 'production' }` so GA only fires in production builds. Current hardcoded GA in `index.html` will be dead code after migration — remove it.
|
||||
|
||||
**Phase:** Analytics migration (can be last)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 13: TypeScript Strict Mode Breaking Existing Data Files
|
||||
|
||||
**What goes wrong:** The existing `src/data/` files (projects, testimonials, FAQ) were likely written with implicit `any` types or loose typing. Enabling `strict: true` in `tsconfig.json` (Nuxt 4 default) will cause build errors for these files.
|
||||
|
||||
**Prevention:** Migrate data files first and define explicit interfaces (`Project`, `Testimonial`, etc.) before TypeScript strict errors compound. Use `satisfies` operator for type-safe data definitions without losing literal types.
|
||||
|
||||
**Phase:** Data migration phase (early)
|
||||
|
||||
---
|
||||
|
||||
## Phase-Specific Warnings
|
||||
|
||||
| Phase Topic | Likely Pitfall | Mitigation |
|
||||
|-------------|----------------|------------|
|
||||
| Foundation / nuxt.config setup | Missing `future: { compatibilityVersion: 4 }` → wrong directory structure | Set this in nuxt.config on day 1 |
|
||||
| i18n integration | SSR locale mismatch (server renders default, client switches) | Cookie strategy, test with curl |
|
||||
| Color mode integration | FOUC despite cookie config — Tailwind v4 dark variant mismatch | Align `classSuffix` with Tailwind v4 `@variant dark` |
|
||||
| Nuxt UI v3 setup | Tailwind config conflicts, broken component styles | No standalone tailwind.config.ts |
|
||||
| Data composables migration | `useAsyncData` key collisions | Route-scoped keys |
|
||||
| Contact page | EmailJS `window is not defined` in SSR | Client-only execution guards |
|
||||
| SEO meta | Duplicate `og:image` tags, relative URL og:image | Absolute URLs, dedup check with curl |
|
||||
| Docker deploy | IPX cache not writable, Node memory | Consider SSG first; if SSR, set writable cache dir |
|
||||
| Analytics | GA firing in dev or firing twice | `enabled: production` flag |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Confidence: MEDIUM — based on Nuxt 3/4 ecosystem knowledge as of August 2025. Web search was unavailable during this research session.
|
||||
- Key source areas (unverifiable without web access): Nuxt 4 migration guide (nuxt.com/docs/getting-started/upgrade), @nuxtjs/i18n v9 changelog, @nuxtjs/color-mode SSR docs, Nuxt UI v3 theming docs, Unhead deduplication behavior.
|
||||
- **Flag:** Pitfalls 2 (@nuxtjs/i18n v9 config), 5 (Nuxt UI v3 + Tailwind v4), and 4 (app/ directory structure) are most likely to have changed since August 2025. These should be verified against current docs before implementation.
|
||||
@@ -1,240 +0,0 @@
|
||||
# Technology Stack
|
||||
|
||||
**Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
|
||||
**Researched:** 2026-04-07
|
||||
**Knowledge cutoff:** August 2025 — all versions marked LOW confidence must be verified against npm before pinning
|
||||
|
||||
---
|
||||
|
||||
## IMPORTANT: Version Verification Required
|
||||
|
||||
All network tools were unavailable during this research session. Versions below are from training data (cutoff August 2025). Before starting the project, run:
|
||||
|
||||
```bash
|
||||
npm info nuxt version
|
||||
npm info @nuxt/ui version
|
||||
npm info @nuxtjs/i18n version
|
||||
npm info @nuxtjs/color-mode version
|
||||
npm info @nuxtjs/sitemap version
|
||||
npm info @nuxtjs/seo version
|
||||
npm info nuxt-gtag version
|
||||
npm info @pinia/nuxt version
|
||||
npm info @nuxt/image version
|
||||
npm info @nuxt/eslint version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
|
||||
### Core Framework
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| nuxt | ^4.0.0 | LOW — verify on npm | SSR framework | Only reason this migration exists: per-route SSR so every page is crawlable without client JS. Nuxt 4 is the current stable major. |
|
||||
| vue | ^3.5.x | MEDIUM | UI layer | Peer dependency of Nuxt 4; Vue 3.5 introduces `useTemplateRef` and improved reactivity — no action needed, Nuxt manages it |
|
||||
| typescript | ^5.x | MEDIUM | Type safety | Nuxt 4 ships its own TS config; strict mode enforced via `tsconfig.json` extends |
|
||||
| node | 22.x LTS | HIGH | Runtime | Matches Docker base image `node:22-alpine`; Node 22 is current LTS as of April 2026 |
|
||||
|
||||
### UI & Styling
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @nuxt/ui | ^3.0.0 | LOW — verify on npm | Component library | v3 is built on Tailwind v4 and Radix Vue, ships production-ready components (UModal, UForm, UInput, UTextarea). Replaces ~80% of custom component work. v2 is NOT compatible with Tailwind v4. |
|
||||
| tailwindcss | ^4.0.0 | LOW — verify on npm | Utility CSS | Bundled as a dependency of @nuxt/ui v3; do NOT install separately or pin a conflicting version. Tailwind v4 ships as a Vite/PostCSS plugin, no `tailwind.config.js` needed. |
|
||||
| @nuxtjs/color-mode | ^3.5.x | LOW — verify on npm | Dark/light mode | Nuxt-native module; writes a cookie on the server, so no FOUC and no hydration mismatch. `localStorage` alternative is explicitly broken for SSR. Must set `storage: 'cookie'` in config. |
|
||||
|
||||
### Internationalisation
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @nuxtjs/i18n | ^9.x | LOW — verify on npm | FR/EN i18n | v9 is the Nuxt 4-compatible major. v8 targets Nuxt 3. Uses `useCookie()` for locale persistence (SSR-safe). Must set `detectBrowserLanguage.cookieKey` and `cookieCrossOrigin` appropriately; `localStorage` fallback must be disabled. |
|
||||
| vue-i18n | ^10.x | LOW — peer dep | Translation runtime | Peer dep of @nuxtjs/i18n v9; do not install vue-i18n v9 (Nuxt 3 era). |
|
||||
|
||||
### SEO
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @nuxtjs/sitemap | ^6.x | LOW — verify on npm | sitemap.xml | Auto-generates sitemap from Nuxt routes including i18n alternates. Required by the PROJECT.md spec. Must be configured with `i18n` option when @nuxtjs/i18n is present to emit `hreflang` entries. |
|
||||
| @nuxtjs/seo | ^2.x | LOW — verify on npm | SEO meta bundle | Meta-module that installs and pre-configures `@nuxtjs/sitemap`, `nuxt-og-image`, `nuxt-schema-org`, `nuxt-link-checker`. Using it avoids duplicate sitemap config. If using @nuxtjs/seo, do NOT also install @nuxtjs/sitemap standalone (conflict risk). Choose one. |
|
||||
|
||||
> **Decision needed:** Use `@nuxtjs/seo` (meta-module, installs sitemap + og-image + schema-org) OR install `@nuxtjs/sitemap` standalone and `useSeoMeta()` manually. Recommendation: use `@nuxtjs/seo` because the portfolio needs og:image and JSON-LD (project requirement), and the meta-module wires them together with zero boilerplate.
|
||||
|
||||
### Analytics
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| nuxt-gtag | ^3.x | LOW — verify on npm | Google Analytics 4 | Replaces GA4 hardcoded in `index.html`. Injects `gtag.js` via Nuxt's head management, respects SSR. Must be configured with `id: 'G-XXXXXXXX'` from `runtimeConfig.public`. |
|
||||
|
||||
### State Management
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @pinia/nuxt | ^0.9.x | LOW — verify on npm | Global state | Required if any state needs to survive navigation (e.g., project filter state). For a portfolio with mostly static data this may be optional; include it anyway because Pinia integrates with Nuxt devtools and SSR hydration is handled automatically. |
|
||||
| pinia | ^3.x | LOW — peer dep | Pinia core | Peer dep of @pinia/nuxt; version must match. |
|
||||
|
||||
### Images
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @nuxt/image | ^1.x | LOW — verify on npm | Optimised images | `<NuxtImg>` replaces `<img>` for automatic lazy loading, srcset, and format conversion. Project requirement: lazy load project gallery images. Use `provider: 'ipx'` (built-in, no external service). |
|
||||
|
||||
### Developer Tooling
|
||||
|
||||
| Technology | Version (training data) | Confidence | Purpose | Why |
|
||||
|------------|------------------------|------------|---------|-----|
|
||||
| @nuxt/eslint | ^0.7.x | LOW — verify on npm | ESLint + Prettier | Nuxt-native flat config ESLint. Replaces manual eslint + prettier wiring. Enforces Vue 3 best practices. One module, one config file. |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| Technology | Version | Confidence | Purpose | Why |
|
||||
|------------|---------|------------|---------|-----|
|
||||
| Docker node:22-alpine | 22-alpine | HIGH | Container base | Alpine keeps image small (~50MB base). Node 22 matches the runtime. Multi-stage build: stage 1 installs deps + builds, stage 2 copies `.output/` only. |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Category | Recommended | Alternative | Why Not |
|
||||
|----------|-------------|-------------|---------|
|
||||
| UI library | @nuxt/ui v3 | Vuetify, PrimeVue, custom | Nuxt UI v3 is Tailwind-native, ships ready for Nuxt 4, has UModal/UForm exactly as specced. Others require extra adapter work. |
|
||||
| CSS | Tailwind v4 (via @nuxt/ui) | Tailwind v3, UnoCSS, plain CSS | v4 is the current generation; UnoCSS is a valid alternative but adds config overhead with no benefit for this scope. |
|
||||
| i18n | @nuxtjs/i18n v9 | lingui, custom composable | @nuxtjs/i18n has Nuxt 4 SSR cookie support built-in; alternatives require manual SSR wiring. |
|
||||
| Analytics | nuxt-gtag | Umami (self-hosted) | Umami is out of scope per PROJECT.md. nuxt-gtag is the standard Nuxt-native GA4 module. |
|
||||
| State | @pinia/nuxt | useState() only | useState() is fine for simple per-component state but Pinia is needed for shared filter state across pages. Include it from day one to avoid a refactor. |
|
||||
| CMS | Static TS data files | @nuxt/content | PROJECT.md explicitly rules out @nuxt/content. Data is bilingual TS objects already; keep them. |
|
||||
| Contact form backend | EmailJS | Custom API, Formspree | No backend to maintain. EmailJS free tier is sufficient for a portfolio contact form. Not a Nuxt module — just an npm package (`emailjs-com`). |
|
||||
| Sitemap + SEO meta | @nuxtjs/seo (bundle) | @nuxtjs/sitemap standalone | @nuxtjs/seo includes og-image and schema-org which the project spec requires. One module is simpler. |
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Use
|
||||
|
||||
| Package | Reason |
|
||||
|---------|--------|
|
||||
| vue-router (manual) | Nuxt 4 ships file-based routing on top of vue-router; never import vue-router directly in a Nuxt project |
|
||||
| @nuxt/content | Explicitly out of scope; TS data files are simpler and already exist |
|
||||
| localStorage for i18n/theme | Not readable on server; causes hydration mismatch and FOUC. Use cookies only. |
|
||||
| Tailwind v3 | @nuxt/ui v3 requires Tailwind v4. Mixing versions breaks everything. |
|
||||
| @nuxtjs/i18n v8 | Only compatible with Nuxt 3. v9 is required for Nuxt 4. |
|
||||
| nuxt generate (full SSG) | May be considered for perf, but SSR is the core value of this migration (per PROJECT.md). Use `nuxt build` + node server in Docker. Revisit after launch if edge deployment is added. |
|
||||
|
||||
---
|
||||
|
||||
## nuxt.config.ts Skeleton
|
||||
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-01-01',
|
||||
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxtjs/color-mode',
|
||||
'@nuxtjs/seo', // includes sitemap, og-image, schema-org
|
||||
'nuxt-gtag',
|
||||
'@pinia/nuxt',
|
||||
'@nuxt/image',
|
||||
'@nuxt/eslint',
|
||||
],
|
||||
|
||||
colorMode: {
|
||||
preference: 'system',
|
||||
fallback: 'light',
|
||||
storage: 'cookie', // SSR-safe: no FOUC
|
||||
},
|
||||
|
||||
i18n: {
|
||||
locales: ['fr', 'en'],
|
||||
defaultLocale: 'fr',
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: 'i18n_redirected',
|
||||
redirectOn: 'root',
|
||||
},
|
||||
},
|
||||
|
||||
gtag: {
|
||||
id: '', // set via runtimeConfig.public.gtag.id
|
||||
},
|
||||
|
||||
image: {
|
||||
provider: 'ipx',
|
||||
},
|
||||
|
||||
typescript: {
|
||||
strict: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Production Setup
|
||||
|
||||
```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
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.output ./output
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["node", "output/server/index.mjs"]
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Only `.output/` is copied to the final image. No `node_modules/`, no source files.
|
||||
- `node:22-alpine` is the project constraint (matches dev runtime).
|
||||
- Nuxt 4 SSR server entry is `.output/server/index.mjs`.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Scaffold Nuxt 4 project
|
||||
npx nuxi@latest init portfolio --template=v4-compat
|
||||
cd portfolio
|
||||
|
||||
# Core modules
|
||||
npm install @nuxt/ui @nuxtjs/i18n @nuxtjs/color-mode @nuxtjs/seo nuxt-gtag @pinia/nuxt pinia @nuxt/image
|
||||
|
||||
# Dev tooling
|
||||
npm install -D @nuxt/eslint typescript
|
||||
|
||||
# Contact form (not a Nuxt module)
|
||||
npm install @emailjs/browser
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Confidence Summary
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Nuxt 4 as framework | MEDIUM | Nuxt 4 was in RC/stable as of mid-2025; verify exact version on npm |
|
||||
| @nuxt/ui v3 | LOW | v3 was in active development; confirm stable tag on npm |
|
||||
| @nuxtjs/i18n v9 (Nuxt 4 compat) | LOW | v9 announced for Nuxt 4; confirm it's the `latest` dist-tag |
|
||||
| @nuxtjs/color-mode cookie storage | MEDIUM | This feature existed in v3.3+; verify it persists in latest |
|
||||
| @nuxtjs/seo as meta-bundle | MEDIUM | Module has been stable; inclusion of sitemap+og-image confirmed in v2 docs |
|
||||
| nuxt-gtag | LOW | Verify v3 is compatible with Nuxt 4 |
|
||||
| @pinia/nuxt | MEDIUM | Pinia 3 + @pinia/nuxt 0.9 tracked Nuxt 4 compat closely |
|
||||
| Docker node:22-alpine | HIGH | Node 22 is current LTS; Alpine variant is standard |
|
||||
| EmailJS (non-Nuxt) | HIGH | Stable library, no Nuxt dependency |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Training data (knowledge cutoff August 2025) — all external tools blocked during this research session
|
||||
- PROJECT.md constraints and requirements: `.planning/PROJECT.md`
|
||||
- npm registry verification required before pinning versions (see commands at top of this file)
|
||||
@@ -1,115 +0,0 @@
|
||||
# Research Summary
|
||||
|
||||
**Project:** Portfolio Killian' DAL-CIN — Vue 3 SPA → Nuxt 4 SSR Migration
|
||||
**Date:** 2026-04-07
|
||||
**Sources:** STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack (verify versions on npm before pinning)
|
||||
|
||||
| Package | Purpose | Confidence |
|
||||
|---------|---------|------------|
|
||||
| nuxt ^4.0.0 | SSR framework | MEDIUM |
|
||||
| @nuxt/ui ^3.0.0 | Component library (Tailwind v4 + Reka UI) | LOW — verify stable |
|
||||
| @nuxtjs/i18n ^9.x | FR/EN i18n SSR-safe | LOW — verify v9 stable |
|
||||
| @nuxtjs/color-mode ^3.5.x | Dark/light mode cookie | MEDIUM |
|
||||
| @nuxtjs/seo ^2.x | Meta bundle (sitemap + og-image + schema-org) | MEDIUM |
|
||||
| nuxt-gtag ^3.x | Google Analytics 4 | LOW — verify Nuxt 4 compat |
|
||||
| @pinia/nuxt ^0.9.x | State management | MEDIUM |
|
||||
| @nuxt/image ^1.x | Image optimization (NuxtImg) | MEDIUM |
|
||||
| @nuxt/eslint ^0.7.x | Linting | MEDIUM |
|
||||
| @emailjs/browser | Contact form (client-only) | HIGH |
|
||||
|
||||
**Critical decision:** Use `@nuxtjs/seo` (meta-bundle) instead of standalone `@nuxtjs/sitemap` — it includes sitemap + og-image + schema-org in one module.
|
||||
|
||||
**Do NOT install separately:** Tailwind CSS (bundled by @nuxt/ui v3), vue-router (Nuxt manages it), @nuxtjs/sitemap (included in @nuxtjs/seo).
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
app/ ← Nuxt 4 source root
|
||||
pages/ ← File-based routing (7 pages)
|
||||
components/ ← Auto-imported UI components
|
||||
composables/ ← Auto-imported reactive logic
|
||||
layouts/default.vue ← TheHeader + slot + TheFooter
|
||||
plugins/ ← EmailJS init (client-only)
|
||||
i18n/locales/ ← fr.ts, en.ts
|
||||
data/ ← Static TS files (projects, testimonials, FAQ, tech)
|
||||
public/ ← Images, fonts
|
||||
nuxt.config.ts ← Modules + config
|
||||
app.config.ts ← Nuxt UI theme customization
|
||||
```
|
||||
|
||||
**Data flow:** `data/*.ts` → `composables/` → `pages/` → `components/` (props down, events up)
|
||||
|
||||
**i18n strategy:** `prefix_except_default` — FR at `/`, EN at `/en/*`. Cookie persistence, SSR-safe.
|
||||
|
||||
**Deployment:** `nuxt build` (SSR) → Docker `node:22-alpine` → `node .output/server/index.mjs`. No nginx needed.
|
||||
|
||||
---
|
||||
|
||||
## Nuxt UI v3 Component Coverage (~80% of custom components replaced)
|
||||
|
||||
| Current Custom | Nuxt UI v3 Replacement |
|
||||
|----------------|----------------------|
|
||||
| AppHeader nav | UNavigationMenu |
|
||||
| Mobile menu | UDrawer |
|
||||
| ThemeToggle | UButton + useColorMode() |
|
||||
| LanguageSwitcher | UDropdownMenu |
|
||||
| ProjectCard | UCard |
|
||||
| GalleryModal | UModal + UCarousel |
|
||||
| ServiceFAQ | UAccordion |
|
||||
| TechBadge | UBadge |
|
||||
| FiverrServiceCard | UCard |
|
||||
| Contact form (NEW) | UForm + UFormField + UInput + UTextarea |
|
||||
| Toast feedback (NEW) | useToast() |
|
||||
|
||||
---
|
||||
|
||||
## Top 5 Pitfalls (Phase 1 blockers)
|
||||
|
||||
1. **localStorage → cookie** — Both locale and theme use localStorage in SPA. Must be cookie-only for SSR. Test with `curl` + cookie header.
|
||||
2. **@nuxtjs/i18n v9 config** — Breaking changes from v8. Must set `vueI18n: './i18n.config.ts'` explicitly or translations render as keys in SSR.
|
||||
3. **Color mode FOUC** — Even with cookie, Tailwind v4 dark variant must align with color-mode class. Test with CPU throttle.
|
||||
4. **Nuxt 4 `app/` directory** — Source files must live under `app/`. Root-level files are NOT auto-imported.
|
||||
5. **Nuxt UI v3 owns Tailwind config** — No standalone `tailwind.config.ts`. Customize via `app.config.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Build Order (strict dependencies)
|
||||
|
||||
```
|
||||
1. nuxt.config.ts + all modules configured → gates everything
|
||||
2. data/ migration (static TS files) → gates composables
|
||||
3. composables/ migration → gates pages
|
||||
4. layouts/default.vue + TheHeader + TheFooter → gates all pages
|
||||
5. pages/ (leaf nodes, parallelizable) → independent
|
||||
6. plugins/ (EmailJS, gtag) → after contact page
|
||||
7. Dockerfile production → after all pages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions Confirmed by Research
|
||||
|
||||
| Decision | Research Finding |
|
||||
|----------|-----------------|
|
||||
| SSR over SSG | i18n cookie detection requires server execution per request |
|
||||
| @nuxtjs/seo over standalone sitemap | Portfolio needs og:image + JSON-LD — meta-bundle covers all |
|
||||
| Static TS data over @nuxt/content | Data is typed, bilingual, static — no CMS overhead needed |
|
||||
| Cookie over localStorage | Only SSR-safe persistence method for i18n + theme |
|
||||
| No standalone tailwind.config | Nuxt UI v3 manages Tailwind v4 via CSS — customize in app.config.ts |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions (verify before implementation)
|
||||
|
||||
- [ ] Confirm @nuxtjs/i18n v9 is `latest` on npm and compatible with Nuxt 4
|
||||
- [ ] Confirm @nuxt/ui v3 is stable (not beta/rc)
|
||||
- [ ] Confirm nuxt-gtag works with Nuxt 4
|
||||
- [ ] Confirm UCarousel exists in Nuxt UI v3 stable
|
||||
- [ ] Confirm exact i18n v9 config syntax for `prefix_except_default` + cookie
|
||||
- [ ] Confirm Nuxt UI v3 theming API (CSS `@theme` vs `app.config.ts`)
|
||||
Reference in New Issue
Block a user