docs: map existing codebase
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Architecture
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## SSR Strategy
|
||||
|
||||
Nuxt 4 with `ssr: true` and `compatibilityVersion: 4`. Every page renders server-side HTML with SEO metadata before hydrating client-side. Cookie-only persistence for locale and theme (no localStorage, SSR-safe).
|
||||
|
||||
## Layer Breakdown
|
||||
|
||||
```
|
||||
Pages (app/pages/)
|
||||
└─> Layout (app/layouts/default.vue)
|
||||
├─> AppHeader (nav, locale toggle, theme toggle)
|
||||
├─> Page content (slot)
|
||||
└─> AppFooter (social links, copyright)
|
||||
└─> Components (app/components/)
|
||||
└─> Composables (app/composables/)
|
||||
└─> Static Data (app/data/)
|
||||
└─> Shared Types (shared/types/)
|
||||
|
||||
Server (server/api/)
|
||||
└─> Contact POST handler (nodemailer SMTP)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Static Data + i18n
|
||||
1. `app/data/projects.ts` exports projects WITHOUT translatable fields (title, description, longDescription omitted)
|
||||
2. `app/composables/useProjects.ts` merges static data with i18n translations at runtime via `computed()`
|
||||
3. Components consume `useProjects()` which returns reactive translated data
|
||||
4. Language changes trigger recomputation automatically
|
||||
|
||||
### SSR Render Flow
|
||||
1. Request hits Nitro server
|
||||
2. Nuxt resolves locale from cookie (`i18n_redirected`) or URL prefix (`/en/`)
|
||||
3. `useLocaleHead()` in `app.vue` sets `<html lang="...">` and alternate links
|
||||
4. Page's `useSeoMeta()` resolves i18n keys server-side
|
||||
5. `useHead()` injects JSON-LD structured data
|
||||
6. Full HTML sent to client with correct locale, theme class, SEO metadata
|
||||
|
||||
### Theme Resolution
|
||||
1. `@nuxtjs/color-mode` reads `nuxt-color-mode` cookie
|
||||
2. Default: `dark` for new visitors
|
||||
3. Cookie persistence — no flash on cold load (class applied server-side)
|
||||
|
||||
### Contact Form Flow
|
||||
1. Client: Zod validation in `ContactForm.vue`
|
||||
2. POST to `/api/contact` (Nitro route)
|
||||
3. Server: manual validation, nodemailer SMTP via `useRuntimeConfig()` env vars
|
||||
4. Response: success/error JSON
|
||||
|
||||
## Module System
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `@nuxt/ui` | Component library (Nuxt UI v3) |
|
||||
| `@nuxtjs/i18n` | Internationalization (prefix_except_default, FR default) |
|
||||
| `@nuxtjs/sitemap` | Auto-generated sitemap with i18n alternates |
|
||||
| `nuxt-gtag` | Google Analytics (runtime config) |
|
||||
| `@nuxt/image` | Image optimization |
|
||||
| `@nuxt/eslint` | ESLint integration |
|
||||
|
||||
## Entry Points
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `app/app.vue` | Root — wraps in `<UApp>`, applies `useLocaleHead()` |
|
||||
| `app/layouts/default.vue` | Default layout — AppHeader + slot + AppFooter |
|
||||
| `app/error.vue` | Global error handler (404 page) |
|
||||
| `nuxt.config.ts` | App configuration |
|
||||
| `app.config.ts` | Nuxt UI theme tokens (primary color) |
|
||||
|
||||
## State Management
|
||||
|
||||
No Pinia store. All state is:
|
||||
- **Composable-scoped:** `useProjects()` returns reactive computed data
|
||||
- **Module-managed:** locale via `@nuxtjs/i18n`, theme via `@nuxtjs/color-mode`
|
||||
- **Component-local:** `ref()` / `reactive()` in `<script setup>`
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Pages: `throw createError({ status: 404 })` for invalid routes/IDs
|
||||
- `app/error.vue` catches all errors with i18n messages and navigation back
|
||||
- Contact form: `try/catch/finally` with `useToast()` user feedback
|
||||
- Server routes: `createError({ statusCode: 400 })` for validation failures
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
- **SEO:** `useSeoMeta()` per page + `useHead()` for JSON-LD + `useLocaleHead()` global
|
||||
- **Accessibility:** Semantic HTML, aria attributes, keyboard navigation
|
||||
- **i18n:** All user-facing text via `t()` keys, `te()` guards for optional keys
|
||||
- **Images:** WebP in `public/images/`, served via `@nuxt/image`
|
||||
|
||||
---
|
||||
|
||||
*Architecture analysis: 2026-04-10*
|
||||
@@ -0,0 +1,40 @@
|
||||
# Concerns
|
||||
|
||||
## Security
|
||||
|
||||
- No rate limiting on `server/api/contact.post.ts` — the contact API accepts unlimited POST requests, enabling spam/email flooding
|
||||
- No CAPTCHA or honeypot bot protection on `app/components/ContactForm.vue`
|
||||
- `.env.example` only documents `NUXT_PUBLIC_GTAG_ID` but the contact form requires four SMTP vars (`NUXT_SMTP_HOST`, `NUXT_SMTP_USER`, `NUXT_SMTP_PASS`, `NUXT_SMTP_TO`) with no documentation
|
||||
- Server-side email validation in `contact.post.ts` line 12 uses `email.includes('@')` instead of a proper regex, while client-side already uses Zod's `z.string().email()`
|
||||
|
||||
## Tech Debt
|
||||
|
||||
- `'https://killiandalcin.fr/og-image.png'` hardcoded verbatim in 6 page files — any domain change requires editing all of them
|
||||
- Static `public/sitemap.xml` bypasses the installed `@nuxtjs/sitemap` module — new projects are never indexed, and `/formation` in the sitemap has no matching page
|
||||
- Both `package-lock.json` (npm) and `pnpm-lock.yaml` (pnpm) coexist; `Dockerfile` uses `npm ci` after migration to pnpm
|
||||
- `flowboard` project `features[]` array in `app/data/projects.ts` (lines 91-97) is hardcoded English, not i18n keys, while all other project content goes through `useProjects.ts`
|
||||
- `siteConfig.seo.organization.aggregateRating` in `app/data/site.ts` claims `reviewCount: '50'` while `app/data/testimonials.ts` has `totalReviews: 10` — mismatched structured data Google could flag
|
||||
- Two Fiverr services have `url: '#'` in `app/data/site.ts` — non-functional CTAs on the `/fiverr` page
|
||||
|
||||
## Performance / UX
|
||||
|
||||
- `HeroSection.vue` splits the title string by `.split(' ').slice(-2)` to apply gradient styling — breaks if the FR/EN title has a different word count
|
||||
- All testimonial avatar URLs point to `https://ui-avatars.com/api/...` (external CDN, external HTTP requests per avatar on every render)
|
||||
|
||||
## Missing SEO Features
|
||||
|
||||
- No `ogUrl` set on any page (all `useSeoMeta` calls omit it)
|
||||
- `app/pages/project/[id].vue` uses the generic `og-image.png` instead of `project.value?.image`
|
||||
- No `<link rel="canonical">` — the `prefix_except_default` i18n strategy produces `/` and `/en/` duplicate URLs without canonical deduplication
|
||||
- `/formation` in `public/sitemap.xml` has no corresponding page (`app/pages/formation.vue` does not exist)
|
||||
|
||||
## i18n Completeness
|
||||
|
||||
- `app/error.vue` lines 39-44: two hardcoded English error description strings not in locale files
|
||||
- `app/components/sections/HeroSection.vue` line 30: `'Available for projects'` badge is raw English, not `t()`
|
||||
- Same file lines 148, 153: `'50+ projects'` and `'5.0 rating'` decorative stats are hardcoded English
|
||||
- `a11y.langToggle` in both locale files hardcodes the current language name as a static string
|
||||
|
||||
## Testing
|
||||
|
||||
- Zero test files exist anywhere in the project — no coverage for the security-sensitive contact API validation, `useProjects` composable, or i18n key resolution
|
||||
@@ -0,0 +1,161 @@
|
||||
# Coding Conventions
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## Naming Patterns
|
||||
|
||||
**Files:**
|
||||
- Vue components: PascalCase — `AppHeader.vue`, `ProjectCard.vue`, `ContactForm.vue`
|
||||
- Pages (Nuxt file-based routing): kebab-case — `about.vue`, `project/[id].vue`
|
||||
- Layouts: kebab-case — `default.vue`
|
||||
- Composables: camelCase with `use` prefix — `useProjects.ts`
|
||||
- Data files: camelCase — `projects.ts`, `faq.ts`, `site.ts`, `techstack.ts`, `testimonials.ts`
|
||||
- Type files: `index.ts` in a typed directory — `shared/types/index.ts`
|
||||
- Server routes: `[name].[method].ts` Nitro convention — `contact.post.ts`
|
||||
- Config files: camelCase — `nuxt.config.ts`, `app.config.ts`
|
||||
|
||||
**Functions:**
|
||||
- Exported composables: `useX()` — `useProjects()` in `app/composables/useProjects.ts`
|
||||
- Toggle handlers: verb + noun — `toggleLocale()`, `toggleTheme()`
|
||||
- Query predicates: verb + noun — `isActive(path)`, `findById(id)`, `filterByCategory(category)`, `search(query)`
|
||||
- Async handlers: `onX` prefix — `onSubmit(event)`
|
||||
- Event handlers: `defineEventHandler` (Nitro server) — `server/api/contact.post.ts`
|
||||
|
||||
**Variables:**
|
||||
- Reactive refs: camelCase — `mobileOpen`, `loading`
|
||||
- Computed values: camelCase — `navLinks`, `translatedCategory`, `relatedProjects`, `featuredProjects`
|
||||
- Constants/config exports: camelCase — `siteConfig`, `projects`, `homeFAQs`
|
||||
|
||||
**Types:**
|
||||
- Interfaces: PascalCase — `Project`, `ProjectButton`, `Technology`, `TechStack`, `SiteConfig`, `FAQ`
|
||||
- Props interfaces: always named `Props` in `<script setup>` — see `app/components/ProjectCard.vue`
|
||||
- Type aliases derived from Zod: inline `type Schema = z.output<typeof schema>` — `app/components/ContactForm.vue`
|
||||
- Enum-like string unions: `'Beginner' | 'Intermediate' | 'Advanced'` — `Technology.level` in `shared/types/index.ts`
|
||||
|
||||
## Code Style
|
||||
|
||||
**Formatting:**
|
||||
- No dedicated Prettier config at root; formatting via ESLint through `@nuxt/eslint`
|
||||
- ESLint config `eslint.config.mjs` delegates entirely to `withNuxt()` from `.nuxt/eslint.config.mjs`
|
||||
- `@nuxt/eslint` module generates type-aware rules; `typescript: { strict: true }` in `nuxt.config.ts`
|
||||
|
||||
**Observed style from source:**
|
||||
- No semicolons
|
||||
- Single quotes for strings
|
||||
- Trailing commas in multi-line objects/arrays
|
||||
- 2-space indentation
|
||||
- Long template attribute chains are NOT broken across lines (single long lines acceptable in templates)
|
||||
|
||||
## TypeScript Usage
|
||||
|
||||
**Strict Mode:** `typescript: { strict: true }` in `nuxt.config.ts` — all strict checks enforced project-wide.
|
||||
|
||||
**Type imports:** Always use `import type` for type-only imports:
|
||||
```typescript
|
||||
import type { Project } from '~~/shared/types'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
```
|
||||
|
||||
**Props typing:** Always `defineProps<Props>()` with an explicit interface named `Props`:
|
||||
```typescript
|
||||
interface Props {
|
||||
project: Project
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
```
|
||||
|
||||
**Return type inference:** Composables rely on inference; explicit generics where needed:
|
||||
```typescript
|
||||
const projects = computed<Project[]>(() => projectsData.map(...))
|
||||
```
|
||||
|
||||
**Zod for runtime validation:** Client-side form schemas with Zod in components (`ContactForm.vue`). Server-side uses manual validation with `createError` — Zod not used on server.
|
||||
|
||||
## Import Organization
|
||||
|
||||
**Path Aliases:**
|
||||
- `~/` -> `app/` directory (Nuxt 4 convention)
|
||||
- `~~/` -> project root (for cross-layer imports: `~~/shared/types`)
|
||||
- Use `~/data/projects` for app-internal imports; `~~/shared/types` to reach shared layer
|
||||
|
||||
**Import order (observed):**
|
||||
1. Third-party: `import { z } from 'zod'`
|
||||
2. Nuxt UI types: `import type { FormSubmitEvent } from '@nuxt/ui'`
|
||||
3. Internal types: `import type { Project } from '~~/shared/types'`
|
||||
4. Internal data: `import { projects as projectsData } from '~/data/projects'`
|
||||
5. Auto-imports (no explicit import): `ref`, `computed`, `reactive`, `useI18n`, `useRoute`, `useColorMode`, `useSeoMeta`, `useHead`, `useToast`, `useLocalePath`
|
||||
|
||||
**Auto-imports:** All Nuxt/Vue composables and all `app/components/**/*.vue` components are auto-imported. Never write explicit `import ref from 'vue'` or component imports in `.vue` files. `pathPrefix: false` in `nuxt.config.ts` means `AppHeader` registers as `AppHeader` not `LayoutAppHeader`.
|
||||
|
||||
## Vue Patterns
|
||||
|
||||
**Component structure order:**
|
||||
1. `<script setup lang="ts">` — always first, always `lang="ts"`
|
||||
2. `<template>` — second
|
||||
3. No `<style>` blocks — all styling via Tailwind utility classes
|
||||
|
||||
**Composition API rules:**
|
||||
- `<script setup>` exclusively — no Options API
|
||||
- Destructure composables at top: `const { t } = useI18n()`
|
||||
- `ref()` for mutable primitives: `const mobileOpen = ref(false)`
|
||||
- `reactive()` for multi-field form state: `const state = reactive({ name: '', email: '', message: '' })`
|
||||
- `computed()` for derived state: `const relatedProjects = computed(() => [...])`
|
||||
- `useTemplateRef()` for component refs (Vue 3.5 API): `const galleryRef = useTemplateRef('gallery')`
|
||||
|
||||
**Pages pattern:**
|
||||
- `useSeoMeta()` called at top of each page's `<script setup>` with reactive getter functions
|
||||
- Structured data injected via `useHead({ script: [{ type: 'application/ld+json', innerHTML: JSON.stringify({...}) }] })`
|
||||
- 404s thrown inline: `throw createError({ status: 404, statusText: 'Project not found' })`
|
||||
|
||||
**i18n:**
|
||||
- `useI18n()` called at component level; `t`, `locale`, `setLocale`, `te` destructured as needed
|
||||
- `useLocalePath()` used for all `<NuxtLink :to>` values: `:to="localePath('/projects')"`
|
||||
- `te()` guards optional translation keys before `t()` call
|
||||
|
||||
**Error handling:**
|
||||
- Async operations wrapped in `try/catch/finally` with user feedback via `useToast()`
|
||||
|
||||
**Template conventions:**
|
||||
- Semantic HTML: `<header>`, `<nav>`, `<article>`, `<aside>`, `<section>`, `<time>`
|
||||
- Schema.org microdata: `itemscope`, `itemtype`, `itemprop` attributes directly on elements
|
||||
- `aria-*` on all interactive elements and navigation landmarks
|
||||
- `aria-current="page"` on active nav links
|
||||
- `aria-hidden="true"` on decorative/background elements
|
||||
|
||||
## Composable Design
|
||||
|
||||
```typescript
|
||||
export function useProjects() {
|
||||
// logic
|
||||
return {
|
||||
projects,
|
||||
featuredProjects,
|
||||
filterByCategory,
|
||||
search,
|
||||
findById,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Always return a named object, never a single value
|
||||
- Filter/find functions return `computed()` so callers get reactivity
|
||||
|
||||
## Data Layer Conventions
|
||||
|
||||
**Static data** in `app/data/`:
|
||||
- Export named typed constants
|
||||
- Translatable fields omitted; resolved at runtime via i18n in the composable layer
|
||||
- `app/data/projects.ts` exports `Omit<Project, 'title' | 'description' | 'longDescription'>[]`
|
||||
|
||||
**Shared types** in `shared/types/index.ts`:
|
||||
- Single source for all domain interfaces
|
||||
- Imported by both `app/` and `server/` via `~~/shared/types`
|
||||
|
||||
**Server routes** in `server/api/`:
|
||||
- Use `defineEventHandler`, `readBody`, `useRuntimeConfig(event)`
|
||||
- Manual input validation with `createError({ statusCode: 400 })`
|
||||
- Return plain serializable objects
|
||||
|
||||
---
|
||||
|
||||
*Convention analysis: 2026-04-10*
|
||||
@@ -0,0 +1,119 @@
|
||||
# External Integrations
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## APIs & External Services
|
||||
|
||||
**Analytics:**
|
||||
- Google Analytics / Google Tag Manager via `nuxt-gtag` ^4.1.0
|
||||
- SDK/Client: `nuxt-gtag` Nuxt module
|
||||
- Auth: `NUXT_PUBLIC_GTAG_ID` env var (public runtime config)
|
||||
- Enabled only in production: `enabled: import.meta.env.NODE_ENV === 'production'`
|
||||
- Config in `nuxt.config.ts` under `gtag:` and `runtimeConfig.public.gtag`
|
||||
|
||||
## Data Storage
|
||||
|
||||
**Databases:**
|
||||
- None — all portfolio data is static (TypeScript data files in `app/data/`)
|
||||
|
||||
**File Storage:**
|
||||
- Local filesystem only — images served from `public/` or via `@nuxt/image`
|
||||
|
||||
**Caching:**
|
||||
- None — Nuxt SSR per-request rendering
|
||||
|
||||
## Authentication & Identity
|
||||
|
||||
**Auth Provider:**
|
||||
- None — no user authentication required for this portfolio site
|
||||
|
||||
## Email
|
||||
|
||||
**SMTP Email (Contact Form):**
|
||||
- Provider: Any SMTP-compatible server (configured at runtime)
|
||||
- Implementation: `nodemailer` ^8.0.5 in server API route `app/api/contact.post.ts`
|
||||
- Validation: `zod` ^4.3.6 validates request body server-side
|
||||
- Auth env vars:
|
||||
- `NUXT_SMTP_HOST` - SMTP server hostname
|
||||
- `NUXT_SMTP_USER` - SMTP credentials username
|
||||
- `NUXT_SMTP_PASS` - SMTP credentials password
|
||||
- `NUXT_SMTP_TO` - Destination email address for contact messages
|
||||
|
||||
## SEO & Discoverability
|
||||
|
||||
**Sitemap:**
|
||||
- `@nuxtjs/sitemap` ^8.0.12 — automatic XML sitemap generation
|
||||
- Base URL: `https://killiandalcin.fr` (configured in `nuxt.config.ts` under `site:`)
|
||||
- Site name: "Killian' DAL-CIN - Developpeur Full Stack"
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
**Error Tracking:**
|
||||
- None detected
|
||||
|
||||
**Logs:**
|
||||
- Standard Node.js stdout/stderr (captured by Docker/host)
|
||||
|
||||
## CI/CD & Deployment
|
||||
|
||||
**Hosting:**
|
||||
- Self-hosted Docker container on VPS
|
||||
- Image: `node:22-alpine` (multi-stage build)
|
||||
- Container port: 3000
|
||||
- Reverse proxy: Traefik
|
||||
- TLS via Let's Encrypt (`certresolver=public`)
|
||||
- Wildcard cert covering `killiandalcin.fr` and `*.killiandalcin.fr`
|
||||
- www → non-www permanent redirect middleware
|
||||
- Config via Docker labels in `docker-compose.yml`
|
||||
|
||||
**CI Pipeline:**
|
||||
- None detected — manual Docker image build and deploy
|
||||
|
||||
**Build process:**
|
||||
1. `docker build` — runs `npm ci` + `nuxt build` in `node:22-alpine`
|
||||
2. Output `.output/` copied to runtime stage
|
||||
3. `docker-compose up` starts the container with runtime env vars
|
||||
|
||||
## Internationalization
|
||||
|
||||
**i18n Provider:**
|
||||
- `@nuxtjs/i18n` ^10.2.4
|
||||
- Strategy: `prefix_except_default` (French at `/`, English at `/en/`)
|
||||
- Default locale: `fr`
|
||||
- Supported locales: `fr` (fr-FR), `en` (en-US)
|
||||
- Locale files: `i18n/locales/fr.json`, `i18n/locales/en.json`
|
||||
- Browser detection: cookie-based (`i18n_redirected`) for SSR safety
|
||||
|
||||
## Image Optimization
|
||||
|
||||
**Provider:**
|
||||
- `@nuxt/image` ^2.0.0
|
||||
- Default provider: local (no external image CDN configured)
|
||||
- Images served from `public/`
|
||||
|
||||
## Webhooks & Callbacks
|
||||
|
||||
**Incoming:**
|
||||
- `POST /api/contact` — contact form submission endpoint (`app/api/contact.post.ts`)
|
||||
|
||||
**Outgoing:**
|
||||
- None
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
**Required env vars (production):**
|
||||
- `NUXT_SMTP_HOST` - SMTP server hostname
|
||||
- `NUXT_SMTP_USER` - SMTP username
|
||||
- `NUXT_SMTP_PASS` - SMTP password
|
||||
- `NUXT_SMTP_TO` - Contact form recipient email
|
||||
- `NUXT_PUBLIC_GTAG_ID` - Google Analytics tag ID
|
||||
- `PORTFOLIO_URL` - Primary domain (used in Traefik labels)
|
||||
- `PORTFOLIO_URL_WWW` - WWW variant (used in Traefik www-redirect rule)
|
||||
|
||||
**Secrets location:**
|
||||
- Passed as Docker environment variables at runtime (not committed to repo)
|
||||
- `docker-compose.yml` reads from host environment via `${VAR_NAME}` syntax
|
||||
|
||||
---
|
||||
|
||||
*Integration audit: 2026-04-10*
|
||||
@@ -0,0 +1,103 @@
|
||||
# Technology Stack
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## Languages
|
||||
|
||||
**Primary:**
|
||||
- TypeScript ~5.8.0 - Full application (strict mode enforced via `nuxt.config.ts`)
|
||||
- HTML5 - Server-rendered markup via Nuxt SSR
|
||||
|
||||
**Secondary:**
|
||||
- CSS - Styling via Tailwind CSS v4 (`app/assets/css/main.css`)
|
||||
|
||||
## Runtime
|
||||
|
||||
**Environment:**
|
||||
- Node.js 22 (Alpine) - Development, build, and production server
|
||||
|
||||
**Package Manager:**
|
||||
- pnpm (primary — `pnpm-lock.yaml` present)
|
||||
- npm also supported (used in Dockerfile via `npm ci`)
|
||||
- Lockfile: both `pnpm-lock.yaml` and `package-lock.json` present
|
||||
|
||||
## Frameworks
|
||||
|
||||
**Core:**
|
||||
- Nuxt 4 (`^4.0.0`) - SSR framework, `compatibilityVersion: 4` set in `nuxt.config.ts`
|
||||
- Vue (latest) - Component framework
|
||||
- Vue Router (latest) - File-based routing via Nuxt
|
||||
|
||||
**Nuxt Modules:**
|
||||
- `@nuxt/ui` ^3.0.0 - Component library (Tailwind v4 based, configured in `app.config.ts`)
|
||||
- `@nuxtjs/i18n` ^10.2.4 - Internationalization with FR/EN support
|
||||
- `@nuxtjs/sitemap` ^8.0.12 - Automatic sitemap generation
|
||||
- `nuxt-gtag` ^4.1.0 - Google Analytics/Google Tag Manager integration
|
||||
- `@nuxt/image` ^2.0.0 - Optimized image handling
|
||||
- `@nuxt/eslint` ^1.15.2 - ESLint integration
|
||||
|
||||
**Build/Dev:**
|
||||
- Tailwind CSS ^4.2.2 (devDependency — compiled at build time)
|
||||
- ESLint (via `@nuxt/eslint`) - Config: `eslint.config.mjs`
|
||||
- TypeScript ~5.8.0 - Compiler and type checking via `nuxt typecheck`
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
**Critical:**
|
||||
- `nuxt` ^4.0.0 - Core framework with SSR engine
|
||||
- `@nuxt/ui` ^3.0.0 - Provides all UI primitives (buttons, forms, modals, etc.)
|
||||
- `@nuxtjs/i18n` ^10.2.4 - Multilingual routing (`fr` default, `en` prefixed via `/en/`)
|
||||
- `zod` ^4.3.6 - Schema validation (used in server API routes for contact form)
|
||||
|
||||
**Infrastructure:**
|
||||
- `nodemailer` ^8.0.5 - SMTP email sending from server API (`app/api/contact.post.ts`)
|
||||
- `@types/nodemailer` ^8.0.0 - Type definitions for nodemailer
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment:**
|
||||
- No `.env` committed; secrets passed at runtime via Docker environment variables
|
||||
- Key runtime config variables (defined in `nuxt.config.ts` `runtimeConfig`):
|
||||
- `NUXT_SMTP_HOST` - SMTP server hostname
|
||||
- `NUXT_SMTP_USER` - SMTP username
|
||||
- `NUXT_SMTP_PASS` - SMTP password
|
||||
- `NUXT_SMTP_TO` - Recipient email address
|
||||
- `NUXT_PUBLIC_GTAG_ID` - Google Analytics tag ID
|
||||
|
||||
**Build:**
|
||||
- `nuxt.config.ts` - Main Nuxt configuration (SSR enabled, modules, i18n, color mode, sitemap)
|
||||
- `app.config.ts` - App-level UI config (primary color: `brand`)
|
||||
- `tsconfig.json` + `tsconfig.app.json` + `tsconfig.node.json` - TypeScript project references
|
||||
- `eslint.config.mjs` - ESLint flat config
|
||||
|
||||
**Key nuxt.config.ts settings:**
|
||||
- `ssr: true` — SSR always enabled
|
||||
- `colorMode.storage: 'cookie'` — SSR-safe theme persistence
|
||||
- `i18n.detectBrowserLanguage.useCookie: true` — SSR-safe locale detection
|
||||
- `typescript.strict: true` — Strict TypeScript mode
|
||||
|
||||
## Platform Requirements
|
||||
|
||||
**Development:**
|
||||
- Node.js 22+
|
||||
- pnpm (or npm)
|
||||
|
||||
**Production:**
|
||||
- Docker with Node.js 22 Alpine image
|
||||
- SSR server runs on port 3000 (`node .output/server/index.mjs`)
|
||||
- Reverse proxy: Traefik (TLS termination, www redirect, routing)
|
||||
|
||||
## Scripts & Commands
|
||||
|
||||
```bash
|
||||
pnpm dev # Start dev server with HMR
|
||||
pnpm build # Build SSR bundle → .output/
|
||||
pnpm generate # Static generation (SSG mode)
|
||||
pnpm preview # Preview built output
|
||||
pnpm lint # Run ESLint
|
||||
pnpm typecheck # Run nuxt typecheck (vue-tsc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Stack analysis: 2026-04-10*
|
||||
@@ -0,0 +1,95 @@
|
||||
# Structure
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
portfolio/
|
||||
├── app/ # Nuxt 4 app directory (srcDir)
|
||||
│ ├── app.vue # Root component (UApp wrapper)
|
||||
│ ├── error.vue # Error/404 page
|
||||
│ ├── assets/css/main.css # Global CSS (Tailwind imports)
|
||||
│ ├── components/
|
||||
│ │ ├── layout/
|
||||
│ │ │ ├── AppHeader.vue # Sticky header with nav, locale/theme toggles
|
||||
│ │ │ └── AppFooter.vue # Footer with social links, copyright
|
||||
│ │ ├── sections/
|
||||
│ │ │ ├── HeroSection.vue # Landing hero with CTA
|
||||
│ │ │ ├── FeaturedProjectsSection.vue
|
||||
│ │ │ ├── ServicesSection.vue
|
||||
│ │ │ ├── TestimonialsSection.vue
|
||||
│ │ │ ├── FAQSection.vue
|
||||
│ │ │ └── CTASection.vue
|
||||
│ │ ├── ContactForm.vue # Form with Zod validation + honeypot
|
||||
│ │ ├── ProjectCard.vue # Project display card
|
||||
│ │ ├── ProjectGallery.vue # Image gallery modal
|
||||
│ │ └── TechBadge.vue # Technology badge with icon
|
||||
│ ├── composables/
|
||||
│ │ └── useProjects.ts # Project data access + i18n + filtering
|
||||
│ ├── data/ # Static typed data
|
||||
│ │ ├── projects.ts # 7 projects (Omit translatable fields)
|
||||
│ │ ├── testimonials.ts # Client testimonials
|
||||
│ │ ├── techstack.ts # Technology categories
|
||||
│ │ ├── faq.ts # FAQ entries (i18n keys)
|
||||
│ │ └── site.ts # Site config (SEO, contact, social)
|
||||
│ ├── layouts/
|
||||
│ │ └── default.vue # Main layout (header + slot + footer)
|
||||
│ └── pages/ # File-based routing
|
||||
│ ├── index.vue # Homepage
|
||||
│ ├── about.vue # About page
|
||||
│ ├── contact.vue # Contact form page
|
||||
│ ├── projects.vue # Project listing with filters
|
||||
│ ├── fiverr.vue # Fiverr services page
|
||||
│ └── project/[id].vue # Dynamic project detail
|
||||
├── i18n/locales/ # Translation files
|
||||
│ ├── fr.json # French (default locale)
|
||||
│ └── en.json # English
|
||||
├── server/api/
|
||||
│ └── contact.post.ts # Contact form POST handler (nodemailer)
|
||||
├── shared/types/
|
||||
│ └── index.ts # All TypeScript interfaces
|
||||
├── public/images/ # Static images (WebP)
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── app.config.ts # Nuxt UI theme tokens
|
||||
├── Dockerfile # Multi-stage SSR build (node:22-alpine)
|
||||
├── docker-compose.yml # Docker compose with Traefik
|
||||
├── package.json # Dependencies (pnpm)
|
||||
└── pnpm-lock.yaml # pnpm lockfile
|
||||
```
|
||||
|
||||
## Page Inventory
|
||||
|
||||
| Route | File | Description |
|
||||
|-------|------|-------------|
|
||||
| `/` | `index.vue` | Homepage with 6 sections (hero, projects, services, testimonials, FAQ, CTA) |
|
||||
| `/about` | `about.vue` | About page with tech stack badges |
|
||||
| `/projects` | `projects.vue` | Project listing with search + category filters |
|
||||
| `/project/:id` | `project/[id].vue` | Dynamic project detail with gallery |
|
||||
| `/contact` | `contact.vue` | Contact form page |
|
||||
| `/fiverr` | `fiverr.vue` | Fiverr services page |
|
||||
| `/en/*` | (same files) | English prefix routes via i18n |
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
- **Layout components** (`layout/`): AppHeader, AppFooter — used in `default.vue` layout
|
||||
- **Section components** (`sections/`): 6 homepage sections — composed in `index.vue`
|
||||
- **Shared components** (root): ContactForm, ProjectCard, ProjectGallery, TechBadge — reused across pages
|
||||
|
||||
All components auto-imported with `pathPrefix: false` — use `AppHeader` not `LayoutAppHeader`.
|
||||
|
||||
## Where to Add Things
|
||||
|
||||
| To add... | Location |
|
||||
|-----------|----------|
|
||||
| New page | `app/pages/newpage.vue` (auto-routed) |
|
||||
| New component | `app/components/` (auto-imported) |
|
||||
| New section | `app/components/sections/` |
|
||||
| New API route | `server/api/name.method.ts` |
|
||||
| New data file | `app/data/name.ts` |
|
||||
| New type | `shared/types/index.ts` |
|
||||
| New i18n keys | `i18n/locales/fr.json` + `en.json` |
|
||||
|
||||
---
|
||||
|
||||
*Structure analysis: 2026-04-10*
|
||||
@@ -0,0 +1,43 @@
|
||||
# Testing Patterns
|
||||
|
||||
**Analysis Date:** 2026-04-10
|
||||
|
||||
## Test Framework
|
||||
|
||||
**Runner:** None detected
|
||||
**Assertion Library:** None detected
|
||||
|
||||
No test runner or test framework is installed. `package.json` contains no testing dependencies (no Vitest, Jest, Playwright, Cypress, or any `@testing-library/*` package). No `test` script is defined in `package.json`.
|
||||
|
||||
**Run Commands:**
|
||||
```bash
|
||||
# No test commands available
|
||||
pnpm run lint # ESLint only
|
||||
pnpm run typecheck # Nuxt type checking (vue-tsc via nuxt typecheck)
|
||||
```
|
||||
|
||||
## Test File Organization
|
||||
|
||||
No test files exist in the codebase. A search for `*.test.*` and `*.spec.*` across the entire project returned no results.
|
||||
|
||||
## What Currently Exists as Quality Gates
|
||||
|
||||
**TypeScript strict mode** (`nuxt.config.ts`):
|
||||
- `typescript: { strict: true }` — all strict checks enforced at compile time
|
||||
- `pnpm run typecheck` runs `nuxt typecheck` (wraps vue-tsc)
|
||||
|
||||
**ESLint** (`eslint.config.mjs`):
|
||||
- `@nuxt/eslint` module with auto-generated type-aware rules
|
||||
- `pnpm run lint` runs `eslint .`
|
||||
|
||||
**Runtime validation:**
|
||||
- Client side: Zod schema in `app/components/ContactForm.vue` validates form input before API call
|
||||
- Server side: Manual validation in `server/api/contact.post.ts` rejects malformed payloads with HTTP 400
|
||||
|
||||
## Test Coverage
|
||||
|
||||
**Current coverage: 0%** — no automated tests of any kind.
|
||||
|
||||
---
|
||||
|
||||
*Testing analysis: 2026-04-10*
|
||||
Reference in New Issue
Block a user