Files
kayjaydee 9cc1dbec5d 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
2026-04-22 18:38:13 +02:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
phase plan type wave depends_on files_modified autonomous requirements tags must_haves
08-content-cocon-semantique 01 execute 1
app/components/HytaleRecentArticles.vue
app/pages/hytale.vue
i18n/locales/fr.json
i18n/locales/en.json
true
SEO-14
nuxt-content
i18n
hytale
cocon-semantique
truths artifacts key_links
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
path provides contains
app/components/HytaleRecentArticles.vue Section composant auto-importé, queryCollection branches littérales + filtre tag hytale + limit 2 queryCollection('blog_fr')
path provides contains
app/pages/hytale.vue Insertion <HytaleRecentArticles /> avant fermeture du root div <HytaleRecentArticles
path provides contains
i18n/locales/fr.json Clés hytale.recentArticles.title/subtitle/viewAll en FR accentué recentArticles
path provides contains
i18n/locales/en.json Clés hytale.recentArticles.title/subtitle/viewAll en EN recentArticles
from to via pattern
app/components/HytaleRecentArticles.vue BlogCard.vue (variant compact) auto-import Nuxt + props article+variant variant="compact"
from to via pattern
app/components/HytaleRecentArticles.vue @nuxt/content collections blog_fr / blog_en queryCollection(literal).where('tags', ...).limit(2) queryCollection('blog_(fr|en)')
from to via pattern
app/pages/hytale.vue app/components/HytaleRecentArticles.vue auto-import + template insertion <HytaleRecentArticles
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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 ```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' } ```
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] },
)
Task 1: Créer composant HytaleRecentArticles.vue (query + filtre tag + render) app/components/HytaleRecentArticles.vue - 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 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.valuequeryCollection('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 :
    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. pnpm typecheck
    • 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
Task 2: Injecter HytaleRecentArticles dans app/pages/hytale.vue + ajouter clés i18n FR/EN app/pages/hytale.vue, i18n/locales/fr.json, i18n/locales/en.json - 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 **Étape 1 — `app/pages/hytale.vue` :**

Template actuel (lignes 30-39) :

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

<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) :

"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": { ... } :

"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. pnpm typecheck && node -e "JSON.parse(require('fs').readFileSync('i18n/locales/fr.json','utf8')); JSON.parse(require('fs').readFileSync('i18n/locales/en.json','utf8'))"
    • 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)

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

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