docs: add project research

This commit is contained in:
2026-04-07 23:17:32 +02:00
parent 2dff0a3d6c
commit 3771e4abe9
5 changed files with 1025 additions and 0 deletions
+230
View File
@@ -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 + `<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)
+155
View File
@@ -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 `<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.
+285
View File
@@ -0,0 +1,285 @@
# 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.
+240
View File
@@ -0,0 +1,240 @@
# Technology Stack
**Project:** Portfolio Killian Dalcin — 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)
+115
View File
@@ -0,0 +1,115 @@
# Research Summary
**Project:** Portfolio Killian Dalcin — 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`)