---
phase: 06-blog-pages
plan: 02
type: execute
wave: 2
depends_on: []
files_modified:
  - i18n/locales/fr.json
  - i18n/locales/en.json
  - app/components/layout/AppHeader.vue
  - app/components/BlogCard.vue
autonomous: true
requirements:
  - BLOG-02
  - BLOG-03
  - BLOG-06
tags:
  - blog
  - i18n
  - nav
  - blog-card
  - shared-components

must_haves:
  truths:
    - "Les clés i18n `nav.blog`, `blog.title`, `blog.subtitle`, `blog.stats.*`, `blog.readingTime`, `blog.prevArticle`, `blog.nextArticle`, `blog.backToBlog`, `blog.toc.title`, `blog.emptyState.*`, `blog.breadcrumb.*`, `a11y.blogTocToggle`, `a11y.blogPrev`, `a11y.blogNext` existent dans fr.json ET en.json avec des valeurs traduites"
    - "AppHeader.vue affiche un lien `Blog` entre Hytale et Projects dans la nav desktop ET mobile"
    - "BlogCard.vue est un composant unique avec variant prop `default` (listing) et `compact` (prev/next), importable partout via auto-import"
    - "BlogCard variant default rend : cover image conditionnelle (si article.image) aspect-16/9 + titre + description line-clamp-2 + date formatée i18n + premier tag UBadge + reading time"
    - "BlogCard variant compact rend : pas d'image + label row 'Article précédent/suivant' + icône arrow + titre + date, utilisé exclusivement par BlogPrevNext en Wave 3"
  artifacts:
    - path: "i18n/locales/fr.json"
      provides: "Clés blog.* + nav.blog + a11y.blog* en français"
      contains: "\"blog\":"
    - path: "i18n/locales/en.json"
      provides: "Clés blog.* + nav.blog + a11y.blog* en anglais"
      contains: "\"blog\":"
    - path: "app/components/layout/AppHeader.vue"
      provides: "Nav link Blog entre hytale et projects (desktop + mobile)"
      contains: "{ key: 'blog', path: '/blog' }"
    - path: "app/components/BlogCard.vue"
      provides: "Composant unifié variant default + compact pour listing et prev/next"
      exports_default: true
  key_links:
    - from: "app/components/BlogCard.vue"
      to: "i18n blog.readingTime / blog.prevArticle / blog.nextArticle"
      via: "t('blog.readingTime', { minutes }) dans le template"
      pattern: "t\\('blog\\.(readingTime|prevArticle|nextArticle)'"
    - from: "app/components/layout/AppHeader.vue"
      to: "i18n nav.blog"
      via: "t(`nav.${link.key}`) avec key 'blog' ajoutée dans navLinks"
      pattern: "key: 'blog'"
    - from: "app/components/BlogCard.vue"
      to: "NuxtLink localePath('/blog/' + slug)"
      via: "absolute inset-0 SEO link pattern de ProjectCard"
      pattern: "localePath"
---

<objective>
Poser les **3 pré-requis transverses** consommés par les deux pages blog (Wave 3) :
1. Les clés i18n dans fr.json + en.json (sans elles, tout template de Wave 3 rendera des `{{ $t(...) }}` vides)
2. Le lien nav `Blog` dans AppHeader (sans lui, la nav ne mène pas au blog — rupture de découvrabilité D-15)
3. Le composant `BlogCard.vue` unifié (sans lui, ni le listing ni la section prev/next ne peuvent rendre quoi que ce soit — D-20 exige composant unique avec variant)

**Purpose:** Les 3 tâches de ce plan sont indépendantes les unes des autres (fichiers disjoints) mais nécessaires ENSEMBLE avant que Wave 3 (pages) puisse être exécutée. Elles forment la "couche composition partagée".

