--- phase: 07-seo-blog plan: 02 type: execute wave: 2 depends_on: [07-01] files_modified: - app/utils/resolve-og-image.ts - public/og-blog-default.jpg - app/pages/blog/[slug].vue autonomous: true requirements: [SEO-10, SEO-11, SEO-13, SEO-15] must_haves: truths: - "curl /fr/blog/{slug} retourne og:title, og:description, og:image UNIQUES (par article)" - "og:image est absolute (https://...) et = frontmatter image || /og-blog-default.jpg (jamais og-image.png générique)" - "Le HTML contient un JSON-LD `@type: Article` avec headline, description, datePublished, dateModified, author (@id=#killian), publisher (@id=#killian), inLanguage, mainEntityOfPage" - "Le HTML contient un JSON-LD `@type: BreadcrumbList` Accueil → Blog → Titre" - "article:published_time et article:modified_time présents (ISO 8601)" - "og:locale:alternate émis uniquement si l'article existe dans les 2 langues" artifacts: - path: "app/utils/resolve-og-image.ts" provides: "resolveOgImage(article) → URL absolue" contains: "export function resolveOgImage" - path: "public/og-blog-default.jpg" provides: "fallback branded 1200x630" - path: "app/pages/blog/[slug].vue" provides: "useSeoMeta enrichi + useSchemaOrg([defineArticle, defineBreadcrumb])" contains: "defineArticle" key_links: - from: "app/pages/blog/[slug].vue" to: "app/utils/resolve-og-image.ts" via: "import resolveOgImage" pattern: "resolveOgImage" - from: "app/pages/blog/[slug].vue (defineArticle.author)" to: "app/app.vue (definePerson global)" via: "@id reference" pattern: "'@id': KILLIAN_PERSON_ID" --- Enrichir la page article `/blog/[slug]` avec (a) `useSeoMeta` étendu (D-15), (b) `useSchemaOrg([defineArticle, defineBreadcrumb])` (D-02, SEO-11, SEO-15), et (c) helper partagé `resolveOgImage` + asset fallback `/og-blog-default.jpg` (D-05, D-06, SEO-13). Purpose: SEO-10/11/13/15 — satisfaire les 4 success criteria curl de la phase sur `/blog/[slug]`. Output: 1 util créé, 1 asset déposé, 1 page enrichie. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-seo-blog/07-CONTEXT.md @.planning/phases/07-seo-blog/07-RESEARCH.md @.planning/phases/07-seo-blog/07-PATTERNS.md @.planning/phases/07-seo-blog/07-01-SUMMARY.md @app/pages/blog/[slug].vue @app/utils/countWords.ts @app/utils/seo-person.ts Depuis `app/utils/seo-person.ts` (créé 07-01) : - `KILLIAN_PERSON_ID = '#killian'` - `killianPerson` (pour référence) Depuis `app/pages/blog/[slug].vue` (existant, à étendre — ne PAS remplacer) : - `const { t, locale } = useI18n()` (ligne 2) - `const localePath = useLocalePath()` (ligne 3) - `const isFr = computed(() => locale.value === 'fr')` (ligne 5) - `const slug = route.params.slug as string` (ligne 6) - `const path = computed(() => ...)` (ligne 7) - `const { data: page } = await useAsyncData(...)` (lignes 10-17) — carry `title, description, date, updated?, image?, tags?` - `useSeoMeta({ title, description, ogTitle, ogDescription, ogType: 'article' })` (lignes 93-99) — à ÉTENDRE Auto-imports nuxt-schema-org disponibles : `useSchemaOrg`, `defineArticle`, `defineBreadcrumb`. `resolveOgImage(article?: { image?: string } | null): string` — retourne URL absolue préfixée par `https://killiandalcin.fr`. Task 1: Créer app/utils/resolve-og-image.ts + déposer public/og-blog-default.jpg app/utils/resolve-og-image.ts, public/og-blog-default.jpg - app/utils/countWords.ts (pattern JSDoc + export nommé) - .planning/phases/07-seo-blog/07-RESEARCH.md §Pattern 4 (resolveOgImage helper) - .planning/phases/07-seo-blog/07-PATTERNS.md §resolve-og-image.ts 1. Créer `app/utils/resolve-og-image.ts` avec contenu exact : ```ts /** * Resolves an article's og:image to an absolute URL. * Strategy (D-05): frontmatter `image` if present, else branded fallback `/og-blog-default.jpg`. * Consumed by: app/pages/blog/[slug].vue (useSeoMeta.ogImage + defineArticle.image) * app/pages/blog/index.vue (useSeoMeta.ogImage fallback only). */ const SITE_URL = 'https://killiandalcin.fr' const FALLBACK = '/og-blog-default.jpg' export function resolveOgImage(article?: { image?: string } | null): string { const raw = article?.image?.trim() || FALLBACK if (raw.startsWith('http://') || raw.startsWith('https://')) return raw return `${SITE_URL}${raw.startsWith('/') ? raw : `/${raw}`}` } ``` 2. Déposer un asset `public/og-blog-default.jpg` (1200×630). Placeholder acceptable (RESEARCH Open Question #2) : générer un JPG simple via ImageMagick (si disponible) ou utiliser un existant cropé. Commande minimale si `magick` disponible : ```sh magick -size 1200x630 gradient:'#0f172a'-'#1e293b' -gravity center -fill white -pointsize 64 -annotate 0 "Blog · killiandalcin.fr" public/og-blog-default.jpg ``` Si `magick` absent, copier `public/og-image.png` en `public/og-blog-default.jpg` via `cp public/og-image.png public/og-blog-default.jpg` COMME DERNIER RECOURS et noter dans le SUMMARY qu'un design définitif reste à produire (checkpoint design report en backlog). L'important est que le fichier existe et soit servable. test -f app/utils/resolve-og-image.ts && grep -q "export function resolveOgImage" app/utils/resolve-og-image.ts && test -f public/og-blog-default.jpg && pnpm typecheck Helper exporté type-check OK, asset JPG servable à `/og-blog-default.jpg`. Task 2: Enrichir app/pages/blog/[slug].vue — useSeoMeta D-15 + useSchemaOrg defineArticle + defineBreadcrumb app/pages/blog/[slug].vue - app/pages/blog/[slug].vue (fichier entier 1-157) - .planning/phases/07-seo-blog/07-RESEARCH.md §Pattern 2 (Article Page JSON-LD + Meta), §useSeoMeta Enrichment table - .planning/phases/07-seo-blog/07-PATTERNS.md §[slug].vue (modify) - .planning/phases/07-seo-blog/07-CONTEXT.md D-13, D-15 Dans `app/pages/blog/[slug].vue`, zone `