---
phase: 08-content-cocon-semantique
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - app/components/HytaleRecentArticles.vue
  - app/pages/hytale.vue
  - i18n/locales/fr.json
  - i18n/locales/en.json
autonomous: true
requirements: [SEO-14]
tags: [nuxt-content, i18n, hytale, cocon-semantique]

must_haves:
  truths:
    - "Visiter /hytale affiche une section 'Articles récents' (uniquement si ≥1 article tagué hytale avec draft:false existe en base content)"
    - "La section réutilise BlogCard variant compact en grille 2 colonnes desktop / 1 colonne mobile"
    - "Switch FR/EN met à jour la section (useAsyncData key inclut la locale + watch)"
    - "Si 0 article hytale publié, la section est entièrement masquée (pas d'empty state)"
    - "Un lien 'Voir tous les articles' pointe vers /blog (FR) ou /en/blog (EN) via localePath"
  artifacts:
    - path: "app/components/HytaleRecentArticles.vue"
      provides: "Section composant auto-importé, queryCollection branches littérales + filtre tag hytale + limit 2"
      contains: "queryCollection('blog_fr')"
    - path: "app/pages/hytale.vue"
      provides: "Insertion <HytaleRecentArticles /> avant fermeture du root div"
      contains: "<HytaleRecentArticles"
    - path: "i18n/locales/fr.json"
      provides: "Clés hytale.recentArticles.title/subtitle/viewAll en FR accentué"
      contains: "recentArticles"
    - path: "i18n/locales/en.json"
      provides: "Clés hytale.recentArticles.title/subtitle/viewAll en EN"
      contains: "recentArticles"
  key_links:
    - from: "app/components/HytaleRecentArticles.vue"
      to: "BlogCard.vue (variant compact)"
      via: "auto-import Nuxt + props article+variant"
      pattern: "variant=\"compact\""
    - from: "app/components/HytaleRecentArticles.vue"
      to: "@nuxt/content collections blog_fr / blog_en"
      via: "queryCollection(literal).where('tags', ...).limit(2)"
      pattern: "queryCollection\\('blog_(fr|en)'\\)"
    - from: "app/pages/hytale.vue"
      to: "app/components/HytaleRecentArticles.vue"
      via: "auto-import + template insertion"
      pattern: "<HytaleRecentArticles"
---

<objective>
Scaffolder l'infrastructure technique du cocon sémantique côté /hytale : composant `HytaleRecentArticles.vue` (queryCollection bilingue, filtre tag=hytale, limit 2, masqué si vide), injection dans `app/pages/hytale.vue`, et clés i18n associées en FR/EN.

Purpose: Préparer le conteneur qui affichera les 2 articles seed publiés en Wave 2. Le composant doit dégrader gracieusement (v-if=length) tant que les articles ne sont pas encore publiés, ce qui permet de shipper cette Wave 1 sans casser /hytale.

Output: 1 nouveau composant + 1 page modifiée + 2 fichiers i18n mis à jour. Aucun article créé à ce stade.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-content-cocon-semantique/08-CONTEXT.md
@.planning/phases/08-content-cocon-semantique/08-PATTERNS.md
@app/pages/blog/index.vue
@app/components/BlogCard.vue
@app/pages/hytale.vue

<interfaces>
<!-- BlogCard.vue props (from app/components/BlogCard.vue lines 2-21) -->
```typescript
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' // default 'next'
}
```

<!-- queryCollection bilingual pattern (from app/pages/blog/index.vue lines 1-21) -->
```typescript
const { t, locale } = useI18n()
const localePath = useLocalePath()
const isFr = computed(() => locale.value === 'fr')

const { data: articles } = await useAsyncData(
  `hytale-recent-${locale.value}`,
  () =>
    isFr.value
      ? queryCollection('blog_fr')
          .where('draft', '=', false)
          .order('date', 'DESC')
          .all()
      : queryCollection('blog_en')
          .where('draft', '=', false)
          .order('date', 'DESC')
          .all(),
  { watch: [locale] },
)
```

<!-- i18n insertion point: existing hytale.* block in i18n/locales/fr.json starts ~line 471 (per PATTERNS.md). Add recentArticles sibling to hero/services/pricing. -->
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Créer composant HytaleRecentArticles.vue (query + filtre tag + render)</name>
  <files>app/components/HytaleRecentArticles.vue</files>
  <read_first>
    - app/pages/blog/index.vue (pattern queryCollection bilingue branches littérales, lignes 1-21)
    - app/components/BlogCard.vue (variant compact, props interface lignes 2-21)
    - .planning/phases/08-content-cocon-semantique/08-PATTERNS.md §"HytaleRecentArticles.vue"
    - .planning/phases/08-content-cocon-semantique/08-CONTEXT.md §D-10, D-11, D-12, D-13
  </read_first>
  <action>