**Output:**
- `i18n/locales/fr.json` + `en.json` : bloc `blog.*` complet + `nav.blog` + 3 clés `a11y.blog*`
- `app/components/layout/AppHeader.vue` : entrée `{ key: 'blog', path: '/blog' }` ajoutée dans `navLinks` entre hytale et projects
- `app/components/BlogCard.vue` : composant variant default + compact, auto-importé par Nuxt
</objective>

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

<context>
@.planning/STATE.md
@.planning/phases/06-blog-pages/06-CONTEXT.md
@.planning/phases/06-blog-pages/06-RESEARCH.md
@.planning/phases/06-blog-pages/06-PATTERNS.md
@.planning/phases/06-blog-pages/06-UI-SPEC.md
@app/components/ProjectCard.vue
@app/components/layout/AppHeader.vue
@i18n/locales/fr.json
@i18n/locales/en.json

<interfaces>
<!-- Shape article depuis queryCollection('blog_fr') avec schema étendu Wave 1 -->
```typescript
interface BlogArticle {
  path: string          // ex: '/fr/blog/my-slug'
  title: string
  description: string
  date: string
  tags?: string[]
  image?: string
  draft?: boolean       // ajouté Wave 1
  wordCount?: number    // ajouté Wave 1 (via hook)
  minutes?: number      // ajouté Wave 1 (via hook)
}
```

<!-- Props BlogCard (contrat D-20) -->
```typescript
interface BlogCardProps {
  article: BlogArticle  // ou un sous-ensemble pour variant compact (fields prop de surround())
  variant?: 'default' | 'compact'
  direction?: 'prev' | 'next'  // requis seulement si variant='compact'
}
```

<!-- Pattern ProjectCard.vue existant (lignes 18-90) à transposer pour variant default -->
```
<article class="group relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm overflow-hidden transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1.5">
  <!-- Cover image + padding p-5 sm:p-6 + tag badge + date + title + description -->
  <!-- NuxtLink absolute inset-0 pour SEO + a11y -->
</article>
```

<!-- AppHeader.vue navLinks shape actuel (lignes 8-15) -->
```typescript
const navLinks = computed(() => [
  { key: 'home', path: '/' },
  { key: 'hytale', path: '/hytale' },
  { key: 'projects', path: '/projects' },
  { key: 'about', path: '/about' },
  { key: 'contact', path: '/contact' },
  { key: 'fiverr', path: '/fiverr' },
])
```
Le template itère via `v-for="link in navLinks"` puis `{{ t(\`nav.${link.key}\`) }}` — ajouter une entrée propage automatiquement au desktop ET au mobile slideover.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 2.1 : Ajouter le bloc complet `blog.*` + `nav.blog` + `a11y.blog*` dans fr.json et en.json</name>
  <files>
    - i18n/locales/fr.json
    - i18n/locales/en.json
  </files>
  <read_first>
    - i18n/locales/fr.json (structure actuelle : "nav" en haut, "footer", "a11y", "seo", "projects" — pour insérer "blog" en suivant la convention de clés top-level du projet)
    - i18n/locales/en.json (mêmes clés, version EN)
    - .planning/phases/06-blog-pages/06-UI-SPEC.md (§Copywriting Contract lignes 115-172 pour les libellés FR/EN EXACTS + §i18n Keys à créer lignes 339-379 pour la structure JSON complète)
    - .planning/phases/06-blog-pages/06-CONTEXT.md (D-21)
    - .planning/phases/06-blog-pages/06-PATTERNS.md (§i18n/locales lignes 464-482 — convention "bloc projects utilise les accents, suivre ce pattern, pas a11y/seo qui sont sans accents")
  </read_first>
  <action>
**Pour `i18n/locales/fr.json` :**

1. Dans le bloc existant `"nav": { ... }` (lignes 2-9), ajouter une nouvelle clé `"blog"` avec la valeur `"Blog"`. Placer logiquement avant `"projects"` pour refléter l'ordre de navigation (hytale → blog → projects), mais l'ordre dans le JSON n'impacte pas le runtime — l'important est la présence de la clé.

