Compare commits

...

3 Commits

Author SHA1 Message Date
kayjaydee 1df6a21c5e docs(05): add detailed pattern map for @nuxt/content setup and renderer
- Introduced a new document outlining the configuration and component patterns for integrating @nuxt/content.
- Included mappings for `nuxt.config.ts`, `content.config.ts`, and new components `ProseImg.vue` and `Alert.vue`.
- Added example markdown content for testing syntax highlighting and layout.
- Documented critical patterns and anti-patterns to follow during implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:52:59 +02:00
kayjaydee cb477006f4 fix(05): resolve checker issues — open questions resolved, depends_on corrected, test.vue added
- 05-RESEARCH.md: rename section to 'Open Questions (RESOLVED)' with explicit answers
  (frontmatter schema: tags array, image relative path, author implicit from site.ts;
   i18n prefix: /blog for blog_fr, /en/blog for blog_en)
- 05-02-PLAN.md: fix depends_on from '05-01-PLAN.md' to '01'
- 05-02-PLAN.md: add app/pages/test.vue in Task 2 files (with note to delete after checkpoint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:52:51 +02:00
kayjaydee 808835d5eb docs(05): create phase 5 plan — @nuxt/content setup & renderer
2 plans, 2 waves. Plan 01 installe @nuxt/content + typography et
configure Shiki dual-theme + collections bilingues. Plan 02 crée
ProseImg/Alert MDC et articles de test FR/EN avec checkpoint visuel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:48:02 +02:00
5 changed files with 1196 additions and 11 deletions
+5 -2
View File
@@ -115,7 +115,10 @@ Plans:
2. Un article markdown de test avec un bloc Kotlin est rendu avec coloration syntaxique visible dans le navigateur 2. Un article markdown de test avec un bloc Kotlin est rendu avec coloration syntaxique visible dans le navigateur
3. Une image referencee dans un article s'affiche via `<NuxtImg>` avec les optimisations (lazy, format webp) 3. Une image referencee dans un article s'affiche via `<NuxtImg>` avec les optimisations (lazy, format webp)
4. Un tableau markdown et un callout/alert sont rendus avec le style correct 4. Un tableau markdown et un callout/alert sont rendus avec le style correct
**Plans**: TBD **Plans:** 2 plans
Plans:
- [ ] 05-01-PLAN.md — Installation @nuxt/content, configuration Shiki dual-theme, content.config.ts collections bilingues
- [ ] 05-02-PLAN.md — Composants MDC ProseImg + Alert, articles de test FR/EN, checkpoint visuel
**UI hint**: yes **UI hint**: yes
### Phase 6: Blog Pages ### Phase 6: Blog Pages
@@ -161,7 +164,7 @@ Plans:
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 5. @nuxt/content Setup & Renderer | 0/? | Not started | - | | 5. @nuxt/content Setup & Renderer | 0/2 | Not started | - |
| 6. Blog Pages | 0/? | Not started | - | | 6. Blog Pages | 0/? | Not started | - |
| 7. SEO Blog | 0/? | Not started | - | | 7. SEO Blog | 0/? | Not started | - |
| 8. Content & Cocon Semantique | 0/? | Not started | - | | 8. Content & Cocon Semantique | 0/? | Not started | - |
@@ -0,0 +1,344 @@
---
phase: 05-nuxt-content-setup-renderer
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- nuxt.config.ts
- app/assets/css/main.css
- content.config.ts
- package.json
autonomous: true
requirements:
- BLOG-01
- BLOG-04
must_haves:
truths:
- "@nuxt/content est installé et `pnpm dev` démarre sans erreur"
- "Shiki est configuré avec les langages Kotlin, Java, TypeScript, Shell et les thèmes github-light/github-dark"
- "Les collections blog_fr et blog_en sont déclarées dans content.config.ts avec le bon prefix i18n"
- "@tailwindcss/typography est chargé via `@plugin` dans main.css"
artifacts:
- path: "content.config.ts"
provides: "Déclaration des collections bilingues blog_fr + blog_en avec schema Zod"
exports: ["defineContentConfig"]
- path: "nuxt.config.ts"
provides: "Module @nuxt/content + config Shiki dual-theme + sqliteConnector native"
contains: "@nuxt/content"
- path: "app/assets/css/main.css"
provides: "Plugin @tailwindcss/typography chargé"
contains: "@plugin"
key_links:
- from: "nuxt.config.ts content.build.markdown.highlight"
to: "Shiki dual-theme github-light/github-dark"
via: "theme.default + theme.dark"
pattern: "github-light.*github-dark|github-dark.*github-light"
- from: "content.config.ts collections.blog_fr"
to: "content/fr/blog/**/*.md"
via: "source.include"
pattern: "fr/blog/\\*\\*"
---
<objective>
Installer `@nuxt/content` v3 et `@tailwindcss/typography`, puis configurer le système de rendu markdown — Shiki dual-theme, collections bilingues, connecteur SQLite natif.
Purpose: Cette phase pose les fondations du CMS. Sans elle, les phases 6, 7 et 8 ne peuvent pas fonctionner. La configuration doit être définitive — aucun retour en arrière attendu.
Output:
- `@nuxt/content` installé et déclaré dans `nuxt.config.ts`
- `content.config.ts` avec collections `blog_fr` + `blog_en`
- Shiki configuré pour Kotlin, Java, TypeScript, Shell avec thèmes dark/light
- `@tailwindcss/typography` chargé via `@plugin` dans `main.css`
- `pnpm dev` démarre sans erreur
</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/05-nuxt-content-setup-renderer/05-CONTEXT.md
@.planning/phases/05-nuxt-content-setup-renderer/05-RESEARCH.md
<interfaces>
<!-- État actuel de nuxt.config.ts — ne PAS réécrire, uniquement étendre -->
<!-- nuxt.config.ts lignes 7-14 (modules) : -->
```typescript
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image'
],
```
<!-- nuxt.config.ts lignes 24-30 (colorMode) : -->
```typescript
colorMode: {
preference: 'dark',
fallback: 'dark',
storage: 'cookie',
storageKey: 'nuxt-color-mode',
classSuffix: '' // ← CRITIQUE: Shiki dual-theme nécessite classSuffix: '' pour html.dark
},
```
<!-- État actuel de app/assets/css/main.css : -->
```css
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--color-brand-500: #85cb85;
/* ... autres tokens brand */
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Installer @nuxt/content et @tailwindcss/typography</name>
<files>package.json</files>
<read_first>
- package.json (vérifier pnpm.onlyBuiltDependencies existant, ne pas écraser)
</read_first>
<action>
Exécuter les deux commandes d'installation suivantes dans l'ordre :
```bash
pnpm add @nuxt/content
pnpm add -D @tailwindcss/typography
```
Versions cibles : `@nuxt/content@^3.6.3`, `@tailwindcss/typography@^0.5.x`.
NE PAS ajouter `better-sqlite3` — le connecteur natif Node 22 sera utilisé via `experimental.sqliteConnector: 'native'` dans nuxt.config.ts (Task 2).
Si `pnpm add` échoue avec une erreur de script build SQLite, c'est normal sans la config native — continuer vers Task 2 qui la résout.
</action>
<verify>
```bash
grep '"@nuxt/content"' package.json
grep '"@tailwindcss/typography"' package.json
```
Les deux lignes doivent apparaître.
</verify>
<acceptance_criteria>
- `package.json` contient `"@nuxt/content"` dans `dependencies`
- `package.json` contient `"@tailwindcss/typography"` dans `devDependencies`
- `node_modules/@nuxt/content` existe
- `node_modules/@tailwindcss/typography` existe
</acceptance_criteria>
<done>Les deux packages sont installés via pnpm sans erreur bloquante.</done>
</task>
<task type="auto">
<name>Task 2: Configurer nuxt.config.ts et app/assets/css/main.css</name>
<files>nuxt.config.ts, app/assets/css/main.css</files>
<read_first>
- nuxt.config.ts (lire INTÉGRALEMENT avant de modifier — ne jamais réécrire, uniquement étendre)
- app/assets/css/main.css (lire INTÉGRALEMENT)
</read_first>
<action>
**1. nuxt.config.ts — deux modifications :**
a) Ajouter `'@nuxt/content'` à la fin du tableau `modules` (après `'@nuxt/image'`) :
```typescript
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image',
'@nuxt/content' // ← ligne ajoutée
],
```
b) Ajouter le bloc `content` après le bloc `gtag` existant (avant la fermeture `}`) :
```typescript
content: {
build: {
markdown: {
highlight: {
theme: {
default: 'github-light',
dark: 'github-dark'
},
langs: ['kotlin', 'java', 'typescript', 'shell', 'bash', 'json', 'vue', 'html', 'css']
}
}
},
experimental: {
sqliteConnector: 'native'
}
}
```
NE PAS utiliser `nativeSqlite: true` (déprécié). Utiliser exclusivement `sqliteConnector: 'native'`.
NE PAS modifier `colorMode.classSuffix` — doit rester `''` pour que Shiki dual-theme fonctionne via `html.dark`.
**2. app/assets/css/main.css — une ligne ajoutée :**
Ajouter `@plugin "@tailwindcss/typography";` après `@import "@nuxt/ui";` :
```css
@import "tailwindcss";
@import "@nuxt/ui";
@plugin "@tailwindcss/typography";
```
NE PAS utiliser `plugins: [require('@tailwindcss/typography')]` dans tailwind.config.js — cette syntaxe est ignorée en Tailwind v4. La syntaxe `@plugin` dans le CSS est la seule valide.
NE PAS toucher le bloc `@theme` existant avec les tokens `--color-brand-*`.
</action>
<verify>
```bash
grep "'@nuxt/content'" nuxt.config.ts
grep "github-dark" nuxt.config.ts
grep "sqliteConnector" nuxt.config.ts
grep "kotlin" nuxt.config.ts
grep '@plugin "@tailwindcss/typography"' app/assets/css/main.css
```
Les cinq lignes doivent retourner un résultat.
</verify>
<acceptance_criteria>
- `nuxt.config.ts` contient `'@nuxt/content'` dans le tableau `modules`
- `nuxt.config.ts` contient le bloc `content.build.markdown.highlight.theme` avec `default: 'github-light'` et `dark: 'github-dark'`
- `nuxt.config.ts` contient `sqliteConnector: 'native'` (PAS `nativeSqlite`)
- `nuxt.config.ts` liste au minimum ces langages Shiki : `'kotlin'`, `'java'`, `'typescript'`, `'shell'`
- `nuxt.config.ts` ne contient PAS `nativeSqlite`
- `app/assets/css/main.css` contient `@plugin "@tailwindcss/typography";` sur sa propre ligne
- `app/assets/css/main.css` contient toujours le bloc `@theme` avec `--color-brand-500`
</acceptance_criteria>
<done>nuxt.config.ts étend le module @nuxt/content avec Shiki dual-theme. main.css charge @tailwindcss/typography via @plugin.</done>
</task>
<task type="auto">
<name>Task 3: Créer content.config.ts avec collections bilingues</name>
<files>content.config.ts</files>
<read_first>
- nuxt.config.ts (vérifier i18n.strategy et i18n.defaultLocale pour confirmer le prefix des collections)
- .planning/phases/05-nuxt-content-setup-renderer/05-RESEARCH.md (Pattern 2 — content.config.ts)
</read_first>
<action>
Créer `content.config.ts` à la RACINE du projet (même niveau que `nuxt.config.ts`).
Contenu exact :
```typescript
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
const blogSchema = z.object({
title: z.string(),
description: z.string(),
date: z.string(),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
})
export default defineContentConfig({
collections: {
blog_fr: defineCollection({
type: 'page',
source: { include: 'fr/blog/**/*.md', prefix: '/blog' },
schema: blogSchema,
}),
blog_en: defineCollection({
type: 'page',
source: { include: 'en/blog/**/*.md', prefix: '/en/blog' },
schema: blogSchema,
}),
},
})
```
Justification des prefixes :
- `blog_fr` → prefix `/blog` (FR est la locale par défaut avec `prefix_except_default`, donc pas de `/fr/` dans l'URL)
- `blog_en` → prefix `/en/blog` (EN reçoit le préfixe de langue)
Ce schema minimal sera étendu en Phase 7 (author, og:image, etc.) — ne pas anticiper.
</action>
<verify>
```bash
test -f content.config.ts && echo "EXISTS"
grep "blog_fr" content.config.ts
grep "blog_en" content.config.ts
grep "prefix: '/blog'" content.config.ts
grep "prefix: '/en/blog'" content.config.ts
```
</verify>
<acceptance_criteria>
- `content.config.ts` existe à la racine du projet
- Contient l'export `defineContentConfig`
- Contient la collection `blog_fr` avec `source.include: 'fr/blog/**/*.md'` et `source.prefix: '/blog'`
- Contient la collection `blog_en` avec `source.include: 'en/blog/**/*.md'` et `source.prefix: '/en/blog'`
- Le schema Zod contient les champs `title`, `description`, `date` (requis) et `tags`, `image` (optionnels)
- `pnpm dev` démarre sans erreur après ces trois tasks (vérification smoke finale)
</acceptance_criteria>
<done>content.config.ts créé avec collections bilingues. `pnpm dev` démarre sans erreur — l'infrastructure @nuxt/content est opérationnelle.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Système de fichiers → Parser @nuxt/content | Fichiers markdown lus au build — source contrôlée (auteur uniquement, pas d'input utilisateur) |
| Node.js 22 → SQLite natif | Connecteur natif utilisé au lieu de better-sqlite3 — pas d'exposition réseau |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-01 | Tampering | `content.config.ts` source.include glob | accept | Seuls les fichiers `*.md` dans `content/` sont indexés — aucun input utilisateur dans cette phase, glob contrôlé par l'auteur |
| T-05-02 | Information Disclosure | Shiki HTML output | accept | Shiki génère du HTML échappé — pas de XSS possible via blocs de code |
| T-05-03 | Denial of Service | SQLite natif Node 22 au build | accept | Build-time uniquement, pas d'exposition runtime — risque nul en production |
| T-05-04 | Elevation of Privilege | `experimental.sqliteConnector: 'native'` | accept | Connecteur natif Node.js — pas de binary externe, surface d'attaque réduite vs better-sqlite3 |
</threat_model>
<verification>
Après exécution du plan 01, vérifier :
```bash
# 1. Packages installés
grep '"@nuxt/content"' package.json && grep '"@tailwindcss/typography"' package.json
# 2. nuxt.config.ts étendu correctement
grep "'@nuxt/content'" nuxt.config.ts
grep "github-dark" nuxt.config.ts
grep "sqliteConnector.*native" nuxt.config.ts
# NE DOIT PAS contenir l'option dépréciée :
grep "nativeSqlite" nuxt.config.ts # doit retourner RIEN
# 3. CSS typography
grep '@plugin "@tailwindcss/typography"' app/assets/css/main.css
# 4. content.config.ts collections
grep "blog_fr\|blog_en" content.config.ts
# 5. Smoke test
pnpm dev # doit démarrer sans erreur
```
</verification>
<success_criteria>
- `pnpm dev` démarre sans erreur après installation et configuration
- `nuxt.config.ts` contient `'@nuxt/content'` dans modules et le bloc `content` avec Shiki dual-theme + langages
- `content.config.ts` existe avec les deux collections bilingues et le bon prefix i18n
- `app/assets/css/main.css` charge `@tailwindcss/typography` via `@plugin`
- `pnpm typecheck` passe (0 erreur TypeScript)
</success_criteria>
<output>
Après completion, créer `.planning/phases/05-nuxt-content-setup-renderer/05-01-SUMMARY.md`
</output>
@@ -0,0 +1,466 @@
---
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>
@@ -0,0 +1,372 @@
# Phase 5: @nuxt/content Setup & Renderer — Pattern Map
**Mapped:** 2026-04-21
**Files analyzed:** 7 (2 modifications + 5 créations)
**Analogs found:** 5 / 7
---
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
| `nuxt.config.ts` | config | — | `nuxt.config.ts` (lui-même, existant) | exact — extension |
| `content.config.ts` | config | CRUD | `nuxt.config.ts` (structure `defineNuxtConfig`) | role-match |
| `app/assets/css/main.css` | config | — | `app/assets/css/main.css` (lui-même, existant) | exact — extension |
| `app/components/content/ProseImg.vue` | component | request-response | `app/components/ProjectCard.vue` (NuxtImg + Props interface) | role-match |
| `app/components/content/Alert.vue` | component | request-response | `app/components/TechBadge.vue` (withDefaults + UBadge + computed map) | role-match |
| `content/fr/blog/test-kotlin-syntax.md` | content | — | aucun | no-analog |
| `content/en/blog/test-kotlin-syntax.md` | content | — | aucun | no-analog |
---
## Pattern Assignments
### `nuxt.config.ts` (config — extension)
**Analog:** `nuxt.config.ts` lui-même (ligne 165)
**État actuel** (lignes 714) :
```typescript
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image'
],
```
**Pattern à ajouter — ajout dans `modules`** :
```typescript
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxt/eslint',
'@nuxtjs/sitemap',
'nuxt-gtag',
'@nuxt/image',
'@nuxt/content' // ← ajouter ici
],
```
**Pattern à ajouter — bloc `content` après les modules existants** :
```typescript
content: {
build: {
markdown: {
highlight: {
theme: {
default: 'github-light',
dark: 'github-dark'
},
langs: ['kotlin', 'java', 'typescript', 'shell', 'bash', 'json', 'vue', 'html', 'css']
}
}
},
experimental: {
sqliteConnector: 'native' // Node 22+ — pas de better-sqlite3
}
},
```
**Contexte critique :** `colorMode.classSuffix: ''` est déjà configuré ligne 29 — Shiki dual-theme fonctionne via `html.dark`, donc compatible sans modification.
---
### `content.config.ts` (config — création, racine du projet)
**Analog:** Structure `nuxt.config.ts` (pattern `defineNuxtConfig``defineContentConfig`)
**Pattern complet** (source: RESEARCH.md Pattern 2) :
```typescript
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
const blogSchema = z.object({
title: z.string(),
description: z.string(),
date: z.string(),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
})
export default defineContentConfig({
collections: {
blog_fr: defineCollection({
type: 'page',
source: { include: 'fr/blog/**/*.md', prefix: '/blog' },
schema: blogSchema,
}),
blog_en: defineCollection({
type: 'page',
source: { include: 'en/blog/**/*.md', prefix: '/en/blog' },
schema: blogSchema,
}),
},
})
```
**Note sur le prefix :** `i18n.strategy: 'prefix_except_default'` avec `defaultLocale: 'fr'` → les articles FR sont sous `/blog/slug`, les EN sous `/en/blog/slug`. (Assumption A3 de RESEARCH.md — valider avec l'article de test.)
---
### `app/assets/css/main.css` (config — extension)
**Analog:** `app/assets/css/main.css` lui-même (lignes 13, existant)
**État actuel** (lignes 13) :
```css
@import "tailwindcss";
@import "@nuxt/ui";
```
**Pattern à ajouter — une ligne après `@import "@nuxt/ui"`** :
```css
@import "tailwindcss";
@import "@nuxt/ui";
@plugin "@tailwindcss/typography"; /* ← ajouter ici — syntaxe Tailwind v4 obligatoire */
```
**Anti-pattern à éviter :** Ne pas utiliser `plugins: [require('@tailwindcss/typography')]` dans `tailwind.config.js` — ignoré en Tailwind v4.
---
### `app/components/content/ProseImg.vue` (component, request-response)
**Analog:** `app/components/ProjectCard.vue` — utilisation de `NuxtImg` avec Props interface (lignes 116, 2535)
**Imports pattern** (depuis ProjectCard.vue, lignes 13) :
```vue
<script setup lang="ts">
// Pas d'import externe — NuxtImg est auto-importé par @nuxt/image
```
**Props pattern** (depuis ProjectCard.vue, lignes 48) :
```typescript
interface Props {
project: Project
}
const props = defineProps<Props>()
```
**NuxtImg pattern** (depuis ProjectCard.vue, lignes 2635) :
```vue
<NuxtImg
:src="project.image"
:alt="`...`"
loading="lazy"
format="webp"
width="400"
height="300"
class="w-full h-52 object-cover"
/>
```
**Pattern cible pour ProseImg.vue** (adapté avec `withDefaults` — depuis TechBadge.vue ligne 11) :
```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>
```
---
### `app/components/content/Alert.vue` (component, request-response)
**Analog:** `app/components/TechBadge.vue``withDefaults` + computed map de valeurs + composant Nuxt UI (`UBadge`) (lignes 157)
**withDefaults pattern** (depuis TechBadge.vue, lignes 1113) :
```typescript
const props = withDefaults(defineProps<Props>(), {
showLevel: true,
showImage: true,
})
```
**Computed map pattern** (depuis TechBadge.vue, lignes 4456) :
```typescript
const levelColor = computed(() => {
switch (techData.value.level) {
case 'Advanced': return 'success' as const
case 'Intermediate': return 'primary' as const
default: return 'neutral' as const
}
})
```
**Pattern cible pour Alert.vue** (adapté avec `UAlert` Nuxt UI + `ContentSlot` MDC) :
```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>
```
**Point critique :** `<ContentSlot :use="$slots.default" unwrap="p" />` est obligatoire — sans lui le contenu entre `::alert` et `::` n'est pas rendu (Pitfall 4 RESEARCH.md).
---
### `content/fr/blog/test-kotlin-syntax.md` (content — création)
**Analog:** aucun (pas de fichiers markdown dans le projet actuellement)
**Pattern depuis RESEARCH.md (Code Examples)** :
```markdown
---
title: "Test Kotlin Syntax Highlighting"
description: "Article de test pour valider le renderer"
date: "2026-04-21"
tags: ["kotlin", "hytale", "test"]
---
## Bloc de code Kotlin
```kotlin
fun main() {
println("Hello, Hytale!")
}
```
## Image optimisée
![Test image](/images/test.png)
## Tableau
| Colonne A | Colonne B |
|-----------|-----------|
| Valeur 1 | Valeur 2 |
## Callout
::alert{type="info"}
Ceci est un callout d'information.
::
```
**Critère de validation :** Ce fichier doit couvrir les 4 success criteria : bloc Kotlin coloré (BLOG-04), image via ProseImg (BLOG-05), tableau, callout (BLOG-01).
---
### `content/en/blog/test-kotlin-syntax.md` (content — création)
**Analog:** même structure que la version FR, même slug, contenu traduit en anglais.
---
## Shared Patterns
### Props avec valeurs par défaut (withDefaults)
**Source:** `app/components/TechBadge.vue` lignes 1114
**Apply to:** `ProseImg.vue`, `Alert.vue`
```typescript
const props = withDefaults(defineProps<Props>(), {
// valeurs par défaut pour props optionnelles
})
```
### NuxtImg usage
**Source:** `app/components/ProjectCard.vue` lignes 2635, `app/components/TechBadge.vue` lignes 6574
**Apply to:** `ProseImg.vue`
- Toujours utiliser `:src`, `:alt`, `loading="lazy"` au minimum
- `format="webp"` si format fixe, sinon laisser @nuxt/image décider
- `sizes` pour responsive
### Composants Nuxt UI (UAlert, UBadge)
**Source:** `app/components/TechBadge.vue` ligne 76, `app/components/FAQSection.vue` ligne 33
**Apply to:** `Alert.vue`
- Nuxt UI est auto-importé — pas d'import explicite nécessaire
- Utiliser `color` + `variant` pour le style
- `as any` acceptable pour les types union non-exhaustifs de Nuxt UI
### Convention import types
**Source:** `app/components/ProjectCard.vue` ligne 2
**Apply to:** `content.config.ts`
```typescript
import type { ... } from '~~/shared/types' // types partagés
import { ... } from '@nuxt/content' // imports de librairie
```
### Auto-import composants
**Source:** `nuxt.config.ts` lignes 1519
**Apply to:** `ProseImg.vue`, `Alert.vue`
```typescript
components: [
{
path: '~/components',
pathPrefix: false, // → components/content/Alert.vue est auto-importé
},
],
```
Les composants dans `components/content/` sont auto-importés par Nuxt ET reconnus par `@nuxt/content` comme Prose overrides / composants MDC.
---
## No Analog Found
| File | Role | Data Flow | Reason |
|---|---|---|---|
| `content/fr/blog/test-kotlin-syntax.md` | content | — | Pas de fichiers markdown dans le projet — nouveau format |
| `content/en/blog/test-kotlin-syntax.md` | content | — | Pas de fichiers markdown dans le projet — nouveau format |
Le planner doit utiliser le pattern RESEARCH.md "Code Examples" pour ces deux fichiers.
---
## Metadata
**Analog search scope:** `app/components/`, `app/assets/css/`, `nuxt.config.ts`
**Files scanned:** 6 (nuxt.config.ts, main.css, ProjectCard.vue, TechBadge.vue, FAQSection.vue, app.vue partiel)
**Pattern extraction date:** 2026-04-21
@@ -425,17 +425,17 @@ highlight: {
--- ---
## Open Questions ## Open Questions (RESOLVED)
1. **Frontmatter schema définitif** 1. **Frontmatter schema définitif** — RESOLVED
- Ce qu'on sait : `title`, `description`, `date` sont nécessaires pour Phase 6 (listing) - `tags`: `z.array(z.string()).optional()` dans content.config.ts (array, pas string)
- Ce qui est flou : `tags` (array ou string?), `image` (path relatif ou absolu?), `author` - `image`: chemin relatif depuis `public/` (ex: `/images/og-image.png`) — string optionnel
- Recommandation : Définir un schema minimal dans `content.config.ts` avec `z.string().optional()` pour les champs non-critiques — peut s'étendre en Phase 7 - `author`: implicite depuis `site.ts` (pas dans le frontmatter de cette phase — ajouté en Phase 7 si besoin)
2. **Prefix des collections i18n** 2. **Prefix des collections i18n** — RESOLVED
- Ce qu'on sait : `prefix_except_default` avec `defaultLocale: 'fr'` → les URLs FR sont sans `/fr/` - `source.prefix` pour `blog_fr` : `/blog` (FR est la locale par défaut, pas de préfixe `/fr/` grâce à `prefix_except_default`)
- Ce qui est flou : Le `source.prefix` dans content.config.ts doit-il matcher exactement le path i18n ? - `source.prefix` pour `blog_en` : `/en/blog` (EN est préfixé)
- Recommandation : Tester avec l'article de validation que `queryCollection('blog_fr').path('/blog/test-kotlin-syntax').first()` retourne le bon article - Aligné avec la strategy `prefix_except_default` de `@nuxtjs/i18n`
--- ---