---
phase: 05-nuxt-content-setup-renderer
plan: 02
type: execute
wave: 2
depends_on:
  - '01'
files_modified:
  - app/components/content/ProseImg.vue
  - app/components/content/Alert.vue
  - app/components/content/ProseImg.vue
  - app/components/content/Alert.vue
  - content/fr/blog/test-kotlin-syntax.md
  - content/en/blog/test-kotlin-syntax.md
  - app/pages/test.vue
autonomous: false
requirements:
  - BLOG-01
  - BLOG-05

must_haves:
  truths:
    - "Un article markdown avec un bloc Kotlin est rendu avec coloration syntaxique visible"
    - "Une image référencée dans l'article s'affiche via NuxtImg avec lazy loading et srcset"
    - "Un tableau markdown est rendu avec le style prose correct"
    - "Un callout ::alert{type='info'} affiche un UAlert stylisé Nuxt UI"
    - "Les quatre types de callout (info, warning, tip, danger) fonctionnent"
  artifacts:
    - path: "app/components/content/ProseImg.vue"
      provides: "Override ProseImg → NuxtImg optimisé"
      exports: ["default (component)"]
    - path: "app/components/content/Alert.vue"
      provides: "Composant MDC callout via UAlert"
      exports: ["default (component)"]
    - path: "content/fr/blog/test-kotlin-syntax.md"
      provides: "Article de test FR couvrant les 4 success criteria"
      contains: "```kotlin"
    - path: "content/en/blog/test-kotlin-syntax.md"
      provides: "Article de test EN — même slug"
      contains: "```kotlin"
  key_links:
    - from: "content/fr/blog/test-kotlin-syntax.md"
      to: "app/components/content/ProseImg.vue"
      via: "ContentRenderer détecte les balises img et les route vers ProseImg"
      pattern: "ProseImg"
    - from: "content/fr/blog/test-kotlin-syntax.md"
      to: "app/components/content/Alert.vue"
      via: "MDC ::alert{type} appelle Alert.vue"
      pattern: "::alert"
---

<objective>
Créer les composants de rendu markdown (ProseImg + Alert) et les articles de test permettant de valider visuellement les 4 success criteria de la phase.

Purpose: Les composants MDC sont le liant entre le markdown brut et le rendu visuel. ProseImg garantit que chaque image passe par NuxtImg (BLOG-05). Alert garantit que les callouts ::alert sont rendus comme des composants Nuxt UI stylisés (BLOG-01).

Output:
- `app/components/content/ProseImg.vue` — override transparent NuxtImg
- `app/components/content/Alert.vue` — callout MDC avec 4 types (info/warning/tip/danger)
- `content/fr/blog/test-kotlin-syntax.md` — article de test couvrant les 4 critères
- `content/en/blog/test-kotlin-syntax.md` — version EN du même article
- Checkpoint visuel validant rendu Kotlin coloré + image NuxtImg + tableau + callout
</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/phases/05-nuxt-content-setup-renderer/05-CONTEXT.md
@.planning/phases/05-nuxt-content-setup-renderer/05-RESEARCH.md
@.planning/phases/05-nuxt-content-setup-renderer/05-UI-SPEC.md
@.planning/phases/05-nuxt-content-setup-renderer/05-01-SUMMARY.md

<interfaces>
<!-- Composants analogues du projet — suivre ces patterns -->

<!-- app/components/ProjectCard.vue — usage NuxtImg (source: PATTERNS.md) -->
```vue
<NuxtImg
  :src="project.image"
  :alt="`...`"
  loading="lazy"
  format="webp"
  width="400"
  height="300"
  class="w-full h-52 object-cover"
/>
```

<!-- app/components/TechBadge.vue — pattern withDefaults + computed map (source: PATTERNS.md) -->
```typescript
const props = withDefaults(defineProps<Props>(), {
  showLevel: true,
  showImage: true,
})
const levelColor = computed(() => {
  switch (techData.value.level) {
    case 'Advanced': return 'success' as const
    // ...
  }
})
```

<!-- nuxt.config.ts components config — auto-import depuis components/content/ -->
```typescript
components: [
  {
    path: '~/components',
    pathPrefix: false,  // composants dans content/ sont auto-importés ET reconnus MDC
  },
],
```