2. Dans le bloc existant `"a11y": { ... }` (lignes 23-34), ajouter 3 nouvelles clés à la fin du bloc :
```json
"blogTocToggle": "Afficher le sommaire",
"blogPrev": "Article précédent : {title}",
"blogNext": "Article suivant : {title}"
```

3. Ajouter un NOUVEAU bloc top-level `"blog": { ... }` (à placer après le bloc `"projects"` pour cohérence thématique, ou à la fin du fichier — l'emplacement est au jugement de l'exécutant tant que le JSON reste valide) contenant :

```json
"blog": {
  "title": "Blog",
  "subtitle": "Articles techniques, retours d'expérience et guides pratiques sur le développement de plugins Hytale et l'écosystème web.",
  "stats": {
    "articles": "Articles",
    "tags": "Tags",
    "languages": "Langues"
  },
  "readingTime": "{minutes} min de lecture",
  "prevArticle": "Article précédent",
  "nextArticle": "Article suivant",
  "backToBlog": "Retour au blog",
  "toc": {
    "title": "Sommaire"
  },
  "emptyState": {
    "title": "Bientôt des articles Hytale",
    "description": "Le blog se prépare. Les premiers articles sur le développement de plugins Hytale arrivent bientôt.",
    "cta": "Me contacter"
  },
  "breadcrumb": {
    "home": "Accueil",
    "blog": "Blog"
  }
}
```

**Pour `i18n/locales/en.json` :**

Mêmes additions, traductions EN :

1. `nav.blog` = `"Blog"`

2. `a11y.blog*` :
```json
"blogTocToggle": "Show table of contents",
"blogPrev": "Previous article: {title}",
"blogNext": "Next article: {title}"
```

3. Bloc `blog` :
```json
"blog": {
  "title": "Blog",
  "subtitle": "Technical articles, experience feedback and practical guides on Hytale plugin development and the web ecosystem.",
  "stats": {
    "articles": "Articles",
    "tags": "Tags",
    "languages": "Languages"
  },
  "readingTime": "{minutes} min read",
  "prevArticle": "Previous article",
  "nextArticle": "Next article",
  "backToBlog": "Back to blog",
  "toc": {
    "title": "Table of contents"
  },
  "emptyState": {
    "title": "Hytale articles coming soon",
    "description": "The blog is being prepared. The first articles on Hytale plugin development are coming soon.",
    "cta": "Contact me"
  },
  "breadcrumb": {
    "home": "Home",
    "blog": "Blog"
  }
}
```

