docs(08): create phase plan — content & cocon sémantique (3 plans, 2 waves)
- 08-01 (W1): HytaleRecentArticles.vue scaffold + injection hytale.vue + i18n - 08-02 (W2): article tutorial how-to-build-your-first-hytale-plugin FR+EN - 08-03 (W2): article positionnement hytale-plugin-development-2026 FR+EN
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
---
|
||||
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 && 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>
|
||||
Reference in New Issue
Block a user