Files

12 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-seo-blog 02 execute 2
07-01
app/utils/resolve-og-image.ts
public/og-blog-default.jpg
app/pages/blog/[slug].vue
true
SEO-10
SEO-11
SEO-13
SEO-15
truths artifacts key_links
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
path provides contains
app/utils/resolve-og-image.ts resolveOgImage(article) → URL absolue export function resolveOgImage
path provides
public/og-blog-default.jpg fallback branded 1200x630
path provides contains
app/pages/blog/[slug].vue useSeoMeta enrichi + useSchemaOrg([defineArticle, defineBreadcrumb]) defineArticle
from to via pattern
app/pages/blog/[slug].vue app/utils/resolve-og-image.ts import resolveOgImage resolveOgImage
from to via pattern
app/pages/blog/[slug].vue (defineArticle.author) app/app.vue (definePerson global) @id reference '@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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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 `<script setup lang="ts">` uniquement (ne PAS toucher au template) :
1. **Imports** — ajouter au tout début du script (après la ligne 1 `<script setup lang="ts">`) :
   ```ts
   import { KILLIAN_PERSON_ID } from '~/utils/seo-person'
   import { resolveOgImage } from '~/utils/resolve-og-image'
   ```

2. **Détection pair bilingue** — après le bloc `surround` (après ligne 39), avant `interface SurroundArticle` :
   ```ts
   // Détecter la version dans l'autre langue (pour og:locale:alternate, D-15, Pitfall 7)
   const { data: altExists } = await useAsyncData(
     `blog-alt-${locale.value}-${slug}`,
     () =>
       isFr.value
         ? queryCollection('blog_en').path(`/en/blog/${slug}`).first()
         : queryCollection('blog_fr').path(`/fr/blog/${slug}`).first(),
     { watch: [locale] },
   )
   ```

3. **Computeds SEO** — après `readingMinutes` computed (ligne 79), AVANT `interface TocLink` :
   ```ts
   const SITE_URL = 'https://killiandalcin.fr'
   const ogImage = computed(() => resolveOgImage(page.value as { image?: string } | null))
   const canonicalUrl = computed(() => `${SITE_URL}${localePath('/blog/' + slug)}`)
   const publishedIso = computed(() => page.value?.date)
   const modifiedIso = computed(() => page.value?.updated ?? page.value?.date)  // D-13
   ```

4. **Remplacer** le `useSeoMeta({...})` existant (lignes 93-99) par la version enrichie D-15 (arrow-fns pour tout ce qui lit `.value` — Pattern "Reactive arrow-fn values") :
   ```ts
   useSeoMeta({
     title: () => page.value?.title,
     description: () => page.value?.description,
     ogTitle: () => page.value?.title,
     ogDescription: () => page.value?.description,
     ogType: 'article',
     ogImage,
     ogUrl: canonicalUrl,
     ogLocale: () => (isFr.value ? 'fr_FR' : 'en_US'),
     ogLocaleAlternate: () => (altExists.value ? [isFr.value ? 'en_US' : 'fr_FR'] : []),
     twitterCard: 'summary_large_image',
     twitterImage: ogImage,
     articlePublishedTime: publishedIso,
     articleModifiedTime: modifiedIso,
     articleAuthor: () => "Killian' Dal-Cin",
   })
   ```

5. **Ajouter** après `useSeoMeta(...)` un bloc `useSchemaOrg` :
   ```ts
   useSchemaOrg([
     defineArticle({
       headline: () => page.value?.title,
       description: () => page.value?.description,
       image: ogImage,
       datePublished: publishedIso,
       dateModified: modifiedIso,
       inLanguage: () => (isFr.value ? 'fr-FR' : 'en-US'),
       author: { '@id': KILLIAN_PERSON_ID },
       publisher: { '@id': KILLIAN_PERSON_ID },
       mainEntityOfPage: canonicalUrl,
     }),
     defineBreadcrumb({
       itemListElement: [
         { name: () => t('blog.breadcrumb.home'), item: () => localePath('/') },
         { name: () => t('blog.breadcrumb.blog'), item: () => localePath('/blog') },
         { name: () => page.value?.title ?? '' },
       ],
     }),
   ])
   ```
   Ne PAS toucher aux computeds `breadcrumbItems`, `formattedDate`, `readingMinutes`, `tocLinks`, ni au template.
grep -q "defineArticle" app/pages/blog/[slug].vue && grep -q "defineBreadcrumb" app/pages/blog/[slug].vue && grep -q "articlePublishedTime" app/pages/blog/[slug].vue && grep -q "resolveOgImage" app/pages/blog/[slug].vue && grep -q "KILLIAN_PERSON_ID" app/pages/blog/[slug].vue && pnpm typecheck && pnpm dev --port 3000 & sleep 12 && SLUG=$(ls content/fr/blog | head -1 | sed 's/\.md$//') && curl -s "http://localhost:3000/fr/blog/$SLUG" | tee /tmp/slug.html | grep -q 'property="og:image".*https://killiandalcin.fr' && grep -q '"@type":"Article"' /tmp/slug.html && grep -q '"@type":"BreadcrumbList"' /tmp/slug.html && grep -q 'property="article:published_time"' /tmp/slug.html && kill %1 curl /fr/blog/{slug} HTML contient : og:image absolu, article:published_time, JSON-LD Article (avec author @id=#killian), JSON-LD BreadcrumbList 3 items. typecheck vert.

<threat_model>

Trust Boundaries

Boundary Description
frontmatter → HTML image: du markdown injecté dans meta tags / JSON-LD (auteur = soi-même, confiance élevée)

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-07-03 Tampering resolveOgImage (URL depuis frontmatter) mitigate Helper construit URL en préfixant SITE_URL ; frontmatter écrit par l'auteur unique (pas de user input externe)
T-07-04 Information Disclosure JSON-LD article (author) accept Identité Killian publique par design
</threat_model>
- Helper vérifiable : `grep "export function resolveOgImage" app/utils/resolve-og-image.ts` - og:image absolu : `curl /fr/blog/{slug} | grep 'property="og:image"' | grep 'https://'` - JSON-LD Article : `curl /fr/blog/{slug} | grep '"@type":"Article"'` - JSON-LD BreadcrumbList : `curl /fr/blog/{slug} | grep '"@type":"BreadcrumbList"'` - article:published_time : `curl /fr/blog/{slug} | grep 'property="article:published_time"'` - Pas de client-only : tout doit être dans le HTML initial SSR (pas de diff après hydratation)

<success_criteria>

  1. SEO-10 : curl /fr/blog/{slug} contient og:title, og:description, og:image uniques (dépendent de page.title/description/image)
  2. SEO-11 : JSON-LD Article valide avec author, datePublished, dateModified, headline
  3. SEO-13 : og:image = frontmatter absolutisé OR https://killiandalcin.fr/og-blog-default.jpg, jamais og-image.png
  4. SEO-15 : JSON-LD BreadcrumbList Accueil → Blog → {title} </success_criteria>
Après complétion, créer `.planning/phases/07-seo-blog/07-02-SUMMARY.md`.