Files
portfolio/.planning/phases/08-content-cocon-semantique/08-PATTERNS.md
T

11 KiB
Raw Blame History

Phase 8: Content & Cocon Sémantique — Pattern Map

Mapped: 2026-04-22 Files analyzed: 7 (6 new + 1 modified) plus 2 locale files modified Analogs found: 7 / 7

File Classification

New/Modified File Role Data Flow Closest Analog Match Quality
content/fr/blog/how-to-build-your-first-hytale-plugin.md content (markdown article) file-I/O (static content) content/fr/blog/test-kotlin-syntax.md exact
content/en/blog/how-to-build-your-first-hytale-plugin.md content (markdown article) file-I/O (static content) content/en/blog/test-kotlin-syntax.md exact
content/fr/blog/hytale-plugin-development-2026.md content (markdown article) file-I/O (static content) content/fr/blog/test-kotlin-syntax.md exact
content/en/blog/hytale-plugin-development-2026.md content (markdown article) file-I/O (static content) content/en/blog/test-kotlin-syntax.md exact
app/components/HytaleRecentArticles.vue component (section) request-response (queryCollection SSR) app/pages/blog/index.vue role-match (page→component)
app/pages/hytale.vue page (composition) request-response current state (self) exact
i18n/locales/fr.json + en.json config (i18n) static existing hytale.* + blog.* blocks exact

Note path correction: CONTEXT mentions app/locales/ but actual path is i18n/locales/ (confirmed via Glob). Planner should use i18n/locales/fr.json and i18n/locales/en.json.

Note page path correction: CONTEXT mentions app/pages/hytale/index.vue. Actual page is app/pages/hytale.vue (flat, 39 lines). No directory routing.

Pattern Assignments

content/{fr,en}/blog/how-to-build-your-first-hytale-plugin.md (content, file-I/O)

Analog: content/fr/blog/test-kotlin-syntax.md (FR) and content/en/blog/test-kotlin-syntax.md (EN)

Frontmatter pattern (lines 1-7 of analog) — adapt for Phase 8 (draft: false, tags include hytale):

---
title: "Guide du format Markdown"
description: "Référence complète de tous les éléments et composants disponibles dans les articles"
date: "2026-04-21"
tags: ["guide", "markdown", "mdc"]
draft: true
---

Required changes for Phase 8 articles (per CONTEXT D-06):

  • draft: false (CONTEXT D-04)
  • tags: ['hytale', 'tutorial', 'kotlin'] (article 1) or ['hytale', 'industry', 'analysis'] (article 2) — tag hytale MANDATORY (D-11, D-15)
  • date: "2026-04-22" (ISO)
  • Omit updated field at initial publish (D-06)
  • image: optional — if present must point to existing asset in public/ (D-05, Phase 7 D-14)

Kotlin code block pattern (lines 25-33 of analog):

\`\`\`kotlin
fun createPlugin(name: String): HytalePlugin {
    return HytalePlugin.builder()
        .name(name)
        .version("1.0.0")
        .onLoad { println("Plugin $name loaded!") }
        .build()
}
\`\`\`

Every seed article MUST include ≥1 realistic Kotlin block (not pseudo-code) per D-05.

Internal link pattern (D-08, D-09): In FR article, inline markdown link uses /hytale. In EN article, uses /en/hytale:

Pour un plugin sur-mesure, vous pouvez [commissionner un plugin Hytale](/hytale) directement.
For a custom plugin, you can [commission a Hytale plugin](/en/hytale) directly.

Hard-code paths (D-09); do NOT use localePath() in markdown. Minimum 12 inline links per article.

Callout pattern available (optional):

::alert{type="tip"}
**Astuce** — Utilisez `pnpm` plutôt que `npm` pour les projets Nuxt.
::

app/components/HytaleRecentArticles.vue (component, request-response)

Analog: app/pages/blog/index.vue — same queryCollection bilingual branch pattern, slimmed to section-level component.

queryCollection bilingual pattern (lines 2-21 of analog) — the critical Phase 5 Pitfall-safe pattern:

const { t, locale } = useI18n()
const localePath = useLocalePath()
const isFr = computed(() => locale.value === 'fr')

const { data: articles } = await useAsyncData(
  `blog-list-${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] },
)

Adaptation for HytaleRecentArticles:

  • Key: hytale-recent-${locale.value} (per CONTEXT "Reusable Assets")
  • Add tag filter: either .where('tags', 'LIKE', '%hytale%') SQL OR JS post-filter article.tags?.includes('hytale') (CONTEXT D-11 — planner decides).
  • Add .limit(2) (D-11).
  • Branches must be LITERAL strings 'blog_fr' / 'blog_en' — never queryCollection(variableName) (Phase 5 D-03 Pitfall).

Conditional render + grid pattern (lines 141-151 of analog) adapted for compact variant + 2-col grid:

<section v-if="articles && articles.length" class="...">
  <h2>{{ t('hytale.recentArticles.title') }}</h2>
  <div class="grid grid-cols-1 md:grid-cols-2 gap-5 lg:gap-6">
    <BlogCard
      v-for="article in articles"
      :key="article.path"
      :article="article"
      variant="compact"
    />
  </div>
  <NuxtLink :to="localePath('/blog')">{{ t('hytale.recentArticles.viewAll') }}</NuxtLink>