<!-- Contrat prose wrapper (UI-SPEC.md) -->
```html
<article class="prose dark:prose-invert max-w-none">
  <ContentRenderer :value="page" />
</article>
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Créer les composants MDC ProseImg.vue et Alert.vue</name>
  <files>app/components/content/ProseImg.vue, app/components/content/Alert.vue</files>
  <read_first>
    - app/components/content/ (vérifier si le dossier existe — le créer si nécessaire)
    - .planning/phases/05-nuxt-content-setup-renderer/05-RESEARCH.md (Pattern 4 ProseImg, Pattern 5 Alert)
    - .planning/phases/05-nuxt-content-setup-renderer/05-UI-SPEC.md (Component Inventory, tableau iconMap/colorMap)
  </read_first>
  <action>
    Créer le dossier `app/components/content/` s'il n'existe pas.

    **1. Créer `app/components/content/ProseImg.vue` :**
    ```vue
    <script setup lang="ts">
    interface Props {
      src: string
      alt?: string
      title?: string
      width?: string | number
      height?: string | number
    }
    const props = withDefaults(defineProps<Props>(), {
      alt: '',
    })
    </script>

    <template>
      <NuxtImg
        :src="props.src"
        :alt="props.alt"
        :title="props.title"
        :width="props.width"
        :height="props.height"
        class="rounded-lg w-full"
        sizes="sm:600px md:800px lg:1000px"
      />
    </template>
    ```

    NuxtImg est auto-importé par @nuxt/image — pas d'import explicite nécessaire.
    NE PAS ajouter `loading="lazy"` explicite sur NuxtImg — @nuxt/image gère lazy par défaut.

    **2. Créer `app/components/content/Alert.vue` :**
    ```vue
    <script setup lang="ts">
    interface Props {
      type?: 'info' | 'warning' | 'tip' | 'danger'
    }
    const props = withDefaults(defineProps<Props>(), {
      type: 'info',
    })

    const iconMap = {
      info: 'i-heroicons-information-circle',
      warning: 'i-heroicons-exclamation-triangle',
      tip: 'i-heroicons-light-bulb',
      danger: 'i-heroicons-x-circle',
    }
    const colorMap = {
      info: 'info',
      warning: 'warning',
      tip: 'success',
      danger: 'error',
    }
    </script>

    <template>
      <UAlert
        :icon="iconMap[props.type]"
        :color="colorMap[props.type] as any"
        variant="soft"
        class="my-4"
      >
        <template #description>
          <ContentSlot :use="$slots.default" unwrap="p" />
        </template>
      </UAlert>
    </template>
    ```

    CRITIQUE : `<ContentSlot :use="$slots.default" unwrap="p" />` est OBLIGATOIRE — sans cette ligne, le contenu entre `::alert` et `::` n'est pas rendu (Pitfall 4 RESEARCH.md).
    UAlert et ContentSlot sont auto-importés — pas d'import explicite.
  </action>
  <verify>
    ```bash
    test -f app/components/content/ProseImg.vue && echo "ProseImg OK"
    test -f app/components/content/Alert.vue && echo "Alert OK"
    grep "NuxtImg" app/components/content/ProseImg.vue
    grep "ContentSlot" app/components/content/Alert.vue
    grep "iconMap" app/components/content/Alert.vue
    grep "i-heroicons-information-circle" app/components/content/Alert.vue
    ```
  </verify>
  <acceptance_criteria>
    - `app/components/content/ProseImg.vue` existe et contient `<NuxtImg` avec `:src`, `:alt`, `sizes`
    - `app/components/content/Alert.vue` existe et contient `<ContentSlot :use="$slots.default" unwrap="p" />`
    - `Alert.vue` définit les 4 types : `'info' | 'warning' | 'tip' | 'danger'`
    - `Alert.vue` contient `iconMap` avec les 4 icônes Heroicons
    - `Alert.vue` contient `colorMap` avec les 4 couleurs Nuxt UI
    - `Alert.vue` utilise `UAlert` avec `variant="soft"`
    - `ProseImg.vue` utilise `withDefaults` avec `alt: ''` comme valeur par défaut
    - Aucun import explicite de NuxtImg, UAlert ou ContentSlot (auto-importés)
  </acceptance_criteria>
  <done>ProseImg.vue et Alert.vue créés et conformes aux patterns du projet.</done>
</task>

<task type="auto">
  <name>Task 2: Créer les articles de test markdown FR et EN</name>
  <files>content/fr/blog/test-kotlin-syntax.md, content/en/blog/test-kotlin-syntax.md, app/pages/test.vue (a supprimer apres checkpoint visuel)</files>
  <read_first>
    - .planning/phases/05-nuxt-content-setup-renderer/05-UI-SPEC.md (Copywriting Contract — copie exacte des textes)
    - .planning/phases/05-nuxt-content-setup-renderer/05-RESEARCH.md (Code Examples — structure de l'article de test)
    - content.config.ts (vérifier que le schema Zod attend ces champs frontmatter)
  </read_first>
  <action>
    Créer les dossiers `content/fr/blog/` et `content/en/blog/` s'ils n'existent pas.

    **1. Créer `content/fr/blog/test-kotlin-syntax.md` :**

    ````markdown
    ---
    title: "Test Kotlin Syntax Highlighting"
    description: "Article de test pour valider le renderer @nuxt/content"
    date: "2026-04-21"
    tags: ["kotlin", "hytale", "test"]
    ---

    ## Bloc de code Kotlin

    ```kotlin
    fun main() {
        println("Hello, Hytale!")
    }
    
    fun createPlugin(name: String): Plugin {
        return Plugin(name = name, version = "1.0.0")
    }
    ```

    ## Image optimisée

    ![Image de test pour NuxtImg dans les articles](/images/og-image.png)

    ## Tableau

    | Fonctionnalité | Statut | Notes |
    |----------------|--------|-------|
    | Syntax highlighting | ✅ Actif | Kotlin, Java, TypeScript, Shell |
    | Images optimisées | ✅ Actif | Via NuxtImg (lazy + srcset) |
    | Tableaux | ✅ Actif | Rendu prose |
    | Callouts | ✅ Actif | MDC ::alert{type} |

    ## Callouts

    ::alert{type="info"}
    Ceci est un callout d'information.
    ::

    ::alert{type="warning"}
    Ceci est un avertissement.
    ::

    ::alert{type="tip"}
    Conseil pratique de développement Kotlin.
    ::

    ::alert{type="danger"}
    Erreur critique — à ne pas ignorer.
    ::
    ````

    **2. Créer `content/en/blog/test-kotlin-syntax.md` :**

    ````markdown
    ---
    title: "Test Kotlin Syntax Highlighting"
    description: "Test article to validate the @nuxt/content renderer"
    date: "2026-04-21"
    tags: ["kotlin", "hytale", "test"]
    ---

    ## Kotlin Code Block

    ```kotlin
    fun main() {
        println("Hello, Hytale!")
    }
    
    fun createPlugin(name: String): Plugin {
        return Plugin(name = name, version = "1.0.0")
    }
    ```

    ## Optimized Image

    ![Test image for NuxtImg in articles](/images/og-image.png)

    ## Table

    | Feature | Status | Notes |
    |---------|--------|-------|
    | Syntax highlighting | ✅ Active | Kotlin, Java, TypeScript, Shell |
    | Optimized images | ✅ Active | Via NuxtImg (lazy + srcset) |
    | Tables | ✅ Active | Prose rendering |
    | Callouts | ✅ Active | MDC ::alert{type} |

    ## Callouts

    ::alert{type="info"}
    This is an information callout.
    ::

    ::alert{type="warning"}
    This is a warning.
    ::

    ::alert{type="tip"}
    Practical Kotlin development tip.
    ::

    ::alert{type="danger"}
    Critical error — do not ignore.
    ::
    ````

    Note sur l'image : utiliser `/images/og-image.png` qui existe déjà dans `public/images/` — cela valide le pipeline ProseImg sans nécessiter une image supplémentaire.
  </action>
  <verify>
    ```bash
    test -f content/fr/blog/test-kotlin-syntax.md && echo "FR OK"
    test -f content/en/blog/test-kotlin-syntax.md && echo "EN OK"
    grep '```kotlin' content/fr/blog/test-kotlin-syntax.md
    grep '::alert{type="info"}' content/fr/blog/test-kotlin-syntax.md
    grep '::alert{type="warning"}' content/fr/blog/test-kotlin-syntax.md
    grep '::alert{type="tip"}' content/fr/blog/test-kotlin-syntax.md
    grep '::alert{type="danger"}' content/fr/blog/test-kotlin-syntax.md
    grep "| Colonne\|Fonctionnalité\|Feature" content/fr/blog/test-kotlin-syntax.md content/en/blog/test-kotlin-syntax.md
    ```
  </verify>
  <acceptance_criteria>
    - `content/fr/blog/test-kotlin-syntax.md` existe avec frontmatter complet (title, description, date, tags)
    - `content/en/blog/test-kotlin-syntax.md` existe avec frontmatter EN
    - Les deux fichiers contiennent un bloc ` ```kotlin ` avec au moins 2 lignes de code
    - Les deux fichiers contiennent une image markdown `![...](...)`
    - Les deux fichiers contiennent un tableau markdown avec header `|...|...|`
    - Les deux fichiers contiennent les 4 callouts : `::alert{type="info"}`, `::alert{type="warning"}`, `::alert{type="tip"}`, `::alert{type="danger"}`
    - Le slug est identique dans les deux langues : `test-kotlin-syntax`
  </acceptance_criteria>
  <done>Articles de test créés en FR et EN. L'article couvre les 4 success criteria de la phase.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Markdown → ContentRenderer | HTML généré par @nuxt/content — pas d'input utilisateur dans cette phase |
