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>
This commit is contained in:
@@ -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,463 @@
|
||||
---
|
||||
phase: 05-nuxt-content-setup-renderer
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "05-01-PLAN.md"
|
||||
files_modified:
|
||||
- app/components/content/ProseImg.vue
|
||||
- app/components/content/Alert.vue
|
||||
- content/fr/blog/test-kotlin-syntax.md
|
||||
- content/en/blog/test-kotlin-syntax.md
|
||||
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</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
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
Reference in New Issue
Block a user