` wrapper — the layout handles structure now.
+
+
+ grep -q "AppHeader" app/layouts/default.vue && grep -q "AppFooter" app/layouts/default.vue && grep -q "simple-icons:gitea" app/components/layout/AppFooter.vue && grep -q "useLocaleHead" app/app.vue && grep -q "NuxtLayout" app/app.vue && echo "PASS" || echo "FAIL"
+
+
+ - app/components/layout/AppFooter.vue contains `simple-icons:gitea` (not github)
+ - app/components/layout/AppFooter.vue contains `simple-icons:linkedin` and `simple-icons:fiverr`
+ - app/components/layout/AppFooter.vue contains `target="_blank"` and `rel="noopener noreferrer"`
+ - app/components/layout/AppFooter.vue contains `t('footer.copyright')`
+ - app/layouts/default.vue contains `` and ``
+ - app/layouts/default.vue contains ``
+ - app/app.vue contains `useLocaleHead({ addSeoAttributes: true })`
+ - app/app.vue contains `` wrapping ``
+ - app/app.vue does NOT contain ``
+
+
AppFooter renders copyright + Gitea/LinkedIn/Fiverr social icons. Default layout wraps header + slot + footer. app.vue uses NuxtLayout and injects global hreflang/canonical via useLocaleHead().
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| External links (footer) | Social icon links open external URLs in new tabs |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-02-04 | Tampering | External social links | mitigate | All external links use `rel="noopener noreferrer"` to prevent reverse tabnabbing |
+| T-02-05 | Spoofing | Locale switching | accept | setLocale only accepts 'fr' or 'en' — constrained by i18n config, no injection risk |
+
+
+
+- `pnpm dev` starts and renders header + footer on localhost:3000
+- Language toggle switches between FR/EN URLs
+- Theme toggle switches dark/light classes
+- Mobile hamburger opens UDrawer
+- `curl http://localhost:3000` returns HTML with `
+
+
+- Header sticky with nav links, FR/EN toggle, dark/light toggle, mobile drawer
+- Footer shows copyright and 3 social icon links
+- Default layout renders header + page content + footer
+- app.vue injects global hreflang/canonical metadata
+- All interactive elements have focus rings and ARIA labels
+
+
+
diff --git a/.planning/phases/02-ssr-shell/02-03-PLAN.md b/.planning/phases/02-ssr-shell/02-03-PLAN.md
new file mode 100644
index 0000000..5ffa184
--- /dev/null
+++ b/.planning/phases/02-ssr-shell/02-03-PLAN.md
@@ -0,0 +1,212 @@
+---
+phase: 02-ssr-shell
+plan: 03
+type: execute
+wave: 2
+depends_on: [02-01]
+files_modified:
+ - app/pages/index.vue
+ - app/pages/projects.vue
+ - app/pages/about.vue
+ - app/pages/contact.vue
+ - app/pages/fiverr.vue
+ - app/pages/formation.vue
+autonomous: true
+requirements: [SEO-01, SEO-02, SEO-04]
+
+must_haves:
+ truths:
+ - "Every route has unique title, description, og:title, og:description in SSR HTML"
+ - "Homepage includes JSON-LD Person + ProfessionalService schema"
+ - "Every route has og:image with absolute URL"
+ - "curl output for each route contains
and og:description meta tag"
+ artifacts:
+ - path: "app/pages/index.vue"
+ provides: "Homepage with SEO metadata and JSON-LD"
+ contains: "useSeoMeta"
+ - path: "app/pages/projects.vue"
+ provides: "Projects stub page with SEO metadata"
+ contains: "useSeoMeta"
+ key_links:
+ - from: "app/pages/index.vue"
+ to: "app/locales/fr.json"
+ via: "t('seo.home.title') for localized SEO"
+ pattern: "seo\\.home\\.title"
+ - from: "app/pages/index.vue"
+ to: "JSON-LD"
+ via: "useHead script tag"
+ pattern: "application/ld\\+json"
+---
+
+
+Add per-route SEO metadata (useSeoMeta) and JSON-LD structured data to all page stubs.
+
+Purpose: Every route returns correct, unique, localized SEO tags in server-rendered HTML — verifiable by curl.
+Output: 6 page files with useSeoMeta(), homepage with JSON-LD, all with og:image.
+
+
+
+@~/.claude/get-shit-done/workflows/execute-plan.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/02-ssr-shell/02-CONTEXT.md
+@.planning/phases/02-ssr-shell/02-RESEARCH.md
+@.planning/phases/02-ssr-shell/02-UI-SPEC.md
+@.planning/phases/02-ssr-shell/02-01-SUMMARY.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Task 1: Per-route SEO metadata on all page stubs
+ app/pages/index.vue, app/pages/projects.vue, app/pages/about.vue, app/pages/contact.vue, app/pages/fiverr.vue, app/pages/formation.vue
+
+ - app/pages/index.vue (current stub — will be enhanced)
+ - app/locales/fr.json (verify seo.* keys exist)
+ - .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 3: useSeoMeta per route; Pattern 4: JSON-LD)
+ - .planning/phases/02-ssr-shell/02-UI-SPEC.md (SEO Contract table)
+ - src/config/site.ts (siteConfig.seo.organization for JSON-LD schema data; url: https://killiandalcin.fr)
+
+
+Update each page stub to include `useSeoMeta()` with localized metadata. Pages remain stubs (minimal template content) — Phase 3 fills real content.
+
+**Pattern for every page** (example: projects.vue):
+```vue
+
+
+
+
+
{{ t('nav.projects') }}
+
Phase 3 content placeholder
+
+
+```
+
+Apply this pattern to all 6 pages using their respective seo.{page}.title and seo.{page}.description keys:
+- index.vue → seo.home.*
+- projects.vue → seo.projects.*
+- about.vue → seo.about.*
+- contact.vue → seo.contact.*
+- fiverr.vue → seo.fiverr.*
+- formation.vue → seo.formation.*
+
+All pages use `ogImage: 'https://killiandalcin.fr/og-image.png'` (per user decision: static image, no nuxt-og-image).
+
+**Homepage (index.vue) ADDITIONALLY gets JSON-LD** (per D-11, SEO-02):
+```typescript
+useHead({
+ script: [
+ {
+ type: 'application/ld+json',
+ innerHTML: JSON.stringify({
+ '@context': 'https://schema.org',
+ '@graph': [
+ {
+ '@type': 'Person',
+ name: 'Killian Dalcin',
+ url: 'https://killiandalcin.fr',
+ jobTitle: 'Developpeur Full Stack Freelance',
+ email: 'contact@killiandalcin.fr',
+ sameAs: [
+ 'https://linkedin.com/in/killian-dal-cin',
+ 'https://www.fiverr.com/users/mr_kayjaydee',
+ 'https://gitea.kamisama.ovh/kayjaydee',
+ ],
+ },
+ {
+ '@type': 'ProfessionalService',
+ name: 'Killian Dalcin - Developpeur Full Stack',
+ url: 'https://killiandalcin.fr',
+ logo: 'https://killiandalcin.fr/images/logo.webp',
+ priceRange: '$$$',
+ areaServed: 'Worldwide',
+ },
+ ],
+ }),
+ },
+ ],
+})
+```
+
+Create pages that do not yet exist (projects.vue, about.vue, contact.vue, fiverr.vue, formation.vue) as new files. Update existing index.vue.
+
+Each stub page template should have:
+- `` wrapper (per D-16)
+- An `
` using the nav translation key
+- A placeholder paragraph
+
+
+ grep -q "useSeoMeta" app/pages/index.vue && grep -q "application/ld+json" app/pages/index.vue && grep -q "useSeoMeta" app/pages/projects.vue && grep -q "useSeoMeta" app/pages/about.vue && grep -q "useSeoMeta" app/pages/contact.vue && grep -q "useSeoMeta" app/pages/fiverr.vue && grep -q "useSeoMeta" app/pages/formation.vue && grep -q "og-image.png" app/pages/index.vue && echo "PASS" || echo "FAIL"
+
+
+ - All 6 page files exist under app/pages/
+ - Every page contains `useSeoMeta` with title, description, ogTitle, ogDescription, ogImage
+ - ogImage value is `https://killiandalcin.fr/og-image.png` on every page
+ - index.vue contains `application/ld+json` with `Person` and `ProfessionalService`
+ - index.vue JSON-LD contains `sameAs` array with LinkedIn, Fiverr, Gitea URLs
+ - Each page uses localized seo keys: `t('seo.home.title')`, `t('seo.projects.title')`, etc.
+ - Each page template has `max-w-7xl mx-auto` wrapper
+ - `npx nuxi typecheck` passes
+
+ All 6 routes have unique, localized SEO metadata via useSeoMeta(). Homepage includes JSON-LD with Person + ProfessionalService schema. Every page has og:image with absolute URL.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| SEO meta tags | Server-rendered meta tags include user-controlled translation values |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-02-06 | Injection | JSON-LD innerHTML | mitigate | JSON.stringify() escapes special characters; no user input in JSON-LD — all values are hardcoded constants |
+| T-02-07 | Information Disclosure | og:image URL | accept | Public URL pointing to public image — no sensitive data |
+
+
+
+- `pnpm dev` then `curl http://localhost:3000` returns HTML containing ``, `og:title`, `og:description` meta tags, and JSON-LD script
+- `curl http://localhost:3000/en/` returns English title/description
+- `curl http://localhost:3000/projects` returns projects-specific title
+- Each page curl output contains `og-image.png` in a meta tag
+
+
+
+- All 6 routes have unique, localized SEO metadata in server-rendered HTML
+- Homepage JSON-LD contains Person + ProfessionalService
+- og:image present on every route with absolute URL
+- `npx nuxi typecheck` passes
+
+
+