</section>

BlogCard compact invocation — confirmed in app/components/BlogCard.vue lines 18-21 + 152-191:

  • Props: article (required), variant="compact", direction (default 'next', can omit here since not prev/next semantics — acceptable since direction only affects icon alignment; choose one OR add neutral behavior).
  • Auto-imported — no explicit import needed.

Hide-if-empty rule (D-12): v-if="articles && articles.length" — section entirely hidden when 0 or <2 hytale-tagged articles. No empty state UI.


app/pages/hytale.vue (page, modification)

Current state (full 39 lines read):

<template>
  <div>
    <HytaleHeroSection />
    <HytaleServicesSection />
    <HytalePricingSection />
    <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
      <TestimonialsSection />
    </div>
  </div>
</template>

Insertion point (CONTEXT D-10): Add <HytaleRecentArticles /> before the last section. Current last section is TestimonialsSection wrapped in a bg div. Two viable positions:

  1. Between HytalePricingSection and the Testimonials wrapper (before testimonials).
  2. After Testimonials wrapper (just before closing </div>).

CONTEXT says "en bas de page, avant le footer-CTA existant". There is no explicit footer-CTA in this page — TestimonialsSection is the last thing. Planner should insert after Testimonials, before closing </div> — or reconcile with actual footer CTA location (may live in AppFooter layout, outside page scope).

Recommended diff:

     <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
       <TestimonialsSection />
     </div>
+    <HytaleRecentArticles />
   </div>
 </template>

No script changes required — component is auto-imported.


i18n/locales/fr.json + i18n/locales/en.json (config)

Analog: existing hytale.* block in fr.json lines 471-556; existing blog.* block lines 557-581 (structure for i18n interpolation / nesting).

Insertion point: Inside the existing "hytale": { ... } object (line 471). Add a new recentArticles sub-object as sibling to hero, services, pricing.

Keys to add (CONTEXT D-14):

FR (i18n/locales/fr.json):

"recentArticles": {
  "title": "Articles récents",
  "subtitle": "Les dernières publications sur le développement Hytale",
  "viewAll": "Voir tous les articles"
}

EN (i18n/locales/en.json) — mirror structure:

"recentArticles": {
  "title": "Recent articles",
  "subtitle": "Latest writing on Hytale plugin development",
  "viewAll": "View all articles"
}

Style conventions observed:

  • FR hytale.* block currently uses ASCII only (no accents in lines 471-556 — e.g. "Developpement", "Tarifs", "A partir de"). Verify per PATTERNS.md §i18n convention: hytale.* appears to follow ASCII convention. But blog.* block (added Phase 6-02) is accentué ("précédent", "Sommaire", "Bientôt"). CONTEXT D-14 places new keys under hytale.recentArticles.* — planner should decide: either match sibling hytale.* ASCII style OR follow the more recent blog.* accentué style. Given 06-02 SUMMARY states "FR i18n accentué dans bloc blog.*", the hytale.* ASCII may be legacy. Recommendation: use accentué for new keys (consistent with 2026 content direction).
  • JSON structure is flat-nested objects; no trailing commas; double quotes.

Shared Patterns

queryCollection Phase 5 Pitfall Guard

Source: app/pages/blog/index.vue lines 11-20 Apply to: HytaleRecentArticles.vue Rule: ALWAYS branch on isFr.value with literal strings 'blog_fr' / 'blog_en' inside the ternary. Never call queryCollection(someVariable). useAsyncData key must include locale.value; pass { watch: [locale] } to invalidate on language switch.

BlogCard auto-import

Source: app/components/BlogCard.vue (auto-importable via Nuxt components dir) Apply to: HytaleRecentArticles.vue Rule: No explicit import. Use <BlogCard :article variant="compact" />. Article object must include path (used for slug derivation), title, date; description, tags, image, minutes optional.

Locale-aware routing in templates

Source: app/pages/blog/index.vue line 3, 41, 171 Apply to: HytaleRecentArticles.vue (for "view all articles" link) Rule: Use useLocalePath() in script setup, then :to="localePath('/blog')" in template. Do NOT hardcode /fr/blog — let i18n prefix strategy resolve. (Exception: markdown files — hardcode per D-09 since @nuxt/content doesn't wrap Prose links in i18n router automatically unless ProseA is customized.)

Markdown article frontmatter Zod contract

Source: content.config.ts schema blog_fr / blog_en (Phase 5, extended Phase 7 D-14 with optional image) Apply to: All 4 new .md files Rule: Required: title, description, date, tags, draft. Optional: image, updated. Unknown fields are stripped. A broken frontmatter breaks pnpm typecheck / SSR curl.

No Analog Found

None — all 7 files have strong analogs in the current codebase.

Metadata

Analog search scope:

  • app/pages/blog/ (index.vue, [slug].vue)
  • app/pages/hytale.vue
  • app/components/BlogCard.vue
  • content/fr/blog/, content/en/blog/
  • i18n/locales/fr.json, i18n/locales/en.json

Files scanned: 8 Pattern extraction date: 2026-04-22