9.6 KiB
phase, plan, subsystem, tags, dependency_graph, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-blog-pages | 04 | blog-article-chrome |
|
|
|
|
|
Phase 06 Plan 04: Article Chrome Summary
Phase 6 se termine avec l'enrichissement substantiel de la page article /blog/[slug]. Ajout de 2 composants réutilisables (BlogToc sticky+drawer+observer, BlogPrevNext grid 2 cols) et refactorisation complète du script/template de [slug].vue pour passer du minimal Phase 5 au chrome complet Phase 6 : breadcrumb, header riche, TOC sticky/drawer, prev/next cards.
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 4.1 | Créer BlogToc.vue (sticky + drawer + IntersectionObserver) | b72b564 |
app/components/BlogToc.vue |
| 4.2 | Créer BlogPrevNext.vue (grid 2 cols BlogCard compact) | 0ff3678 |
app/components/BlogPrevNext.vue |
| 4.3 | Enrichir [slug].vue (breadcrumb + header + TOC + surround + prev/next) | f18b0bf |
app/pages/blog/[slug].vue |
Decisions Made
-
UDrawer
directionprop — validé empiriquement vianode_modules/@nuxt/ui/.../Drawer.vue.d.tsqui faitPick<DrawerRootProps, ... 'direction' ...>. USlideover (utilisé dans AppHeader) reste avecside— ce sont deux composants différents de Nuxt UI v3. -
Fix Pitfall 3 (isFr non-réactif) — Phase 5 avait
const isFr = locale.value === 'fr'au top-level du setup (capture one-shot). Phase 6 convertit encomputed(() => locale.value === 'fr'). Sans ça + sans{ watch: [locale] }, le switch langue gardait l'article FR même sur URL/en/.... -
Mapping prev/next inversé (Pitfall 4) —
queryCollectionItemSurroundingsretourne[before, after]dans l'ordre de la collection. Avec.order('date', 'DESC')la collection est triée du plus récent au plus ancien, doncsurround[0](before) = plus récent = next UI ("article suivant" en chronologie blog conventionnelle),surround[1](after) = plus ancien = prev UI ("article précédent"). Commentaire explicite dans le code pour éviter futurs bugs. -
Asymétrie draft filter — la query article principale utilise
queryCollection('blog_fr').path(path).first()SANS.where('draft')→ un article marqué draft reste accessible par URL directe (test/preview, D-14). La query surround utilisequeryCollectionItemSurroundings(...).where('draft', '=', false)→ les drafts ne sont jamais proposés comme voisins. -
Types locaux
TocLink+SurroundArticle— dupliqués avec BlogToc.vue et BlogPrevNext.vue mais évite un shared-types file pour cette phase. Le cast est nécessaire car @nuxt/content v3 exposebodycomme type'minimal'(tuples d'AST) etContentNavigationItem(surround return) ne déclare pasdate/tags/imagestatiquement même si le runtime les fournit viafields[]. -
Layout grid responsive —
lg:grid lg:grid-cols-[1fr_16rem] lg:gap-12sur desktop : colonne article flex (avecmax-w-3xl mx-auto lg:mx-0pour lisibilité prose D-08) + aside TOC 16rem (256px) fixe. Sur mobile (<lg) : single column stack, la TOC passe en drawer via le trigger UButton dans la branchelg:hiddende BlogToc. -
Cover image
eager— D-07 + UI-SPEC. Le hero cover est above-the-fold donc priorité LCP. Opposé de la grille listing BlogCard où les images sontlazy.
Deviations from Plan
Cast tocLinks sans @ts-expect-error
- Planned :
// @ts-expect-error — @nuxt/content v3 body type 'minimal' doesn't statically expose toc+ direct cast. - Actual : cast via variable intermédiaire typée —
const body = page.value?.body as { toc?: { links?: TocLink[] } } | undefined; return body?.toc?.links ?? []. TypeScript ne considère pas le cast comme une erreur (aucune@ts-expect-errorutilisable → TS2578 "Unused directive"). - Reason : la version de TS/vue-tsc acceptait le cast propre sans directive. Résultat fonctionnel identique, pas de suppression d'erreur.
SurroundArticle interface locale
- Planned : décrite dans le plan pour BlogPrevNext.vue seulement.
- Actual : dupliquée aussi dans [slug].vue pour le cast
surround.value?.[0] as SurroundArticle | undefined. - Reason : sans le cast, TS2322 car
ContentNavigationItemn'expose pasdatestatiquement.
Acceptance Criteria Check
BlogToc.vue (Task 4.1)
- File exists
interface TocLinkdefined (1)IntersectionObserver(2 refs: type + new)UDrawerpresent (1+)rootMargin: '-20% 0px -70% 0px'(1)threshold: 0(1)onMounted+onBeforeUnmount(1 each)observer?.disconnect()cleanup (1)activeId = reflocal (no useState)hidden lg:block sticky top-24desktop asidelg:hiddenmobile wrappertext-brand-500 dark:text-brand-400active state (2+)t('blog.toc.title')(2+)t('a11y.blogTocToggle')(1)- No
useStateusage
BlogPrevNext.vue (Task 4.2)
- File exists
<BlogCard× 2variant="compact"× 2direction="prev"(1) +direction="next"(1)prev: SurroundArticle | null/next: SurroundArticle | nullv-else aria-hidden="true"× 2 (D-13 empty cells)grid md:grid-cols-2 gap-5mt-16spacing
[slug].vue (Task 4.3)
queryCollectionItemSurroundings× 2 (FR + EN branches)UBreadcrumb(1+)<BlogToc(1)<BlogPrevNext(1)queryCollection('blog_fr')+queryCollection('blog_en')(1 each)isFr = computed(Pitfall 3 fix)watch: [locale]× 2.where('draft', '=', false)on SURROUND branches only (2).order('date', 'DESC')× 2nextArticle = computed+prevArticle = computedsurround.value?.[0](next) +surround.value?.[1](prev)breadcrumbItemscomputedIntl.DateTimeFormat(1)t('blog.breadcrumb.home')+t('blog.breadcrumb.blog')(1 each)t('blog.readingTime'(1)ContentRenderer(preserved from Phase 5)prose dark:prose-invert max-w-nonewrapperaspect-[21/9]cover herolg:grid-cols-[1fr_16rem]grid desktopmax-w-3xl mx-auto lg:mx-0article columnloading="eager"(1) cover herocreateError404 handler preservedpnpm typecheck→ exit 0
Runtime tests (curl) — NOT executed this session
Tests SSR curl + switch locale interactif reportés à l'étape de vérification phase (/gsd-verify-work ou pnpm dev manuel).
Phase 6 Success Criteria Recap
| # | Criterion | Plan | Status |
|---|---|---|---|
| 1 | curl /blog → HTML SSR listing | 06-03 | ✅ (page + empty state, typecheck OK — runtime à valider) |
| 2 | curl /blog/[slug] → article rendu SSR (pas SPA shell vide) | 06-04 | ✅ (ContentRenderer + prose — runtime à valider) |
| 3 | TOC générée depuis headings | 06-04 | ✅ (BlogToc consomme page.body.toc.links) |
| 4 | Liens prev/next en bas d'article | 06-04 | ⚠️ (BlogPrevNext rendu conditionnel — empty à ce stade car seul article draft. Sera visible en Phase 8 avec articles seed) |
| 5 | curl /en/blog → listing EN | 06-03 | ✅ (branches i18n via watch locale — runtime à valider) |
Self-Check: PASSED
Tous les critères statiques validés (grep patterns, typecheck exit 0). Critères runtime (curl SSR, switch locale interactif, TOC highlight au scroll, drawer mobile) reportés à la vérification phase.