diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 4ebf629..5342861 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -27,7 +27,10 @@
3. `package.json` ne contient ni `"latest"` ni `"*"` dans les deps
4. `siteConfig.seo.organization.aggregateRating.reviewCount` correspond a `testimonials.totalReviews`
5. 10 requetes POST rapides sur `/api/contact` → les dernieres sont rejetees (rate limit)
-**Plans**: TBD
+**Plans:** 2 plans
+Plans:
+- [ ] 01-01-PLAN.md — Delete static sitemap, pin deps, fix data inconsistencies
+- [ ] 01-02-PLAN.md — Migrate Dockerfile to pnpm, add contact API rate limiting
### Phase 2: Content
**Goal**: Un visiteur comprend immediatement que Killian est dev Hytale, peut voir les services/prix, et lire des temoignages clients
@@ -71,7 +74,7 @@
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Cleanup & Fixes | 0/? | Not started | - |
+| 1. Cleanup & Fixes | 0/2 | Planning complete | - |
| 2. Content | 0/? | Not started | - |
| 3. SEO & i18n | 0/? | Not started | - |
| 4. Ship | 0/? | Not started | - |
diff --git a/.planning/phases/01-cleanup-fixes/01-01-PLAN.md b/.planning/phases/01-cleanup-fixes/01-01-PLAN.md
new file mode 100644
index 0000000..5756e14
--- /dev/null
+++ b/.planning/phases/01-cleanup-fixes/01-01-PLAN.md
@@ -0,0 +1,138 @@
+---
+phase: 01-cleanup-fixes
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - public/sitemap.xml
+ - package.json
+ - app/data/site.ts
+autonomous: true
+requirements:
+ - FIX-01
+ - FIX-04
+ - FIX-05
+must_haves:
+ truths:
+ - "public/sitemap.xml no longer exists so @nuxtjs/sitemap serves the dynamic sitemap"
+ - "package.json has no 'latest' or '*' version specs"
+ - "reviewCount in site.ts matches totalReviews in testimonials.ts (both 10)"
+ - "Fiverr placeholder URLs '#' are replaced with the profile URL"
+ artifacts:
+ - path: "package.json"
+ provides: "Pinned vue and vue-router versions"
+ contains: "\"vue\": \"^3.5.0\""
+ - path: "app/data/site.ts"
+ provides: "Consistent review data and valid Fiverr URLs"
+ contains: "reviewCount: '10'"
+ key_links:
+ - from: "app/data/site.ts"
+ to: "app/data/testimonials.ts"
+ via: "reviewCount must equal totalReviews"
+ pattern: "reviewCount.*10"
+---
+
+
+Fix static sitemap conflict, pin dangerous dependency versions, and correct data inconsistencies.
+
+Purpose: Eliminate config conflicts and data integrity issues that affect SEO and build reproducibility.
+Output: Clean package.json, no static sitemap, consistent site data.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/research/PITFALLS.md
+
+
+
+
+
+ Task 1: Delete static sitemap and pin dependency versions
+ public/sitemap.xml, package.json
+ public/sitemap.xml, package.json
+
+1. Delete `public/sitemap.xml` entirely. This static file overrides the `@nuxtjs/sitemap` module dynamic route. Nitro serves `public/` files before server routes, so the module handler at `/sitemap.xml` is never reached while this file exists.
+
+2. In `package.json`, replace the two dangerous `"latest"` specs:
+ - Change `"vue": "latest"` to `"vue": "^3.5.0"`
+ - Change `"vue-router": "latest"` to `"vue-router": "^4.5.0"`
+
+Do NOT run `pnpm install` -- just update the version specs. The lockfile already has correct resolved versions.
+
+
+ bash -c "test ! -f public/sitemap.xml && echo 'sitemap deleted' || echo 'FAIL: sitemap exists'" && grep -c '"latest"' package.json | grep -q '^0$' && echo "no latest found" || echo "FAIL: latest still in package.json"
+
+
+- `public/sitemap.xml` does not exist
+- `grep '"latest"' package.json` returns zero matches
+- `grep '"vue": "\\^3.5.0"' package.json` returns a match
+- `grep '"vue-router": "\\^4.5.0"' package.json` returns a match
+
+ Static sitemap removed, vue and vue-router pinned to caret ranges
+
+
+
+ Task 2: Fix data inconsistencies in site.ts
+ app/data/site.ts, app/data/testimonials.ts
+ app/data/site.ts
+
+In `app/data/site.ts`, fix these inconsistencies:
+
+1. **reviewCount mismatch**: On line ~99, change `reviewCount: '50'` to `reviewCount: '10'`. The testimonials.ts file has `totalReviews: 10` -- these must match. Google penalises inflated aggregateRating claims in structured data.
+
+2. **Fiverr placeholder URLs**: On lines ~61 and ~67, two services have `url: '#'`:
+ - `id: 'telegram-bot'` (line ~61): change `url: '#'` to `url: 'https://www.fiverr.com/users/mr_kayjaydee'` (link to profile since no dedicated gig page exists)
+ - `id: 'website-development'` (line ~67): change `url: '#'` to `url: 'https://www.fiverr.com/users/mr_kayjaydee'` (same fallback)
+
+These are the Fiverr profile URL already defined at `fiverr.profileUrl` in the same file.
+
+
+ bash -c "grep -q \"reviewCount: '10'\" app/data/site.ts && echo 'reviewCount OK' || echo 'FAIL: reviewCount'" && bash -c "grep -c \"url: '#'\" app/data/site.ts | grep -q '^0$' && echo 'no placeholder URLs' || echo 'FAIL: placeholder URLs remain'"
+
+
+- `grep "reviewCount: '10'" app/data/site.ts` returns a match
+- `grep "reviewCount: '50'" app/data/site.ts` returns zero matches
+- `grep "url: '#'" app/data/site.ts` returns zero matches
+- Both telegram-bot and website-development services have `url: 'https://www.fiverr.com/users/mr_kayjaydee'`
+
+ reviewCount matches totalReviews (10), Fiverr placeholder URLs replaced with profile URL
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Static assets vs server routes | `public/` files override Nitro server handlers |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-01 | Information Disclosure | aggregateRating JSON-LD | mitigate | Set reviewCount to actual value (10) to avoid Google penalty for inflated claims |
+| T-01-02 | Tampering | package.json "latest" | mitigate | Pin to caret ranges to prevent unvetted major version upgrades |
+
+
+
+- `ls public/sitemap.xml` fails (file deleted)
+- `grep '"latest"' package.json` returns 0 matches
+- `grep "reviewCount: '10'" app/data/site.ts` returns 1 match
+- `grep "url: '#'" app/data/site.ts` returns 0 matches
+
+
+
+Static sitemap removed, deps pinned, site data consistent with testimonials data.
+
+
+
diff --git a/.planning/phases/01-cleanup-fixes/01-02-PLAN.md b/.planning/phases/01-cleanup-fixes/01-02-PLAN.md
new file mode 100644
index 0000000..787949a
--- /dev/null
+++ b/.planning/phases/01-cleanup-fixes/01-02-PLAN.md
@@ -0,0 +1,208 @@
+---
+phase: 01-cleanup-fixes
+plan: 02
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - Dockerfile
+ - server/plugins/rate-limit.ts
+ - server/api/contact.post.ts
+autonomous: true
+requirements:
+ - FIX-02
+ - FIX-03
+ - DEPLOY-01
+must_haves:
+ truths:
+ - "Dockerfile uses pnpm install --frozen-lockfile, not npm"
+ - "Rapid POST requests to /api/contact are rejected with 429 after the limit"
+ - "Docker build succeeds with pnpm"
+ artifacts:
+ - path: "Dockerfile"
+ provides: "pnpm-based Docker build"
+ contains: "pnpm install --frozen-lockfile"
+ - path: "server/plugins/rate-limit.ts"
+ provides: "In-memory rate limiting for contact API"
+ contains: "429"
+ key_links:
+ - from: "server/plugins/rate-limit.ts"
+ to: "/api/contact"
+ via: "Nitro request hook filtering on path"
+ pattern: "/api/contact"
+---
+
+
+Migrate Dockerfile from npm to pnpm and add rate limiting to the contact API endpoint.
+
+Purpose: Fix build reproducibility (pnpm lockfile used in Docker) and protect against email flooding via unthrottled contact form submissions.
+Output: Working Dockerfile with pnpm, rate-limited contact endpoint.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/research/PITFALLS.md
+@.planning/research/STACK.md
+
+
+
+
+
+ Task 1: Migrate Dockerfile to pnpm
+ Dockerfile, package.json
+ Dockerfile
+
+Replace the entire Dockerfile with:
+
+```dockerfile
+# Stage 1: Build
+FROM node:22-alpine AS builder
+WORKDIR /app
+
+# Install pnpm via corepack
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+# Copy manifests first for layer caching
+COPY package.json pnpm-lock.yaml ./
+
+# Install all dependencies (including devDeps needed for build)
+RUN pnpm install --frozen-lockfile
+
+# Copy source and build
+COPY . .
+RUN pnpm build
+
+# Stage 2: Runtime
+FROM node:22-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+ENV HOST=0.0.0.0
+ENV PORT=3000
+
+# Nuxt SSR bundles all server deps into .output/server/
+COPY --from=builder /app/.output /app/.output
+
+EXPOSE 3000
+CMD ["node", "/app/.output/server/index.mjs"]
+```
+
+Key changes from original:
+- `corepack enable` + `corepack prepare pnpm@latest --activate` instead of relying on npm
+- `COPY package.json pnpm-lock.yaml ./` instead of `COPY package*.json ./`
+- `pnpm install --frozen-lockfile` instead of `npm ci`
+- `pnpm build` instead of `npm run build`
+- Explicit ENV vars for NODE_ENV, HOST, PORT in runtime stage
+
+
+ bash -c "grep -q 'pnpm install --frozen-lockfile' Dockerfile && grep -q 'corepack enable' Dockerfile && grep -q 'pnpm build' Dockerfile && ! grep -q 'npm' Dockerfile && echo 'Dockerfile OK' || echo 'FAIL'"
+
+
+- `grep 'pnpm install --frozen-lockfile' Dockerfile` returns a match
+- `grep 'corepack enable' Dockerfile` returns a match
+- `grep 'pnpm build' Dockerfile` returns a match
+- `grep 'npm' Dockerfile` returns zero matches
+- `grep 'pnpm-lock.yaml' Dockerfile` returns a match
+
+ Dockerfile uses pnpm exclusively with frozen lockfile for reproducible builds
+
+
+
+ Task 2: Add rate limiting to contact API
+ server/api/contact.post.ts
+ server/plugins/rate-limit.ts
+
+Create `server/plugins/rate-limit.ts` as a Nitro server plugin implementing in-memory IP-based rate limiting for the contact endpoint.
+
+```typescript
+// server/plugins/rate-limit.ts
+const ipMap = new Map()
+
+// Clean stale entries every 5 minutes to prevent memory leak
+setInterval(() => {
+ const now = Date.now()
+ for (const [ip, entry] of ipMap) {
+ if (entry.reset < now) ipMap.delete(ip)
+ }
+}, 5 * 60 * 1000)
+
+export default defineNitroPlugin((nitro) => {
+ nitro.hooks.hook('request', (event) => {
+ // Only rate-limit the contact POST endpoint
+ if (event.method !== 'POST' || !event.path.startsWith('/api/contact')) return
+
+ const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
+ const now = Date.now()
+ const window = 60_000 // 1 minute window
+ const limit = 3 // max 3 requests per minute per IP
+
+ const entry = ipMap.get(ip)
+ if (!entry || entry.reset < now) {
+ ipMap.set(ip, { count: 1, reset: now + window })
+ return
+ }
+
+ entry.count++
+ if (entry.count > limit) {
+ throw createError({ statusCode: 429, message: 'Too many requests. Please try again later.' })
+ }
+ })
+})
+```
+
+This uses Nitro's built-in `getRequestIP` and `createError` helpers (auto-imported in server context). The rate limit is 3 requests per IP per 60-second window. The 4th+ request within the window gets a 429 response.
+
+The plugin hooks into ALL requests but filters to only `/api/contact` POST. No changes needed to `contact.post.ts` itself.
+
+
+ bash -c "test -f server/plugins/rate-limit.ts && grep -q '429' server/plugins/rate-limit.ts && grep -q '/api/contact' server/plugins/rate-limit.ts && echo 'rate-limit OK' || echo 'FAIL'"
+
+
+- `server/plugins/rate-limit.ts` exists
+- File contains `statusCode: 429`
+- File contains check for `/api/contact`
+- File contains `getRequestIP`
+- File contains `Map`
+- Rate limit is 3 requests per 60-second window
+
+ Contact API rate-limited to 3 POST requests per IP per minute, 429 returned on excess
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Client -> /api/contact | Untrusted POST from internet, potential spam/abuse |
+| Docker build -> production | Build must use same lockfile as dev to prevent supply chain drift |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-01-03 | Denial of Service | /api/contact | mitigate | In-memory rate limit: 3 req/min/IP via Nitro plugin, returns 429 on excess |
+| T-01-04 | Elevation of Privilege | Dockerfile npm vs pnpm | mitigate | Use pnpm --frozen-lockfile to ensure exact dependency resolution matches dev |
+| T-01-05 | Tampering | Rate limit bypass via IP spoofing | accept | X-Forwarded-For can be spoofed but acceptable risk for a portfolio contact form; reverse proxy (Docker/Cloudflare) controls the header |
+
+
+
+- `grep 'pnpm install --frozen-lockfile' Dockerfile` succeeds
+- `grep -c 'npm' Dockerfile` returns 0
+- `server/plugins/rate-limit.ts` exists with 429 response
+- Rate limit targets `/api/contact` POST only
+
+
+
+Dockerfile builds with pnpm, contact API rejects rapid submissions with 429.
+
+
+