**Conventions à respecter :**
- **Accents** : FR utilise les accents dans le bloc `blog.*` (comme le bloc `projects` existant), PAS le pattern ASCII des blocs `a11y`/`seo`. Ex: "Bientôt" avec ô, "précédent" avec é, "sommaire" — accentués. Cohérent avec PATTERNS.md §convention.
- **Interpolation** : `{minutes}` et `{title}` sont la syntaxe vue-i18n standard (pas `{{ minutes }}`, pas `%{minutes}`). Cette syntaxe est déjà utilisée dans le projet (ex: à vérifier dans les blocs existants).
- **Valid JSON** : ne PAS laisser de virgule traînante après la dernière clé d'un bloc (JSON strict).
- **Ordre des blocs** : ne pas réorganiser les blocs existants (`nav`, `footer`, `a11y`, `seo`, `projects`, `home`, `about`, etc.) — uniquement ajouter.
  </action>
  <verify>
    <automated>node -e "const fr=require('./i18n/locales/fr.json'); const en=require('./i18n/locales/en.json'); console.log(fr.nav.blog, en.nav.blog, fr.blog.title, en.blog.title, fr.blog.readingTime, en.blog.readingTime, fr.a11y.blogTocToggle, en.a11y.blogTocToggle)"</automated>
  </verify>
  <acceptance_criteria>
    - `node -e "console.log(require('./i18n/locales/fr.json').nav.blog)"` affiche `Blog`
    - `node -e "console.log(require('./i18n/locales/en.json').nav.blog)"` affiche `Blog`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.title)"` affiche `Blog`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.subtitle)"` commence par `Articles techniques`
    - `node -e "console.log(require('./i18n/locales/en.json').blog.subtitle)"` commence par `Technical articles`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.readingTime)"` affiche `{minutes} min de lecture`
    - `node -e "console.log(require('./i18n/locales/en.json').blog.readingTime)"` affiche `{minutes} min read`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.emptyState.cta)"` affiche `Me contacter`
    - `node -e "console.log(require('./i18n/locales/en.json').blog.emptyState.cta)"` affiche `Contact me`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.toc.title)"` affiche `Sommaire`
    - `node -e "console.log(require('./i18n/locales/en.json').blog.toc.title)"` affiche `Table of contents`
    - `node -e "console.log(require('./i18n/locales/fr.json').a11y.blogTocToggle)"` affiche `Afficher le sommaire`
    - `node -e "console.log(require('./i18n/locales/fr.json').a11y.blogPrev)"` contient `{title}`
    - `node -e "console.log(require('./i18n/locales/fr.json').blog.breadcrumb.home)"` affiche `Accueil`
    - `node -e "console.log(require('./i18n/locales/en.json').blog.breadcrumb.home)"` affiche `Home`
    - `node -e "JSON.parse(require('fs').readFileSync('./i18n/locales/fr.json'))"` ne throw pas (JSON valide)
    - `node -e "JSON.parse(require('fs').readFileSync('./i18n/locales/en.json'))"` ne throw pas
    - Les clés existantes (`nav.home`, `nav.hytale`, `seo.*`, `projects.*`) sont inchangées — vérifier par `node -e "console.log(require('./i18n/locales/fr.json').nav.hytale)"` = `Hytale`
  </acceptance_criteria>
  <done>
Les deux fichiers i18n contiennent `nav.blog`, 3 clés `a11y.blog*`, et un bloc `blog.*` complet avec les 14 clés listées (title, subtitle, stats.articles/tags/languages, readingTime, prevArticle, nextArticle, backToBlog, toc.title, emptyState.title/description/cta, breadcrumb.home/blog). JSON valide. Aucune clé existante modifiée.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2.2 : Ajouter le lien Blog dans AppHeader.vue navLinks (entre hytale et projects)</name>
  <files>app/components/layout/AppHeader.vue</files>
  <read_first>
    - app/components/layout/AppHeader.vue (état actuel : navLinks lignes 8-15, template desktop lignes 44-55, slideover mobile lignes 89-100 — le template itère via v-for donc UN SEUL changement suffit)
    - .planning/phases/06-blog-pages/06-CONTEXT.md (D-15 : ordre final Home / Hytale / **Blog** / Projects / About / Contact / Fiverr)
    - .planning/phases/06-blog-pages/06-PATTERNS.md (§AppHeader lignes 431-460)
  </read_first>
  <action>
Dans `app/components/layout/AppHeader.vue`, modifier UNIQUEMENT l'array `navLinks` computed (lignes 8-15). Insérer `{ key: 'blog', path: '/blog' }` entre l'entrée `hytale` et l'entrée `projects`.

Avant :
```typescript
const navLinks = computed(() => [
  { key: 'home', path: '/' },
  { key: 'hytale', path: '/hytale' },
  { key: 'projects', path: '/projects' },
  { key: 'about', path: '/about' },
  { key: 'contact', path: '/contact' },
  { key: 'fiverr', path: '/fiverr' },
])
```

Après :
```typescript
const navLinks = computed(() => [
  { key: 'home', path: '/' },
  { key: 'hytale', path: '/hytale' },
  { key: 'blog', path: '/blog' },
  { key: 'projects', path: '/projects' },
  { key: 'about', path: '/about' },
  { key: 'contact', path: '/contact' },
  { key: 'fiverr', path: '/fiverr' },
])
```