Créer `app/components/HytaleRecentArticles.vue` (~70-90 lignes).

**Script setup (TypeScript strict) :**
- `const { t, locale } = useI18n()` + `const localePath = useLocalePath()`
- `const isFr = computed(() => locale.value === 'fr')`
- `useAsyncData` avec key littérale interpolée `` `hytale-recent-${locale.value}` ``, ternaire `isFr.value` → `queryCollection('blog_fr')` / `queryCollection('blog_en')` (branches littérales obligatoires — Pitfall Phase 5 D-03, voir STATE.md gotcha).
- Chaîne de la query : `.where('draft', '=', false).order('date', 'DESC').all()` — **SANS `.limit(2)` au SQL ni `.where('tags', 'LIKE', ...)`** (l'opérateur LIKE sur champ JSON array SQLite n'est pas fiable selon D-11). À la place, **filtre JS post-query** :
  ```ts
  const articles = computed(() => {
    const all = data.value ?? []
    return all.filter((a) => Array.isArray(a.tags) && a.tags.includes('hytale')).slice(0, 2)
  })
  ```
  (renomme la destructuration `useAsyncData` en `{ data }` et expose `articles` computed — documente en commentaire `// Filtre JS car LIKE SQLite unreliable sur tags[] — D-11`).
- Option `{ watch: [locale] }` sur useAsyncData (re-fetch au switch langue).

**Template :**
- `<section v-if="articles.length" class="py-16 md:py-20 px-4 sm:px-6 lg:px-8">`
- Wrapper intérieur `max-w-7xl mx-auto` pour cohérence /blog.
- Header section : petit `<span>// recent-articles</span>` style mono brand + `<h2>{{ t('hytale.recentArticles.title') }}</h2>` (réutiliser tailwind styles de /blog hero h1, taille h2 plus sobre : `text-3xl sm:text-4xl font-bold`) + `<p>{{ t('hytale.recentArticles.subtitle') }}</p>` si clé présente.
- Grille : `<div class="grid grid-cols-1 md:grid-cols-2 gap-5 lg:gap-6 mt-8">` avec `<BlogCard v-for="article in articles" :key="article.path" :article="article" variant="compact" />` (pas de `direction` → default 'next' accepté, D-13 ne spécifie pas prev/next sémantique, acceptable).
- Footer : `<NuxtLink :to="localePath('/blog')" class="inline-flex items-center gap-2 mt-8 text-brand-500 hover:text-brand-600 font-medium">{{ t('hytale.recentArticles.viewAll') }} <UIcon name="i-lucide-arrow-right" /></NuxtLink>`.

**Règles strictes (D-09, D-10, D-11, D-12, D-13) :**
- BlogCard **auto-importé** — pas d'import explicite.
- Pas de fallback empty state (D-12 : masquer section complète).
- Pas d'usage de `queryCollection(variableName)` — littéraux uniquement.
  </action>
  <verify>
    <automated>pnpm typecheck</automated>
  </verify>
  <done>
    - Fichier `app/components/HytaleRecentArticles.vue` existe
    - `grep -E "queryCollection\\('blog_(fr|en)'\\)" app/components/HytaleRecentArticles.vue` retourne les 2 branches
    - `grep "v-if=\"articles.length\"" app/components/HytaleRecentArticles.vue` passe
    - `grep "variant=\"compact\"" app/components/HytaleRecentArticles.vue` passe
    - `grep "tags.*includes.*'hytale'" app/components/HytaleRecentArticles.vue` passe (filtre JS)
    - `pnpm typecheck` exit 0
  </done>
</task>

<task type="auto">
  <name>Task 2: Injecter HytaleRecentArticles dans app/pages/hytale.vue + ajouter clés i18n FR/EN</name>
  <files>app/pages/hytale.vue, i18n/locales/fr.json, i18n/locales/en.json</files>
  <read_first>
    - app/pages/hytale.vue (39 lignes, template section actuelle)
    - .planning/phases/08-content-cocon-semantique/08-PATTERNS.md §"app/pages/hytale.vue" + §"i18n/locales"
    - i18n/locales/fr.json (bloc hytale.* ~ligne 471, blog.* ~ligne 557 pour le style accentué 2026)
    - i18n/locales/en.json (bloc hytale.* miroir)
    - .planning/phases/08-content-cocon-semantique/08-CONTEXT.md §D-14
  </read_first>
  <action>