| MDC composants → DOM | Composants Vue rendus côté serveur — auto-échappement Vue actif |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-05 | Tampering | `app/components/content/Alert.vue` prop `type` | accept | Valeur `type` vient du frontmatter markdown (auteur contrôlé) — pas d'input utilisateur; TypeScript union `'info' \| 'warning' \| 'tip' \| 'danger'` limite les valeurs |
| T-05-06 | Information Disclosure | `ProseImg.vue` prop `src` | accept | `src` vient du markdown statique — pas d'SSRF possible (NuxtImg résout les chemins au build) |
| T-05-07 | Spoofing | `ContentSlot` dans Alert.vue | accept | ContentSlot est un composant officiel @nuxt/content — pas de XSS, le contenu est du texte markdown échappé |
</threat_model>

<verification>
Après exécution du plan 02 (checkpoint visuel requis) :

1. Démarrer le serveur de dev : `pnpm dev`
2. Créer une page de test temporaire (ou utiliser la console) pour rendre l'article :
   - Si une page `/test` existe, y ajouter `<ContentRenderer>`
   - Sinon, créer `app/pages/test.vue` temporairement avec :
     ```vue
     <script setup lang="ts">
     const { data: page } = await useAsyncData('test', () =>
       queryCollection('blog_fr').path('/blog/test-kotlin-syntax').first()
     )
     </script>
     <template>
       <article class="prose dark:prose-invert max-w-none p-8">
         <ContentRenderer v-if="page" :value="page" />
       </article>
     </template>
     ```
