--- 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" --- <objective> 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. </objective> <execution_context> @~/.claude/get-shit-done/workflows/execute-plan.md @~/.claude/get-shit-done/templates/summary.md </execution_context> <context> @.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 <interfaces> <!-- From app/locales/fr.json (Plan 01): seo.home.title, seo.home.description, seo.projects.title, etc. --> <!-- From nuxt.config.ts (Plan 01): site.url = 'https://killiandalcin.fr' --> <!-- From public/og-image.png (Plan 01): static og:image file --> <!-- Nuxt built-in: useSeoMeta(), useHead() — auto-imported --> <!-- @nuxtjs/i18n: useI18n() for t() function --> </interfaces> </context> <tasks> <task type="auto"> <name>Task 1: Per-route SEO metadata on all page stubs</name> <files>app/pages/index.vue, app/pages/projects.vue, app/pages/about.vue, app/pages/contact.vue, app/pages/fiverr.vue, app/pages/formation.vue</files> <read_first> - 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) </read_first> <action> 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 <script setup lang="ts"> const { t } = useI18n() useSeoMeta({ title: () => t('seo.projects.title'), description: () => t('seo.projects.description'), ogTitle: () => t('seo.projects.title'), ogDescription: () => t('seo.projects.description'), ogImage: 'https://killiandalcin.fr/og-image.png', ogImageWidth: 1200, ogImageHeight: 630, ogType: 'website', }) </script> <template> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <h1 class="text-2xl font-bold">{{ t('nav.projects') }}</h1> <p class="text-gray-600 dark:text-gray-400 mt-4">Phase 3 content placeholder</p> </div> </template> ``` 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' DAL-CIN', 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' DAL-CIN - 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: - `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">` wrapper (per D-16) - An `<h1>` using the nav translation key - A placeholder paragraph </action> <verify> <automated>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"</automated> </verify> <acceptance_criteria> - 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 </acceptance_criteria> <done>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.</done> </task> </tasks> <threat_model> ## 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 | </threat_model> <verification> - `pnpm dev` then `curl http://localhost:3000` returns HTML containing `<title>`, `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 </verification> <success_criteria> - 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 </success_criteria> <output> After completion, create `.planning/phases/02-ssr-shell/02-03-SUMMARY.md` </output>