Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b76208c24 | |||
| 5ec19a5f13 | |||
| f96f25aee9 | |||
| 456f6bfb6f | |||
| bd33e64e1a |
+13
-11
@@ -1,15 +1,16 @@
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.1
|
||||
milestone_name: SEO Hytale — Autorité & Contenu
|
||||
status: In Progress
|
||||
last_updated: "2026-04-22T00:30:00.000Z"
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: Context gathered — ready for /gsd-plan-phase 6
|
||||
last_updated: "2026-04-21T22:41:50.383Z"
|
||||
last_activity: "2026-04-22 — Phase 6 context captured (bd33e64): grille cards, TOC sticky+drawer, surround() prev/next, draft:true pour test article, nav link Blog"
|
||||
progress:
|
||||
total_phases: 4
|
||||
completed_phases: 1
|
||||
total_plans: 2
|
||||
completed_plans: 2
|
||||
percent: 25
|
||||
total_phases: 8
|
||||
completed_phases: 3
|
||||
total_plans: 7
|
||||
completed_plans: 9
|
||||
percent: 100
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -24,8 +25,9 @@ progress:
|
||||
|
||||
Phase: Phase 6 — Blog Pages
|
||||
Plan: —
|
||||
Status: Ready to plan Phase 6
|
||||
Last activity: 2026-04-22 — Phase 5 verified complete (UAT 7/7 pass after 3 post-UAT fixes)
|
||||
Status: Context gathered — ready for /gsd-plan-phase 6
|
||||
Last activity: 2026-04-22 — Phase 6 context captured (bd33e64): grille cards, TOC sticky+drawer, surround() prev/next, draft:true pour test article, nav link Blog
|
||||
Resume file: .planning/phases/06-blog-pages/06-UI-SPEC.md
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Phase 6: Blog Pages - Context
|
||||
|
||||
**Gathered:** 2026-04-22
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Construire les deux pages SSR bilingues qui composent l'expérience blog :
|
||||
|
||||
1. **Listing `/blog`** (nouveau) — grille d'articles publiés avec hero de page, tri chronologique descendant, cards riches (titre, description, date, tags, image cover, reading time).
|
||||
2. **Article `/blog/[slug]`** (amélioration de l'existant phase 5) — ajout d'un chrome complet : header riche (titre, date, tags, cover hero, reading time, breadcrumb visuel), TOC sidebar sticky avec highlight au scroll + drawer mobile, navigation prev/next en bas via cards riches.
|
||||
|
||||
Hors scope de cette phase (→ autres phases) : JSON-LD `Article`, `useSeoMeta` enrichi par article, `og:image` par article, sitemap étendu, `BreadcrumbList` structured data (Phase 7). Articles Hytale réels et cocon sémantique blog ↔ /hytale (Phase 8). Recherche full-text, filtres cliquables, pagination (hors roadmap — backlog).
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Layout listing `/blog`
|
||||
- **D-01:** Format = grille de cards (1 col mobile, 2 col tablet, 3 col desktop). Même pattern visuel que `/projects` (ProjectCard) — cohérence du site.
|
||||
- **D-02:** Infos par card = titre (h2) + description tronquée + date formatée i18n + tags (UBadge, non-cliquables) + image cover (si frontmatter `image`) + reading time ("X min de lecture" / "X min read").
|
||||
- **D-03:** Fallback image cover = aucun (pas d'image si `image` absent du frontmatter). Pas de placeholder branded générique — cards homogènes visuellement même sans image, incite l'auteur à fournir une image pour les articles importants.
|
||||
- **D-04:** Hero section en haut de `/blog` = pattern `/projects` (slogan `// blog`, H1 gradient, subtitle, stats total articles + total tags uniques). Coche avec la charte existante.
|
||||
|
||||
### Chrome article `/blog/[slug]`
|
||||
- **D-05:** TOC = sidebar sticky à droite sur desktop (≥lg), drawer mobile déclenché par bouton "Sommaire" sur <lg. Génération depuis `page.body.toc` (@nuxt/content expose auto les headings). Pas de TOC inline au-dessus du body.
|
||||
- **D-06:** Highlight du heading courant dans la TOC au scroll via `IntersectionObserver` — heading visible surligné `text-brand-500`. Implémentation client-only, hydrate proprement après SSR.
|
||||
- **D-07:** Header article (au-dessus du body markdown) = **tout** le combo : titre H1, date formatée i18n, tags badges (UBadge), image cover hero (si frontmatter `image`, aspect 21/9 ou 16/9 pleine largeur), reading time, breadcrumb visuel (Accueil → Blog → Titre) via UBreadcrumb Nuxt UI. Le JSON-LD BreadcrumbList viendra en Phase 7 — Phase 6 = visuel uniquement.
|
||||
- **D-08:** Largeur max body markdown = `max-w-3xl` (~768px), confirmer l'existant. Wrapper `prose dark:prose-invert` de Phase 5 conservé tel quel.
|
||||
|
||||
### Nav prev/next en bas d'article
|
||||
- **D-09:** Style = cards riches côte à côte (titre de l'article cible + date + icon flèche + label "Article précédent" / "Article suivant"). Fond subtil, hover bg-brand. Pattern docs Nuxt / Stripe.
|
||||
- **D-10:** Pas d'image cover dans ces cards (fallback image non décidé, cohérent avec D-03).
|
||||
- **D-11:** Helper utilisé = `surround()` de @nuxt/content — `queryCollection('blog_fr').path(currentPath).surround()`. Zero logique de tri custom.
|
||||
- **D-12:** Ordre = date frontmatter descendant. Plus récent en haut du listing, "Article précédent" = plus ancien. Nécessite `date` fiable (schéma actuel le requiert déjà).
|
||||
- **D-13:** Edge cases (pas de voisin) = afficher seul le lien existant. Cards alignées, la case absente reste vide. Pas de fallback vers `/blog`.
|
||||
|
||||
### Visibilité blog & article de test
|
||||
- **D-14:** `content/{fr,en}/blog/test-kotlin-syntax.md` = ajouter `draft: true` dans le frontmatter. Schéma `blog_fr`/`blog_en` à étendre dans `content.config.ts` avec `draft: z.boolean().optional().default(false)`. Toutes les queries (listing, surround, [slug] direct) filtrent `draft: false`. Article reste accessible par URL directe pour les tests internes si besoin.
|
||||
- **D-15:** Ajouter un lien "Blog" dans `AppHeader.vue` entre "Hytale" et "Projects". Ordre final nav : Home / Hytale / Blog / Projects / About / Contact / Fiverr. Le blog est un levier SEO — à rendre découvrable prioritairement.
|
||||
- **D-16:** Empty state listing `/blog` (0 article non-draft) = message "Bientôt des articles Hytale" / "Hytale articles coming soon" + icône lucide + CTA `UButton` vers `/contact`. Pattern similaire à `/projects` noResults. Non-bloquant, professionnel.
|
||||
- **D-17:** Structure URLs finale = `/fr/blog`, `/en/blog` (listings), `/fr/blog/[slug]`, `/en/blog/[slug]` (articles). Pas de changement vs Phase 5. `/blog` sans préfixe → 302 via `detectBrowserLanguage` (déjà configuré). Pas d'alias `/articles`.
|
||||
|
||||
### Additions techniques requises
|
||||
- **D-18:** Étendre le schéma Zod dans `content.config.ts` : ajouter `draft: z.boolean().optional().default(false)` sur `blogSchema`.
|
||||
- **D-19:** Créer un composable `useReadingTime(content: string): number` (200 mots/min) ou utiliser `page.body.toc` + word count helper — à décider en research/planning.
|
||||
- **D-20:** Composant unique `BlogCard.vue` réutilisé par le listing ET les cards prev/next (variant prop pour adapter le rendu).
|
||||
- **D-21:** i18n : ajouter les clés `blog.*` (title, subtitle, stats, emptyState, readingTime, prevArticle, nextArticle, toc, backToBlog, breadcrumb) dans `i18n/locales/fr.json` et `en.json`. Ainsi que `nav.blog` + `a11y.blogTocToggle`.
|
||||
|
||||
### Claude's Discretion
|
||||
- Nom exact du composable reading time (`useReadingTime`, `useArticleMeta` …)
|
||||
- Structure interne du composant TOC (`BlogToc.vue`) : sticky container, drawer composition (UDrawer vs custom `<details>`)
|
||||
- Format exact de la date i18n (`Intl.DateTimeFormat` avec locale / style `long`)
|
||||
- Classes Tailwind exactes du hero cover image (aspect-[21/9] vs aspect-[16/9])
|
||||
- Emplacement exact du breadcrumb (au-dessus du titre vs sous la nav vs inside header)
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Requirements & roadmap
|
||||
- `.planning/REQUIREMENTS.md` §BLOG-02, BLOG-03, BLOG-06 — success criteria exacts
|
||||
- `.planning/ROADMAP.md` Phase 6 — goal, dependencies, success criteria
|
||||
|
||||
### Décisions héritées Phase 5 (à respecter tel quel)
|
||||
- `.planning/phases/05-nuxt-content-setup-renderer/05-CONTEXT.md` §D-01..D-04 — prose Tailwind, MDC callouts, structure content/, Shiki github-dark
|
||||
- `.planning/phases/05-nuxt-content-setup-renderer/05-02-SUMMARY.md` — gotchas (Alert SVG inline, ProseImg `<span class="block">`, Shiki single theme, [slug].vue single-segment)
|
||||
- `.planning/STATE.md` §Gotchas Phase 5 — pièges i18n `prefix` strategy + queryCollection littéral obligatoire
|
||||
|
||||
### Stack existant à étendre (NE PAS réécrire)
|
||||
- `content.config.ts` — collections `blog_fr`/`blog_en`, schéma Zod à étendre avec `draft`
|
||||
- `nuxt.config.ts` — config `content`, `i18n` (prefix strategy, baseUrl, detectBrowserLanguage), `routeRules` (aucune sur `/blog/**` — déjà nettoyée phase 5)
|
||||
- `app/pages/blog/[slug].vue` — page actuelle minimale (post-phase 5) à enrichir avec TOC, header riche, prev/next
|
||||
- `app/pages/projects.vue` — référence de pattern pour hero listing + grille + empty state
|
||||
- `app/components/ProjectCard.vue` — référence de pattern pour BlogCard
|
||||
- `app/components/layout/AppHeader.vue` — ajout du lien "Blog"
|
||||
- `app/components/content/*.vue` — MDC components phase 5 (Alert, ProseImg, ProsePre, Columns, Details, Badge, Video, Clear) — réutilisés par ContentRenderer
|
||||
|
||||
### Localisation
|
||||
- `i18n/locales/fr.json` et `i18n/locales/en.json` — ajouter les clés `blog.*`, `nav.blog`, `a11y.blogTocToggle`
|
||||
|
||||
### Documentation externe
|
||||
- `@nuxt/content` v3 docs : https://content.nuxt.com/docs/utils/query-collection — `queryCollection`, `surround()`, `order()`, filter patterns
|
||||
- `@nuxt/content` v3 docs : https://content.nuxt.com/docs/components/content-renderer — page.body.toc structure
|
||||
- `@nuxtjs/i18n` v10 : https://i18n.nuxtjs.org — `useLocalePath`, `useLocaleRoute`, `switchLocalePath`
|
||||
- Nuxt UI v3 : https://ui.nuxt.com/components — UBreadcrumb, UBadge, UDrawer, UButton, UIcon
|
||||
- Nuxt Image : https://image.nuxt.com — NuxtImg avec preset (déjà configuré)
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- **ProjectCard.vue** — pattern de card existant (hover effects, shadow, rounded, dark/light). BlogCard.vue doit s'en inspirer pour cohérence visuelle.
|
||||
- **`useI18n()` + `useLocalePath()`** — pattern déjà établi dans tous les composants pour routage i18n + strings traduits.
|
||||
- **`useSeoMeta()`** — déjà appelé dans `[slug].vue` (minimal phase 5). À enrichir en Phase 7.
|
||||
- **MDC components `app/components/content/*`** — auto-importés par @nuxt/content via `pathPrefix: false`. Utilisables dans les articles markdown et réutilisables dans les templates si pertinent.
|
||||
- **`colorMode()` cookie-based** — SSR-safe. TOC highlight peut s'adapter au dark/light naturellement via Tailwind classes `dark:`.
|
||||
|
||||
### Established Patterns
|
||||
- **Hero listing pattern** (`/projects.vue`) : slogan mono font + H1 gradient + subtitle + stats inline (3 items séparés par divider vertical). Direct transposable à `/blog`.
|
||||
- **Empty state pattern** (`/projects.vue` noResults) : icon lucide dans un round carré + h3 + p + CTA UButton. Réplicable pour blog.
|
||||
- **i18n strategy prefix** : toutes les routes doivent être préfixées (`/fr/*` ou `/en/*`). Pas de route `/blog` directe — 302 via `detectBrowserLanguage`.
|
||||
- **queryCollection littéral** : le Vite extractor de @nuxt/content n'analyse PAS les variables. Toujours `queryCollection('blog_fr')` / `queryCollection('blog_en')` en dur, jamais `queryCollection(variable)`. Conséquence : chaque page blog aura un bloc if/else isFr ↔ isEn.
|
||||
|
||||
### Integration Points
|
||||
- `app/pages/blog/index.vue` (nouveau) → listing SSR
|
||||
- `app/pages/blog/[slug].vue` (existant → à enrichir)
|
||||
- `app/components/BlogCard.vue` (nouveau)
|
||||
- `app/components/BlogToc.vue` (nouveau) — sidebar sticky + drawer mobile
|
||||
- `app/components/BlogPrevNext.vue` (nouveau) — ou intégré dans `[slug].vue`
|
||||
- `app/composables/useReadingTime.ts` (nouveau)
|
||||
- `content.config.ts` (étendre schema avec `draft`)
|
||||
- `app/components/layout/AppHeader.vue` (ajouter lien Blog dans `navLinks`)
|
||||
- `i18n/locales/fr.json` + `en.json` (ajouter clés blog.*, nav.blog, a11y.blogTocToggle)
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Highlight TOC via IntersectionObserver avec threshold `[0, 1]` et `rootMargin` ajusté (ex: `-20% 0px -70% 0px`) pour que l'active switch soit naturel au scroll.
|
||||
- Reading time affiché **cohérent listing ↔ article** : même calcul côté card et côté article header.
|
||||
- `UBreadcrumb` de Nuxt UI v3 avec items `[{ label: t('nav.home'), to: localePath('/') }, { label: t('nav.blog'), to: localePath('/blog') }, { label: page.title }]`.
|
||||
- Empty state CTA : `{ label: t('blog.emptyState.cta'), to: localePath('/contact') }` — réutilise la route contact déjà existante.
|
||||
- Drawer TOC mobile : UDrawer Nuxt UI (side="right") avec bouton trigger `UButton icon="i-lucide-list"` dans le header article sur mobile.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **Filtrage par tag cliquable** (tags clickables → liste filtrée) — nouveau capability, backlog après M1.1.
|
||||
- **Recherche full-text blog** — feature dédiée, backlog.
|
||||
- **Pagination / infinite scroll** — non pertinent tant qu'on a <20 articles. Backlog.
|
||||
- **JSON-LD Article + BreadcrumbList structured data** — Phase 7.
|
||||
- **useSeoMeta enrichi par article (og:image, canonical, dateModified)** — Phase 7.
|
||||
- **Sitemap étendu avec URLs blog** — Phase 7 (auto via `@nuxtjs/sitemap` + @nuxt/content ? à confirmer par researcher).
|
||||
- **OG image generator dynamique** — backlog SEO-06.
|
||||
- **Articles Hytale réels (2+ seed)** — Phase 8.
|
||||
- **Section "Articles récents" sur /hytale** (cocon sémantique) — Phase 8.
|
||||
- **Alias /articles** — scope creep.
|
||||
- **Tags page `/blog/tag/[tag]`** — nouveau capability, backlog.
|
||||
- **RSS feed** — non demandé, backlog.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 06-blog-pages*
|
||||
*Context gathered: 2026-04-22*
|
||||
@@ -0,0 +1,211 @@
|
||||
# Phase 6: Blog Pages - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-04-22
|
||||
**Phase:** 06-blog-pages
|
||||
**Areas discussed:** Layout listing /blog, Chrome article (TOC + header), Nav prev/next article, Visibilité blog & article de test
|
||||
|
||||
---
|
||||
|
||||
## Layout listing /blog
|
||||
|
||||
### Q1 — Format de la liste
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Grille de cards | Pattern /projects, 1/2/3 col responsive | ✓ |
|
||||
| Liste verticale pleine largeur | Cards larges empilées, style éditorial | |
|
||||
| Hybride : hero + grille | Dernier article en hero, suivants en grille | |
|
||||
|
||||
**User's choice:** Grille de cards (recommended)
|
||||
|
||||
### Q2 — Infos par card (multi-select)
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Titre + description + date | Minimum vital | ✓ |
|
||||
| Tags (UBadge) | Visuels seulement, non-cliquables | ✓ |
|
||||
| Image cover (frontmatter `image`) | 16/9 ou 3/2 via NuxtImg | ✓ |
|
||||
| Reading time | Calculé depuis word count | ✓ |
|
||||
|
||||
**User's choice:** Tous les 4
|
||||
|
||||
### Q3 — Fallback image cover
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Pas de fallback, pas d'image | Cards sans image si absent | ✓ |
|
||||
| Gradient branded générique | Bloc coloré avec titre overlay | |
|
||||
| og-image.png branded du site | Réutiliser l'OG existant | |
|
||||
|
||||
**User's choice:** Pas de fallback (recommended)
|
||||
|
||||
### Q4 — Hero section en haut de /blog
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Hero comme /projects | Slogan + H1 + subtitle + stats | ✓ |
|
||||
| Header minimal | H1 + subtitle uniquement | |
|
||||
| Aucun header | Direct sur la grille | |
|
||||
|
||||
**User's choice:** Hero comme /projects (recommended)
|
||||
|
||||
---
|
||||
|
||||
## Chrome article (TOC + header)
|
||||
|
||||
### Q1 — Placement TOC (premier essai)
|
||||
|
||||
**User's choice (free text):** "what table des matières c'est quoi ???" — demande d'explication, pas un choix.
|
||||
|
||||
### Q1bis — Placement TOC après explication vulgarisée
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Sidebar sticky droite + drawer mobile | Pattern blog dev moderne, tutos longs | ✓ |
|
||||
| Inline en haut de l'article, dépliable | Plus simple SSR pur | |
|
||||
| Pas de TOC | Retire la feature (⚠ roadmap criterion) | |
|
||||
|
||||
**User's choice:** Sidebar sticky droite + drawer mobile (recommended)
|
||||
|
||||
**Notes:** l'utilisateur ne connaissait pas le terme "Table des matières". Explication fournie avec exemple concret (tuto Hytale à 5 sections) avant de présenter le choix.
|
||||
|
||||
### Q2 — Header article (multi-select)
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Titre H1 + date + tags badges | Minimum vital | ✓ |
|
||||
| Image cover en hero | Aspect 21/9 si frontmatter `image` | ✓ |
|
||||
| Reading time | Cohérent avec listing | ✓ |
|
||||
| Breadcrumb visuel (Accueil > Blog > Titre) | UBreadcrumb Nuxt UI | ✓ |
|
||||
|
||||
**User's choice:** Tous les 4
|
||||
|
||||
### Q3 — Largeur max body markdown
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| max-w-3xl (~768px) | Déjà en place, standard | ✓ |
|
||||
| max-w-4xl (~896px) | Plus large pour blocs code | |
|
||||
|
||||
**User's choice:** max-w-3xl (recommended)
|
||||
|
||||
### Q4 — Highlight heading courant au scroll
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Oui, IntersectionObserver | Heading visible surligné brand-500 | ✓ |
|
||||
| Non, liens d'ancre statiques | Plus simple, moins JS | |
|
||||
|
||||
**User's choice:** Oui, IntersectionObserver (recommended)
|
||||
|
||||
---
|
||||
|
||||
## Nav prev/next article
|
||||
|
||||
### Q1 — Style des liens prev/next
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Cards riches : titre + date + flèche | Pattern docs Nuxt/Stripe | ✓ |
|
||||
| Liens texte simples | Minimaliste | |
|
||||
| Cards avec image cover | Plus visuel mais cassé si pas d'image | |
|
||||
|
||||
**User's choice:** Cards riches (recommended)
|
||||
|
||||
### Q2 — Comment déterminer prev/next
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| surround() de @nuxt/content | Helper officiel, zero logique custom | ✓ |
|
||||
| Query custom triée par date | Plus verbeux mais contrôle total | |
|
||||
|
||||
**User's choice:** surround() (recommended)
|
||||
|
||||
### Q3 — Ordre des articles
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Date frontmatter descendant | Plus récent en premier | ✓ |
|
||||
| Alphabetic par titre | Style docs/références | |
|
||||
| Ordre de fichier | Alphabétique slug | |
|
||||
|
||||
**User's choice:** Date descendant (recommended)
|
||||
|
||||
### Q4 — Edge cases (pas de voisin)
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Afficher seul le lien existant | Case vide à droite ou gauche | ✓ |
|
||||
| Lien de fallback vers /blog | Toujours 2 actions | |
|
||||
| Cacher la section si 1 seul article | Pas de section du tout | |
|
||||
|
||||
**User's choice:** Afficher seul le lien existant (recommended)
|
||||
|
||||
---
|
||||
|
||||
## Visibilité blog & article de test
|
||||
|
||||
### Q1 — Que faire de `test-kotlin-syntax.md`
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Masquer via `draft: true` | Pattern standard @nuxt/content | ✓ |
|
||||
| Déplacer vers `content/_drafts/` | Ignoré complètement, 404 URL | |
|
||||
| Le garder visible dans /blog | Risque blog vide/démo | |
|
||||
| Renommer en article réel "Guide Markdown" | Contenu permanent | |
|
||||
|
||||
**User's choice:** draft: true (recommended)
|
||||
|
||||
### Q2 — Lien "Blog" dans AppHeader.vue
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Oui, entre 'Hytale' et 'Projects' | Blog = levier SEO majeur | ✓ |
|
||||
| Oui, en fin de nav (après Fiverr) | Moins proéminent | |
|
||||
| Non, accessible via footer uniquement | Nav épurée | |
|
||||
|
||||
**User's choice:** Oui, entre Hytale et Projects (recommended)
|
||||
|
||||
### Q3 — Empty state listing /blog
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| "Articles à venir" + CTA contact | Pattern /projects noResults | ✓ |
|
||||
| Rediriger /blog vers / si vide | Cache le blog | |
|
||||
| Page 404 si vide | Dur sur SEO | |
|
||||
|
||||
**User's choice:** "Articles à venir" + CTA contact (recommended)
|
||||
|
||||
### Q4 — Structure URLs finale
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| /fr/blog + /en/blog + slugs, pas d'alias | Pattern phase 5, pas de changement | ✓ |
|
||||
| Ajouter /fr/articles alias | Scope creep | |
|
||||
|
||||
**User's choice:** Pattern phase 5 (recommended)
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Nom exact du composable reading time
|
||||
- Structure interne du composant TOC (UDrawer vs `<details>`)
|
||||
- Format exact de la date i18n
|
||||
- Classes Tailwind exactes du hero cover (21/9 vs 16/9)
|
||||
- Emplacement exact du breadcrumb
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Filtrage par tag cliquable (backlog)
|
||||
- Recherche full-text blog (backlog)
|
||||
- Pagination / infinite scroll (backlog)
|
||||
- JSON-LD Article + BreadcrumbList (Phase 7)
|
||||
- useSeoMeta enrichi par article (Phase 7)
|
||||
- Sitemap étendu (Phase 7)
|
||||
- OG image generator (backlog SEO-06)
|
||||
- Articles Hytale réels + cocon /hytale (Phase 8)
|
||||
- Alias /articles, tags page, RSS feed (backlog)
|
||||
@@ -0,0 +1,403 @@
|
||||
---
|
||||
phase: 6
|
||||
slug: blog-pages
|
||||
status: approved
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-04-22
|
||||
reviewed_at: 2026-04-22
|
||||
---
|
||||
|
||||
# Phase 6 — UI Design Contract
|
||||
## Blog Pages — Listing `/blog` + Article `/blog/[slug]`
|
||||
|
||||
> Contrat visuel et d'interaction pour les deux pages blog SSR bilingues.
|
||||
> Hérite des tokens de Phase 5 (prose, Shiki, MDC). Génère deux nouvelles pages + trois nouveaux composants.
|
||||
> Généré par gsd-ui-researcher — à valider par gsd-ui-checker.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value | Source |
|
||||
|----------|-------|--------|
|
||||
| Tool | Nuxt UI v3 (pas de shadcn) | `nuxt.config.ts` + 05-UI-SPEC |
|
||||
| Preset | not applicable | — |
|
||||
| Component library | Nuxt UI v3 (`@nuxt/ui`) | CONTEXT D-05/D-07 (UDrawer, UBreadcrumb, UBadge, UButton, UIcon) |
|
||||
| Icon library | Lucide via Nuxt UI (`i-lucide-*`) | `AppHeader.vue`, `ProjectCard.vue`, `projects.vue` usage existant |
|
||||
| Font | Hérité (system-ui via Nuxt UI) + mono pour sloganeuse `// blog` | `app/assets/css/main.css` |
|
||||
| CSS | Tailwind v4 + `@theme` tokens `--color-brand-*` | `app/assets/css/main.css` |
|
||||
| Typography plugin | `@tailwindcss/typography` (hérité Phase 5) | `main.css` + 05-UI-SPEC |
|
||||
| Theme | `colorMode` cookie-based (SSR-safe), dark default | `nuxt.config.ts` |
|
||||
|
||||
> La shadcn gate ne s'applique pas — stack Nuxt UI. La vetting gate registry tiers ne s'applique pas non plus.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Échelle 8-points standard (multiples de 4). Tailwind v4 fournit ces valeurs via les utilitaires `p-*`, `m-*`, `gap-*`.
|
||||
|
||||
| Token | Value | Usage dans cette phase |
|
||||
|-------|-------|------------------------|
|
||||
| xs | 4px | Gap icône/texte dans meta article (date + reading time) |
|
||||
| sm | 8px | Gap entre badges UBadge dans une rangée de tags |
|
||||
| md | 16px | Padding interne des cards (entêtes, content spacing) |
|
||||
| lg | 24px | Gap entre cards de la grille, padding BlogCard `p-5 sm:p-6` |
|
||||
| xl | 32px | Espace vertical entre header article et body markdown |
|
||||
| 2xl | 48px | Marge verticale de la section hero (pt-20 pb-16 pattern projects) |
|
||||
| 3xl | 64px | `pt-20 pb-16` du hero listing, espace entre sections de page |
|
||||
|
||||
Exceptions :
|
||||
- Section listing content `py-16 md:py-20` (64→80px responsive) — conforme pattern `/projects`
|
||||
- Sticky TOC offset top : `top-24` (96px = header 64px + 32px breathing) — multiple de 8, conforme
|
||||
- Cover hero article `aspect-[21/9]` — ratio uniquement, pas une valeur de spacing
|
||||
- Grille listing `gap-5 lg:gap-6` (20→24px) — `gap-5` = 20px est hors échelle stricte 8-points ; aligné avec le pattern existant `/projects` pour cohérence visuelle ; le checker doit accepter cette exception documentée
|
||||
- Prev/Next cards `p-5` (20px) — idem exception alignée sur l'existant
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
Le corps de l'article reste géré par `@tailwindcss/typography` via `prose dark:prose-invert` (hérité Phase 5, inchangé).
|
||||
Le chrome de la page (hero, cards, header article, TOC, prev/next) utilise les valeurs ci-dessous.
|
||||
|
||||
| Role | Size | Weight | Line Height | Usage |
|
||||
|------|------|--------|-------------|-------|
|
||||
| Display (hero H1) | 36→48→60px (`text-4xl sm:text-5xl lg:text-6xl`) | 700 (bold) | 1.1 (`leading-tight` implicite) | H1 gradient de la section hero `/blog` |
|
||||
| Heading (card title, article H1) | 18→20px (`text-lg` card / `text-3xl sm:text-4xl` article header) | 700 (bold) | 1.2 | Titres BlogCard + titre article `[slug]` |
|
||||
| Body (subtitle, description) | 16→20px (`text-lg sm:text-xl` subtitle / `text-sm` card desc) | 400 (regular) | 1.5 (`leading-relaxed`) | Subtitle hero, descriptions cards |
|
||||
| Meta (date, reading time, slogan) | 12→14px (`text-xs`/`text-sm`) | 400 (regular) | 1.5 | Date ISO mono, reading time, slogan `// blog` |
|
||||
|
||||
Règles Phase 6 :
|
||||
- **2 poids uniquement** : regular (400) + bold (700). Pas de medium/semibold pour éviter la pollution typographique.
|
||||
- **Mono réservée** : classe `font-mono` uniquement pour le slogan `// blog` et la date `datetime` attribut dans les cards (cohérence avec `ProjectCard.vue`).
|
||||
- **Gradient text** : le H1 du hero hérite du gradient `from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500` — identique `projects.vue` pour cohérence.
|
||||
- **Body article** (prose) : 16px / 400 / 1.75 — inchangé Phase 5.
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
Dark mode par défaut, light mode synchronisé via cookie. Le palette `--color-brand-*` est déjà déclaré dans `main.css`.
|
||||
|
||||
| Role | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| Dominant (60%) | `bg-white` light / `bg-gray-950` dark (Tailwind) | Fond page, body article, surface hero dégradée |
|
||||
| Secondary (30%) | `bg-gray-50/80` light / `bg-gray-900/40` dark — cards/panels `bg-white/80` / `bg-gray-900/60` | Fond hero section, fond BlogCard, fond prev/next card, fond TOC sidebar, fond drawer TOC |
|
||||
| Accent (10%) | `--color-brand-500: #85cb85` (light) / `--color-brand-400: #a3d6a3` (dark) | Liens prose, slogan `// blog`, hover border cards, TOC highlight heading actif, CTA empty state (solid), gradient stats numbers |
|
||||
| Destructive | `color-error` (Nuxt UI token, rouge) | Aucun usage dans cette phase (callouts `danger` déjà réservés Phase 5) |
|
||||
|
||||
Accent `brand-*` réservé EXCLUSIVEMENT à :
|
||||
1. Slogan mono `// blog` (hero top) — `text-brand-500 dark:text-brand-400`
|
||||
2. Gradient numérique des stats dans le hero (`from-brand-400 to-brand-600`) — identique pattern `/projects`
|
||||
3. Hover border de BlogCard (`hover:border-brand-500/40`)
|
||||
4. Hover title de BlogCard (`group-hover:text-brand-600 dark:group-hover:text-brand-400`)
|
||||
5. Shadow hover de BlogCard (`hover:shadow-brand-500/10`)
|
||||
6. Heading actif courant dans la TOC au scroll (`text-brand-500 dark:text-brand-400`) — IntersectionObserver
|
||||
7. CTA empty state UButton (`color="primary"` mappé sur brand par Nuxt UI)
|
||||
8. Liens prose (hérité Phase 5 — inchangé)
|
||||
9. Icônes arrow de prev/next cards au hover (`group-hover:text-brand-500`)
|
||||
|
||||
Accent INTERDIT sur :
|
||||
- Date, reading time, meta info (gris neutre)
|
||||
- Tags UBadge (doivent rester en variant `subtle` color `neutral` ou `primary` une seule teinte — voir Registry)
|
||||
- Breadcrumb inactif (gris)
|
||||
- Corps de texte général
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
Tous les textes passent par `useI18n()` — clés déclarées dans `i18n/locales/{fr,en}.json`.
|
||||
Les clés `blog.*`, `nav.blog`, `a11y.blogTocToggle` sont déjà listées dans CONTEXT D-21.
|
||||
|
||||
### Hero listing `/blog`
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| Slogan (mono) | `// blog` | `// blog` | littéral (pas d'i18n) |
|
||||
| H1 | Blog | Blog | `blog.title` |
|
||||
| Subtitle | Articles techniques, retours d'expérience et guides pratiques sur le développement de plugins Hytale et l'écosystème web. | Technical articles, experience feedback and practical guides on Hytale plugin development and the web ecosystem. | `blog.subtitle` |
|
||||
| Stat 1 label | Articles | Articles | `blog.stats.articles` |
|
||||
| Stat 2 label | Tags | Tags | `blog.stats.tags` |
|
||||
| Stat 3 label | Langues | Languages | `blog.stats.languages` |
|
||||
|
||||
### BlogCard (listing + prev/next)
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| Reading time | `{n} min de lecture` | `{n} min read` | `blog.readingTime` (avec variable `{minutes}`) |
|
||||
| Label prev article | Article précédent | Previous article | `blog.prevArticle` |
|
||||
| Label next article | Article suivant | Next article | `blog.nextArticle` |
|
||||
|
||||
### Article `/blog/[slug]` chrome
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| Breadcrumb home | Accueil | Home | `nav.home` (existant) |
|
||||
| Breadcrumb blog | Blog | Blog | `nav.blog` (nouveau) |
|
||||
| TOC title | Sommaire | Table of contents | `blog.toc.title` |
|
||||
| Back to blog | Retour au blog | Back to blog | `blog.backToBlog` |
|
||||
|
||||
### Empty state listing (0 articles non-draft)
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| Icon | `i-lucide-book-open` | `i-lucide-book-open` | littéral |
|
||||
| Heading | Bientôt des articles Hytale | Hytale articles coming soon | `blog.emptyState.title` |
|
||||
| Body | Le blog se prépare. Les premiers articles sur le développement de plugins Hytale arrivent bientôt. | The blog is being prepared. The first articles on Hytale plugin development are coming soon. | `blog.emptyState.description` |
|
||||
| CTA label (primary) | Me contacter | Contact me | `blog.emptyState.cta` |
|
||||
| CTA target | `/contact` via `localePath` | `/contact` via `localePath` | — |
|
||||
| CTA icon | `i-lucide-mail` | `i-lucide-mail` | littéral |
|
||||
|
||||
### Error state (404 article introuvable)
|
||||
|
||||
Utilise `createError({ statusCode: 404 })` côté serveur → rendu via `error.vue` du layout global. Cette phase **n'ajoute pas** d'UI d'erreur custom — l'erreur 404 existante du projet s'applique. Aucune autre erreur visible prévue.
|
||||
|
||||
### Accessibility copy
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| TOC toggle button aria-label | Afficher le sommaire | Show table of contents | `a11y.blogTocToggle` |
|
||||
| Prev card aria-label | Article précédent : {titre} | Previous article: {title} | `a11y.blogPrev` (avec `{title}`) |
|
||||
| Next card aria-label | Article suivant : {titre} | Next article: {title} | `a11y.blogNext` (avec `{title}`) |
|
||||
|
||||
### Nav link AppHeader
|
||||
|
||||
| Element | FR | EN | i18n key |
|
||||
|---------|----|----|----------|
|
||||
| Nav label Blog | Blog | Blog | `nav.blog` |
|
||||
|
||||
Position finale AppHeader : Home / Hytale / **Blog** / Projects / About / Contact / Fiverr (CONTEXT D-15).
|
||||
|
||||
### Destructive actions
|
||||
|
||||
Aucune action destructive dans cette phase (lecture seule, pas de suppression, pas de formulaire).
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
Tous nouveaux composants — aucun shadcn, 100% Tailwind + Nuxt UI.
|
||||
|
||||
| Composant | Chemin | Rôle | Base technique |
|
||||
|-----------|--------|------|----------------|
|
||||
| `BlogCard.vue` | `app/components/BlogCard.vue` | Card article réutilisable (listing + prev/next) | Tailwind + NuxtImg + UBadge, variant prop `default` / `compact` |
|
||||
| `BlogToc.vue` | `app/components/BlogToc.vue` | Sommaire sticky desktop + drawer mobile | UDrawer (mobile) + sticky div (desktop) + IntersectionObserver |
|
||||
| `BlogPrevNext.vue` | `app/components/BlogPrevNext.vue` | Navigation prev/next cards | 2× BlogCard variant `compact` + UIcon flèches |
|
||||
| Page listing | `app/pages/blog/index.vue` (NEW) | Hero + grille + empty state | queryCollection(`blog_fr`\|`blog_en`) + BlogCard |
|
||||
| Page article | `app/pages/blog/[slug].vue` (ENRICH) | Breadcrumb + header + body + TOC + prev/next | Existant Phase 5 enrichi |
|
||||
|
||||
### Composants Nuxt UI consommés
|
||||
|
||||
| Composant | Variant / Props | Usage |
|
||||
|-----------|-----------------|-------|
|
||||
| `UBadge` | `color="primary"` `variant="subtle"` | Tags dans BlogCard + header article (non-cliquables) |
|
||||
| `UBreadcrumb` | Items array avec `label` + `to` | Breadcrumb visuel en haut de l'article (D-07) |
|
||||
| `UDrawer` | `side="right"` | TOC mobile (<lg) déclenchée par UButton `i-lucide-list` |
|
||||
| `UButton` | `variant="solid" color="primary"` | CTA empty state (`Me contacter`) |
|
||||
| `UButton` | `variant="ghost" color="neutral" icon="i-lucide-list"` | Trigger drawer TOC mobile |
|
||||
| `UButton` | `variant="ghost" icon="i-lucide-arrow-left"` | Lien "Retour au blog" (optionnel, si budget) |
|
||||
| `UIcon` | `i-lucide-arrow-right` / `i-lucide-arrow-left` | Flèches prev/next cards |
|
||||
| `UIcon` | `i-lucide-book-open` | Icon empty state |
|
||||
| `UIcon` | `i-lucide-clock` | Icon reading time (optionnel, inline avec texte) |
|
||||
| `UIcon` | `i-lucide-calendar` | Icon date (optionnel, inline avec texte) |
|
||||
| `UIcon` | `i-lucide-mail` | Icon CTA empty state |
|
||||
| `NuxtImg` | `loading="lazy"` `format="webp"` | Image cover card + hero article (si frontmatter.image présent) |
|
||||
| `NuxtLink` | `:to="localePath('/blog/' + slug)"` | Navigation SPA vers article |
|
||||
| `ContentRenderer` | `:value="page"` | Rendu markdown article (hérité Phase 5, inchangé) |
|
||||
|
||||
### BlogCard variant contract
|
||||
|
||||
```
|
||||
variant="default" (listing)
|
||||
├── NuxtImg cover (si image) — aspect 16/9, rounded-t-2xl
|
||||
├── Padding p-5 sm:p-6, flex-col gap-3
|
||||
├── Header row : UBadge tag[0] (primary subtle) + <time> date mono text-xs
|
||||
├── Title h2 text-lg font-bold, group-hover:text-brand-600
|
||||
├── Description text-sm line-clamp-2 leading-relaxed
|
||||
├── Footer row : reading time text-xs gray-400 + tags supplémentaires (+N) pills neutres
|
||||
└── NuxtLink absolute inset-0 (SEO + a11y)
|
||||
|
||||
variant="compact" (prev/next)
|
||||
├── Pas d'image cover (D-10)
|
||||
├── Padding p-5, flex-col gap-2
|
||||
├── Label row : UIcon arrow-left|arrow-right + "Article précédent|suivant" text-xs uppercase tracking-wider gray-500
|
||||
├── Title h3 text-base font-bold, group-hover:text-brand-500
|
||||
├── Date <time> text-xs mono gray-400
|
||||
└── NuxtLink absolute inset-0
|
||||
```
|
||||
|
||||
### BlogToc contract
|
||||
|
||||
**Desktop (≥ lg — 1024px)** :
|
||||
- `<aside>` avec `position: sticky; top: 24 (96px)` — offset header h-16 + breathing
|
||||
- Largeur `w-64` (256px) dans une grille `lg:grid-cols-[1fr_16rem] gap-12`
|
||||
- Liste `<ol>` flat ou nested selon `page.body.toc` (niveau h2/h3 uniquement, pas h4+)
|
||||
- Chaque item : `<a href="#id">` avec classe conditionnelle `text-brand-500` si actif, `text-gray-500 hover:text-gray-900` sinon
|
||||
- Titre de la TOC `Sommaire` / `Table of contents` — `text-sm font-bold uppercase tracking-wider text-gray-500` en haut
|
||||
- Indentation nested h3 : `pl-4` sous leur h2 parent
|
||||
|
||||
**Mobile (< lg)** :
|
||||
- `<aside>` hidden
|
||||
- UButton trigger en haut du header article : `<UButton icon="i-lucide-list" variant="ghost">{{ t('blog.toc.title') }}</UButton>`
|
||||
- `<UDrawer side="right">` avec header `{ t('blog.toc.title') }` + body identique à la liste desktop
|
||||
- Fermeture au clic sur un item (navigation ancrée)
|
||||
|
||||
**IntersectionObserver (client-only via `onMounted`)** :
|
||||
- `rootMargin: '-20% 0px -70% 0px'`
|
||||
- `threshold: 0`
|
||||
- Observer les headings h2/h3 de l'article
|
||||
- Met à jour une `ref<string | null>(activeId)` qui pilote la classe active
|
||||
- Cleanup dans `onBeforeUnmount`
|
||||
|
||||
### Hero section `/blog` — contract exact
|
||||
|
||||
Structure identique `app/pages/projects.vue` lignes 56-83 (décision D-04) :
|
||||
```
|
||||
<section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<!-- Background gradient blur (identical pattern) -->
|
||||
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" />
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl" />
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto text-center">
|
||||
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// blog</span>
|
||||
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mt-3 mb-5 bg-gradient-to-r ...">{{ t('blog.title') }}</h1>
|
||||
<p class="text-lg sm:text-xl text-gray-500 ... max-w-2xl mx-auto">{{ t('blog.subtitle') }}</p>
|
||||
|
||||
<!-- Stats 3× with dividers identical pattern -->
|
||||
<div class="flex justify-center gap-8 sm:gap-12 mt-12"> ... </div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
Stats calculés :
|
||||
- Stat 1 : `articles.length` (articles non-draft)
|
||||
- Stat 2 : `uniqueTags.length` (nouveau — Set depuis tous les articles)
|
||||
- Stat 3 : `2` (FR + EN — valeur fixe)
|
||||
|
||||
### Article header contract (au-dessus du body `prose`)
|
||||
|
||||
Ordre de haut en bas dans `app/pages/blog/[slug].vue` :
|
||||
|
||||
1. **UBreadcrumb** (Accueil → Blog → Titre) — au-dessus du H1, `text-sm`, `mb-6`
|
||||
2. **H1 article** (titre frontmatter) — `text-3xl sm:text-4xl font-bold mb-4`
|
||||
3. **Meta row** (flex inline) : date i18n formatée long + `·` + reading time + UButton trigger TOC (mobile only)
|
||||
4. **Tags row** (si `tags` frontmatter) : flex wrap gap-2 de UBadge variant subtle color primary
|
||||
5. **Cover hero image** (si `image` frontmatter) : NuxtImg `aspect-[21/9] w-full object-cover rounded-2xl mt-8 mb-12`
|
||||
6. **Séparateur implicite** : la marge du cover (ou `mb-12` si pas de cover) sert de séparation avant le body
|
||||
7. **Body markdown** : `<article class="prose dark:prose-invert max-w-none">` inchangé Phase 5
|
||||
8. **BlogPrevNext** : composant en bas, `mt-16 grid md:grid-cols-2 gap-5`
|
||||
|
||||
### Layout responsive article
|
||||
|
||||
```
|
||||
< lg (mobile/tablet) :
|
||||
max-w-3xl mx-auto px-4 py-12 (existant Phase 5)
|
||||
TOC dans UDrawer, bouton trigger inline dans meta row
|
||||
|
||||
≥ lg (desktop) :
|
||||
max-w-7xl mx-auto px-4 py-12
|
||||
grid grid-cols-[1fr_16rem] gap-12
|
||||
colonne gauche : article prose (max-w-3xl mx-auto pour rester lisible)
|
||||
colonne droite : aside sticky TOC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interaction Contract
|
||||
|
||||
| Interaction | Déclencheur | Effet | A11y |
|
||||
|-------------|-------------|-------|------|
|
||||
| Click card listing | Clic sur BlogCard | Navigation `/blog/[slug]` via NuxtLink `localePath` | NuxtLink absolute inset-0 avec `aria-label="{title} - {description}"` |
|
||||
| Click TOC item (desktop) | Clic sur `<a href="#id">` | Scroll natif vers heading (offset via `scroll-margin-top: 5rem` hérité Phase 5) | `<a>` native, gère focus |
|
||||
| Click TOC item (mobile) | Clic dans drawer | Scroll ancré + ferme le drawer (`open = false`) | Drawer close + focus retour sur trigger button |
|
||||
| Toggle drawer TOC | Clic bouton `i-lucide-list` | Ouvre UDrawer side="right" | `aria-label` via `t('a11y.blogTocToggle')`, `aria-expanded` géré par UDrawer |
|
||||
| Hover card | Hover BlogCard | border-brand-500/40 + shadow-xl + translate -y-1.5 (pattern ProjectCard) | Transition `duration-300`, respecte `prefers-reduced-motion` |
|
||||
| Hover card title | Hover | group-hover:text-brand-600 dark:group-hover:text-brand-400 | Effet visuel uniquement |
|
||||
| Scroll page article | Scroll | IntersectionObserver met à jour TOC active heading | Pas de changement de focus ; mise à jour visuelle uniquement |
|
||||
| CTA empty state | Clic "Me contacter" | Navigation `/contact` via `localePath` | UButton natif |
|
||||
| Prev/next card hover | Hover BlogCard variant=compact | border + shadow + flèche icon `group-hover:translate-x-1` (next) ou `-x-1` (prev) | Transition `duration-200` |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| Nuxt UI officiel | `UBadge`, `UBreadcrumb`, `UDrawer`, `UButton`, `UIcon` | Non requis — composants officiels `@nuxt/ui` |
|
||||
| @nuxt/content officiel | `ContentRenderer`, `queryCollection`, `surround()` | Non requis — module officiel Nuxt |
|
||||
| @nuxt/image officiel | `NuxtImg` | Non requis — module officiel Nuxt |
|
||||
| @nuxtjs/i18n officiel | `useI18n`, `useLocalePath` | Non requis — module officiel Nuxt |
|
||||
| Tiers | aucun | Non applicable |
|
||||
|
||||
> Ce projet utilise Nuxt UI v3, pas shadcn. Aucun composant tiers hors écosystème Nuxt officiel. La vetting gate ne s'applique pas.
|
||||
|
||||
---
|
||||
|
||||
## i18n Keys à créer (contrat avec planner)
|
||||
|
||||
Ajouts dans `i18n/locales/fr.json` et `i18n/locales/en.json` :
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"nav": {
|
||||
"blog": "Blog" // nouveau
|
||||
},
|
||||
"a11y": {
|
||||
"blogTocToggle": "Afficher le sommaire", // FR
|
||||
"blogPrev": "Article précédent : {title}",
|
||||
"blogNext": "Article suivant : {title}"
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"subtitle": "Articles techniques, retours d'expérience et guides pratiques sur le développement de plugins Hytale et l'écosystème web.",
|
||||
"stats": {
|
||||
"articles": "Articles",
|
||||
"tags": "Tags",
|
||||
"languages": "Langues"
|
||||
},
|
||||
"readingTime": "{minutes} min de lecture",
|
||||
"prevArticle": "Article précédent",
|
||||
"nextArticle": "Article suivant",
|
||||
"backToBlog": "Retour au blog",
|
||||
"toc": {
|
||||
"title": "Sommaire"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "Bientôt des articles Hytale",
|
||||
"description": "Le blog se prépare. Les premiers articles sur le développement de plugins Hytale arrivent bientôt.",
|
||||
"cta": "Me contacter"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"home": "Accueil",
|
||||
"blog": "Blog"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
EN : mêmes clés avec traductions correspondantes listées dans la section Copywriting.
|
||||
|
||||
---
|
||||
|
||||
## Dépendances héritées (Phase 5 — NE PAS modifier)
|
||||
|
||||
- `app/assets/css/main.css` : `@plugin "@tailwindcss/typography"` + `--color-brand-*` + `scroll-margin-top: 5rem`
|
||||
- `content.config.ts` : schema Zod `blog_fr` + `blog_en` (à étendre avec `draft` — voir CONTEXT D-18, couvert par planner)
|
||||
- `app/components/content/*.vue` : MDC ProseImg, Alert, ProsePre, etc. — utilisés par `<ContentRenderer>`, inchangés
|
||||
- `nuxt.config.ts` : `i18n` strategy `prefix`, `detectBrowserLanguage`, `colorMode` cookie, `image` preset — inchangés
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [ ] Dimension 1 Copywriting: PASS
|
||||
- [ ] Dimension 2 Visuals: PASS
|
||||
- [ ] Dimension 3 Color: PASS
|
||||
- [ ] Dimension 4 Typography: PASS
|
||||
- [ ] Dimension 5 Spacing: PASS
|
||||
- [ ] Dimension 6 Registry Safety: PASS
|
||||
|
||||
**Approval:** pending
|
||||
Reference in New Issue
Block a user