**Étape 1 — `app/pages/hytale.vue` :**

Template actuel (lignes 30-39) :
```vue
<template>
  <div>
    <HytaleHeroSection />
    <HytaleServicesSection />
    <HytalePricingSection />
    <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
      <TestimonialsSection />
    </div>
  </div>
</template>
```

Modification exacte : insérer `<HytaleRecentArticles />` **après** le `</div>` qui ferme le wrapper testimonials, **avant** le `</div>` final du root. Résultat attendu :
```vue
<template>
  <div>
    <HytaleHeroSection />
    <HytaleServicesSection />
    <HytalePricingSection />
    <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
      <TestimonialsSection />
    </div>
    <HytaleRecentArticles />
  </div>
</template>
```
Aucun changement dans le `<script setup>`. Aucun import (auto-import Nuxt).

**Étape 2 — `i18n/locales/fr.json` :**

Localiser le bloc `"hytale": { ... }` (début ~ligne 471). Ajouter un sous-objet `recentArticles` en sibling de `hero`/`services`/`pricing` (ordre libre, mais placer à la fin du bloc hytale pour minimiser le diff). Style **accentué** (cohérent avec `blog.*` ajouté Phase 6-02, voir PATTERNS.md §i18n) :

```json
"recentArticles": {
  "title": "Articles récents",
  "subtitle": "Les dernières publications sur le développement de plugins Hytale",
  "viewAll": "Voir tous les articles"
}
```

**Étape 3 — `i18n/locales/en.json` :**

Miroir exact dans le bloc `"hytale": { ... }` :
```json
"recentArticles": {
  "title": "Recent articles",
  "subtitle": "Latest writing on Hytale plugin development",
  "viewAll": "View all articles"
}
```

**Règles :**
- JSON valide : pas de trailing comma, double quotes, virgule correcte entre la clé sibling précédente et `recentArticles`.
- Conserver l'indentation existante du fichier.
- Ne PAS modifier d'autres clés.
  </action>
  <verify>
    <automated>pnpm typecheck &amp;&amp; node -e "JSON.parse(require('fs').readFileSync('i18n/locales/fr.json','utf8')); JSON.parse(require('fs').readFileSync('i18n/locales/en.json','utf8'))"</automated>
  </verify>
  <done>
    - `grep "<HytaleRecentArticles" app/pages/hytale.vue` passe
    - `grep -A 3 "\"recentArticles\"" i18n/locales/fr.json` affiche title/subtitle/viewAll
    - `grep -A 3 "\"recentArticles\"" i18n/locales/en.json` affiche title/subtitle/viewAll
    - `pnpm typecheck` exit 0
    - JSON parse FR + EN sans erreur
    - Run `pnpm dev` puis `curl http://localhost:3000/hytale` → HTML rendu sans erreur 500 (section absente tant que 0 article hytale, conforme D-12)
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| content DB → SSR render | Données lues par queryCollection ; Zod-validées Phase 5, pas d'user input |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08-01 | T (Tampering) | filtre JS tags.includes('hytale') | mitigate | `Array.isArray(a.tags)` guard avant `.includes()` pour éviter TypeError si frontmatter cassé passe le schema |
| T-08-02 | I (Info Disclosure) | queryCollection where draft=false | mitigate | Filtre draft=false SQL obligatoire (déjà pattern éprouvé /blog) — pas de leak d'articles draft:true |
| T-08-03 | D (DoS) | limit 2 post-filter | accept | Limite post-filter sur JS, volume d'articles < 100 attendu, négligeable |
</threat_model>

<verification>
- `pnpm typecheck` exit 0
- `curl http://localhost:3000/hytale` et `curl http://localhost:3000/en/hytale` retournent 200 SSR sans erreur
- Tant qu'aucun article hytale:true n'existe, section invisible dans HTML (grep `recentArticles` absent de la sortie curl) — conforme D-12
- Après Wave 2, re-curl : section visible avec 2 slugs
</verification>

<success_criteria>
- Composant HytaleRecentArticles.vue livré, auto-importé, TypeScript strict, pattern Phase 5 Pitfall-safe
- app/pages/hytale.vue injecte `<HytaleRecentArticles />` en dernière position du root
- Clés i18n FR+EN présentes sous `hytale.recentArticles.*`
- Zéro erreur typecheck, zéro warning console SSR sur /hytale
</success_criteria>

<output>
Après complétion, créer `.planning/phases/08-content-cocon-semantique/08-01-SUMMARY.md` (template summary).
</output>