**Ne toucher à RIEN d'autre** dans le fichier :
- Pas de modification du template (le `v-for="link in navLinks"` prend l'array updated automatiquement)
- Pas de modification du slideover mobile (même v-for sur la même source)
- Pas de modification des imports, des refs, des fonctions `isActive`/`toggleLocale`/`toggleTheme`
- Ne pas ajouter un bloc blog dédié — passer par le pattern itératif existant est intentionnel (cohérence visuelle + moins de code)

**Pourquoi `path: '/blog'` (pas `/fr/blog`) :** le template wrap `localePath(link.path)` dans le NuxtLink `:to` (ligne 46 et 91). `localePath('/blog')` résout automatiquement vers `/fr/blog` ou `/en/blog` selon la locale active — pattern i18n existant respecté.

**Pourquoi la clé `'blog'` :** le template interpole `{{ t(\`nav.${link.key}\`) }}` — la clé `nav.blog` ajoutée par Task 2.1 sera automatiquement utilisée, pas de hardcode.
  </action>
  <verify>
    <automated>grep -c "{ key: 'blog', path: '/blog' }" app/components/layout/AppHeader.vue</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "{ key: 'blog', path: '/blog' }" app/components/layout/AppHeader.vue` retourne 1
    - `grep -n "key: 'hytale'" app/components/layout/AppHeader.vue` retourne une ligne (ex: ligne 10)
    - `grep -n "key: 'blog'" app/components/layout/AppHeader.vue` retourne une ligne (ex: ligne 11)
    - `grep -n "key: 'projects'" app/components/layout/AppHeader.vue` retourne une ligne (ex: ligne 12)
    - Le numéro de ligne de `key: 'blog'` est STRICTEMENT entre celui de `key: 'hytale'` et `key: 'projects'` (ordre respecté D-15)
    - `grep -c "key: 'home'" app/components/layout/AppHeader.vue` retourne 1 (pas de duplication/suppression)
    - `grep -c "key: 'fiverr'" app/components/layout/AppHeader.vue` retourne 1
    - `grep -c "v-for=\"link in navLinks\"" app/components/layout/AppHeader.vue` retourne 2 (desktop + mobile templates intacts)
    - `pnpm typecheck` passe
    - `pnpm dev` + visite manuelle de `/fr/` montre un lien `Blog` entre `Hytale` et `Projets` dans la nav desktop (validation visuelle optionnelle, non-bloquante)
  </acceptance_criteria>
  <done>
AppHeader.vue contient `{ key: 'blog', path: '/blog' }` dans navLinks, positionné entre hytale et projects. Aucune autre modification. Template v-for inchangé, le nouveau lien apparaît automatiquement en desktop et dans le slideover mobile. Le libellé `Blog` vient de `nav.blog` (Task 2.1).
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2.3 : Créer app/components/BlogCard.vue (variants default + compact, D-20)</name>
  <files>app/components/BlogCard.vue</files>
  <read_first>
    - app/components/ProjectCard.vue (pattern COMPLET à transposer pour variant default : lignes 18-90 — article wrapper, NuxtImg cover, content section, NuxtLink absolute inset-0)
    - .planning/phases/06-blog-pages/06-UI-SPEC.md (§BlogCard variant contract lignes 213-230 pour le layout EXACT des deux variants + §Typography + §Color pour les classes)
    - .planning/phases/06-blog-pages/06-PATTERNS.md (§BlogCard.vue lignes 152-252 — adaptation vs ProjectCard détaillée)
    - .planning/phases/06-blog-pages/06-RESEARCH.md (§Pattern 6 lignes 520-556 pour la structure TypeScript + § Pitfall 6 lignes 625-629 pour le a11y SEO link)
    - .planning/phases/06-blog-pages/06-CONTEXT.md (D-02 : tags non-cliquables ; D-03 : pas de fallback image ; D-10 : pas d'image en variant compact)
    - i18n/locales/fr.json (après Task 2.1 — confirmer que blog.readingTime / prevArticle / nextArticle sont bien présents)
  </read_first>
  <action>
Créer `app/components/BlogCard.vue` avec `<script setup lang="ts">`, props typées, date formattée via `Intl.DateTimeFormat`, deux templates (variant default / compact) branchés par `v-if`. Le composant est auto-importé par Nuxt (convention `app/components/*.vue`).

**Script setup complet :**

```vue
<script setup lang="ts">
interface BlogArticle {
  path: string
  title: string
  description?: string
  date: string
  tags?: string[]
  image?: string
  minutes?: number
}

interface Props {
  article: BlogArticle
  variant?: 'default' | 'compact'
  direction?: 'prev' | 'next' // uniquement si variant='compact'
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'default',
  direction: 'next',
})

const { t, locale } = useI18n()
const localePath = useLocalePath()

// Slug extrait du path pour construire l'URL locale-agnostique
// path = '/fr/blog/my-slug' ou '/en/blog/my-slug' → slug = 'my-slug'
const slug = computed(() => {
  const parts = props.article.path.split('/').filter(Boolean)
  return parts[parts.length - 1] ?? ''
})

const formattedDate = computed(() => {
  try {
    return new Intl.DateTimeFormat(locale.value === 'fr' ? 'fr-FR' : 'en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    }).format(new Date(props.article.date))
  } catch {
    return props.article.date
  }
})

// Reading time avec fallback composable si minutes non injecté (ex: dev hot-reload)
const readingMinutes = computed(() => {
  if (typeof props.article.minutes === 'number') return props.article.minutes
  return useReadingTime(props.article.description ?? '')
})

const directionIcon = computed(() =>
  props.direction === 'prev' ? 'i-lucide-arrow-left' : 'i-lucide-arrow-right'
)

const directionLabel = computed(() =>
  props.direction === 'prev' ? t('blog.prevArticle') : t('blog.nextArticle')
)
</script>
```

**Template — variant default (listing) :**

Transposition directe du pattern ProjectCard.vue. Différences :
- `NuxtLink` utilise `localePath('/blog/' + slug)` (pas `/project/${id}`)
- `aspect-[16/9]` sur l'image (pas `h-52`)
- `<h2>` (pas `<h3>`) pour le titre — c'est un listing d'articles (hierarchie SEO)
- Description `line-clamp-2` (pas `line-clamp-3`)
- Footer row : reading time + tags supplémentaires (+N) à la place des technologies
- Schema.org `BlogPosting` (pas `CreativeWork`)
- **Cover image conditionnelle** : uniquement si `article.image` présent (D-03 pas de fallback)

```vue
<template>
  <article
    v-if="variant === 'default'"
    class="group relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm overflow-hidden transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1.5"
    itemscope
    itemtype="https://schema.org/BlogPosting"
  >
    <!-- Cover image (D-03 : aucun fallback si absent) -->
    <NuxtLink
      v-if="article.image"
      :to="localePath(`/blog/${slug}`)"
      class="block relative overflow-hidden"
    >
      <NuxtImg
        :src="article.image"
        :alt="article.title"
        loading="lazy"
        format="webp"
        width="400"
        height="225"
        class="w-full aspect-[16/9] object-cover transition-transform duration-500 group-hover:scale-105"
        itemprop="image"
      />
    </NuxtLink>

    <!-- Content -->
    <div class="p-5 sm:p-6 flex flex-col gap-3">
      <!-- Tag + Date -->
      <div class="flex items-center justify-between">
        <UBadge v-if="article.tags?.[0]" color="primary" variant="subtle" itemprop="keywords">
          {{ article.tags[0] }}
        </UBadge>
        <time
          class="text-xs text-gray-400 dark:text-gray-500 font-mono"
          :datetime="article.date"
          itemprop="datePublished"
        >
          {{ formattedDate }}
        </time>
      </div>

      <!-- Title -->
      <h2
        class="text-lg font-bold text-gray-900 dark:text-white group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors"
        itemprop="headline"
      >
        {{ article.title }}
      </h2>

      <!-- Description -->
      <p
        v-if="article.description"
        class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed"
        itemprop="description"
      >
        {{ article.description }}
      </p>

      <!-- Footer: reading time + extra tags -->
      <div class="flex items-center justify-between pt-2">
        <span class="text-xs text-gray-400 dark:text-gray-500 font-medium inline-flex items-center gap-1.5">
          <UIcon name="i-lucide-clock" class="w-3.5 h-3.5" />
          {{ t('blog.readingTime', { minutes: readingMinutes }) }}
        </span>
        <div v-if="article.tags && article.tags.length > 1" class="flex gap-1.5">
          <span
            v-for="tag in article.tags.slice(1, 3)"
            :key="tag"
            class="text-[11px] px-2.5 py-1 rounded-full bg-gray-100 dark:bg-gray-800/80 text-gray-600 dark:text-gray-400 font-medium border border-gray-200/50 dark:border-gray-700/30"
          >
            {{ tag }}
          </span>
          <span
            v-if="article.tags.length > 3"
            class="text-[11px] px-2.5 py-1 rounded-full bg-gray-100 dark:bg-gray-800/80 text-gray-500 font-medium border border-gray-200/50 dark:border-gray-700/30"
          >
            +{{ article.tags.length - 3 }}
          </span>
        </div>
      </div>
    </div>

    <!-- SEO + a11y: full-card clickable link (D-02 tags non-cliquables → safe per Pitfall 6) -->
    <NuxtLink
      :to="localePath(`/blog/${slug}`)"
      class="absolute inset-0 z-10"
      :aria-label="`${article.title} - ${formattedDate}`"
      itemprop="url"
    />
  </article>

  <!-- Variant compact (prev/next) — D-10 pas d'image, D-09 label row + icon -->
  <article
    v-else
    class="group relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-5 transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1.5 flex flex-col gap-2"
    :class="direction === 'next' ? 'items-end text-right' : 'items-start text-left'"
  >
    <div class="inline-flex items-center gap-2 text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400 font-medium">
      <UIcon
        v-if="direction === 'prev'"
        :name="directionIcon"
        class="w-4 h-4 transition-transform duration-200 group-hover:-translate-x-1 group-hover:text-brand-500"
      />
      <span>{{ directionLabel }}</span>
      <UIcon
        v-if="direction === 'next'"
        :name="directionIcon"
        class="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1 group-hover:text-brand-500"
      />
    </div>
    <h3 class="text-base font-bold text-gray-900 dark:text-white group-hover:text-brand-500 dark:group-hover:text-brand-400 transition-colors">
      {{ article.title }}
    </h3>
    <time class="text-xs font-mono text-gray-400 dark:text-gray-500" :datetime="article.date">
      {{ formattedDate }}
    </time>
    <NuxtLink
      :to="localePath(`/blog/${slug}`)"
      class="absolute inset-0 z-10"
      :aria-label="t(direction === 'prev' ? 'a11y.blogPrev' : 'a11y.blogNext', { title: article.title })"
    />
  </article>
</template>
```

**Décisions de conception documentées :**
- `slug` calculé depuis `article.path` : les articles @nuxt/content ont un `path` de forme `/fr/blog/my-slug` → extraire le dernier segment. Évite de réclamer un champ `slug` explicite dans le frontmatter.
- Variant compact sans image (D-10 + cohérent D-03 : pas de fallback image).
- `absolute inset-0` SEO link pattern OK tant que tags restent non-cliquables (Pitfall 6 + D-02 respectés).
- Schema.org : `BlogPosting` + `datePublished` + `headline` + `description` + `keywords` + `url` + `image` (prépare Phase 7 JSON-LD Article sans effort supplémentaire — tout est déjà structuré).
- `text-right` sur variant=next, `text-left` sur prev : UX directionnelle (la flèche et le texte suivent la direction du clic).
  </action>
  <verify>
    <automated>test -f app/components/BlogCard.vue && grep -c "variant === 'default'" app/components/BlogCard.vue</automated>
  </verify>
  <acceptance_criteria>
    - `test -f app/components/BlogCard.vue` retourne 0
    - `grep -c "variant === 'default'" app/components/BlogCard.vue` retourne 1 (template branche)
    - `grep -c "variant?: 'default' | 'compact'" app/components/BlogCard.vue` retourne 1 (type union exact)
    - `grep -c "direction?: 'prev' | 'next'" app/components/BlogCard.vue` retourne 1
    - `grep -c "withDefaults(defineProps<Props>()" app/components/BlogCard.vue` retourne 1
    - `grep -c "Intl.DateTimeFormat" app/components/BlogCard.vue` retourne 1
    - `grep -c "t('blog.readingTime'" app/components/BlogCard.vue` retourne 1
    - `grep "localePath(\`/blog/\${slug}\`)" app/components/BlogCard.vue` retourne 2+ matches (au moins 2 NuxtLink)
    - `grep "useReadingTime" app/components/BlogCard.vue` retourne 1+ match (fallback utilisé)
    - `grep "i-lucide-arrow-left" app/components/BlogCard.vue` retourne 1 match (icône prev)
    - `grep "i-lucide-arrow-right" app/components/BlogCard.vue` retourne 1 match (icône next)
    - `grep "BlogPosting" app/components/BlogCard.vue` retourne 1 match (Schema.org)
    - `grep "aspect-\\[16/9\\]" app/components/BlogCard.vue` retourne 1 match (ratio cover listing)
    - `grep -c "a11y.blogPrev" app/components/BlogCard.vue` retourne 1 (label a11y interpolé)
    - `grep -c "a11y.blogNext" app/components/BlogCard.vue` retourne 1
    - `pnpm typecheck` passe sans erreur TS
    - `pnpm lint` passe sans nouvelle erreur ESLint sur BlogCard.vue
  </acceptance_criteria>
  <done>
BlogCard.vue créé avec script setup TS, 2 variants (default + compact), date i18n via Intl.DateTimeFormat, reading time avec fallback `useReadingTime`, NuxtLink absolute inset-0 pour SEO/a11y (tags non-cliquables D-02 respecté), icônes arrow directionnelles avec translate hover. Schema.org BlogPosting markup. Auto-importé par Nuxt. Typecheck + lint verts.
  </done>
</task>

</tasks>

<verification>
1. `pnpm typecheck` passe
2. `pnpm lint` passe (pas de nouvelle erreur)
3. `pnpm dev` démarre sans erreur — le lien `Blog` apparaît dans la nav desktop après Hytale, avant Projets
4. Clic sur le lien `Blog` va vers `/fr/blog` (404 attendu à ce stade — la page sera créée Wave 3)
5. Validation JSON : `node -e "JSON.parse(require('fs').readFileSync('./i18n/locales/fr.json')); JSON.parse(require('fs').readFileSync('./i18n/locales/en.json')); console.log('valid')"`
6. Le composant BlogCard n'est consommé nulle part à ce stade — c'est normal, il sera utilisé par les pages de Wave 3.
</verification>

<success_criteria>
- fr.json et en.json contiennent tous les blocs blog.*, nav.blog, a11y.blog* (14+ clés ajoutées par locale)
- AppHeader.vue a `{ key: 'blog', path: '/blog' }` à la bonne position dans navLinks (entre hytale et projects)
- BlogCard.vue existe, typecheck vert, supporte variant default et compact
- Aucune régression sur les clés i18n existantes ni sur la nav existante
</success_criteria>

<output>
After completion, create `.planning/phases/06-blog-pages/06-02-SUMMARY.md` with:
- Diff i18n (nombre de clés ajoutées FR + EN)
- Position exacte du lien blog dans navLinks (ligne du fichier)
- Décisions de conception BlogCard (aspects intéressants : slug derivation, direction icons, a11y label template)
- Any deviation (ex: convention accents, ordre des blocs JSON)
</output>
