- 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 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 |
|
true |
|
|
|
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] },
)
Script setup (TypeScript strict) :
const { t, locale } = useI18n()+const localePath = useLocalePath()const isFr = computed(() => locale.value === 'fr')useAsyncDataavec key littérale interpolée`hytale-recent-${locale.value}`, ternaireisFr.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 :(renomme la destructurationconst articles = computed(() => { const all = data.value ?? [] return all.filter((a) => Array.isArray(a.tags) && a.tags.includes('hytale')).slice(0, 2) })useAsyncDataen{ data }et exposearticlescomputed — 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-autopour 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 dedirection→ 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.vueexiste grep -E "queryCollection\\('blog_(fr|en)'\\)" app/components/HytaleRecentArticles.vueretourne les 2 branchesgrep "v-if=\"articles.length\"" app/components/HytaleRecentArticles.vuepassegrep "variant=\"compact\"" app/components/HytaleRecentArticles.vuepassegrep "tags.*includes.*'hytale'" app/components/HytaleRecentArticles.vuepasse (filtre JS)pnpm typecheckexit 0
- Fichier
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.vuepassegrep -A 3 "\"recentArticles\"" i18n/locales/fr.jsonaffiche title/subtitle/viewAllgrep -A 3 "\"recentArticles\"" i18n/locales/en.jsonaffiche title/subtitle/viewAllpnpm typecheckexit 0- JSON parse FR + EN sans erreur
- Run
pnpm devpuiscurl 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> |
<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>