3. Naviguer vers `http://localhost:3000/test`
4. Vérifier visuellement les 4 critères

La page de test temporaire peut être supprimée après validation — elle est hors scope de cette phase.

**Vérifications grep :**
```bash
test -f app/components/content/ProseImg.vue
test -f app/components/content/Alert.vue
grep "ContentSlot" app/components/content/Alert.vue
test -f content/fr/blog/test-kotlin-syntax.md
test -f content/en/blog/test-kotlin-syntax.md
```
</verification>

<success_criteria>
- `ProseImg.vue` et `Alert.vue` existent dans `app/components/content/`
- Les articles de test FR et EN existent avec les 4 éléments de validation
- Checkpoint visuel : bloc Kotlin coloré visible (spans avec couleurs Shiki)
- Checkpoint visuel : image rendue via `<img srcset=...>` (NuxtImg actif)
- Checkpoint visuel : tableau affiché avec bordures prose
- Checkpoint visuel : callout info affiché comme UAlert bleu avec icône
- `pnpm typecheck` passe (0 erreur TypeScript)
</success_criteria>

<task type="checkpoint:human-verify" gate="blocking">
  <what-built>
    - ProseImg.vue : override transparent qui route toutes les images markdown vers NuxtImg
    - Alert.vue : composant MDC pour ::alert{type} avec 4 types (info/warning/tip/danger) via UAlert Nuxt UI
    - Article de test FR/EN contenant les 4 éléments de validation
  </what-built>
  <how-to-verify>
    1. S'assurer que `pnpm dev` tourne
    2. Créer `app/pages/test.vue` temporairement (voir section verification ci-dessus)
    3. Visiter http://localhost:3000/test
    4. Vérifier visuellement :
       - [ ] Le bloc Kotlin est coloré (pas du texte brut gris) — en mode dark, fond sombre avec tokens colorés
       - [ ] L'image s'affiche (pas de 404) et l'élément DOM est `<img srcset="...">` (inspecter avec DevTools)
       - [ ] Le tableau markdown est rendu avec des lignes horizontales et en-têtes distingués
       - [ ] Le callout "info" apparaît comme une alerte bleue avec icône cercle-information
       - [ ] En passant en mode light (toggle du site), les couleurs Shiki changent (github-light)
    5. Supprimer `app/pages/test.vue` après validation
  </how-to-verify>
  <resume-signal>Taper "approved" si les 5 points sont validés, ou décrire le problème rencontré</resume-signal>
</task>

<output>
Après completion, créer `.planning/phases/05-nuxt-content-setup-renderer/05-02-SUMMARY.md`
</output>
