ae274e77ca
Plan 07-02 shipped: useSeoMeta D-15 + useSchemaOrg Article/Breadcrumb on /blog/[slug], resolveOgImage helper + og-blog-default.jpg fallback. Curl SSR validated, typecheck green. Requirements satisfied: SEO-10, SEO-11, SEO-13, SEO-15.
5.9 KiB
5.9 KiB
gsd_state_version, milestone, milestone_name, status, last_updated, last_activity, progress
| gsd_state_version | milestone | milestone_name | status | last_updated | last_activity | progress | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1.0 | v1.0 | milestone | Plan 07-02 shipped — page /blog/[slug] enrichie useSeoMeta D-15 + useSchemaOrg Article+Breadcrumb (author @id=#killian), resolveOgImage helper + fallback og-blog-default.jpg, typecheck vert, curl SSR validé | 2026-04-22T11:20:00.000Z | 2026-04-22 |
|
Project State
Project Reference
- PROJECT.md: .planning/PROJECT.md
- REQUIREMENTS.md: .planning/REQUIREMENTS.md
- ROADMAP.md: .planning/ROADMAP.md
Current Focus
Phase: Phase 7 — SEO Blog Plan: 07-03 (next — Wave 2, blog index/tags SEO) Status: Plan 07-02 shipped — /blog/[slug] SEO complet (useSeoMeta D-15 + Article/BreadcrumbList JSON-LD), resolveOgImage helper + fallback og-blog-default.jpg, typecheck vert Last activity: 2026-04-22 Resume file: .planning/phases/07-seo-blog/07-03-PLAN.md
Accumulated Context
- M1 complet — déployé en production sur killiandalcin.fr (phases 1–4)
- Stack : Nuxt 4 SSR + Nuxt UI v3 + Tailwind v4 + pnpm + @nuxt/content v3
- Phase 5 shipped: @nuxt/content installé, collections bilingues
blog_fr/blog_en, composants MDC (ProseImg, Alert, ProsePre, Columns, Details, Badge, Video, Clear), Shiki single github-dark,app/pages/blog/[slug].vuerend les articles FR/EN - Gotchas Phase 5 (à retenir) :
- Catch-all
[...slug].vue+@nuxtjs/i18nstrategyprefix→ page component résout à{}(Vue warn: missing template). Fix : single-segment[slug].vue. queryCollection(variable)pas analysable par le Vite extractor de @nuxt/content → utiliser toujours des littérauxqueryCollection('blog_fr')/queryCollection('blog_en').i18n.baseUrlrequis pouruseLocaleHead(SEO tags). Ne pas retirer.- Redirection langue-détectée sans langue dans l'URL :
detectBrowserLanguage.redirectOn: 'no prefix'+fallbackLocale. Éviter lesrouteRules/blog/**hardcodés (cassent le slug + bloquent la détection navigateur).
- Catch-all
- Objectif double : ranker sur "Hytale plugin developer" ET capter trafic longue traîne via contenu communauté
- Articles bilingues : structure FR/EN dans content/ (ex: content/fr/blog/, content/en/blog/)
- og:image par article : image frontmatter ou fallback branded — jamais l'og-image.png générique M1
- Plan 06-01 shipped (2026-04-22) : blogSchema étendu (draft.default(false) + wordCount.optional + minutes.optional), Nitro hook
content:file:afterParseinjecte wordCount+minutes (200 wpm, floor 1 min) sur chaque.mdviacountWordsInMinimalBody, composable fallbackuseReadingTime(number|string)auto-importé, articlestest-kotlin-syntax.md(FR+EN) marquésdraft: true— exclus des listingswhere('draft', '=', false)mais accessibles par URL directe. Cachenode_modules/.cache/content+.nuxtvidés. - Gotcha 06-01 : Le hook
content:file:afterParseexige quewordCount/minutessoient déclarés dans le schema Zod (.optional()sans default) sinon ils sont strippés avant persistance DB — les propriétés injectées par hook ne sont queryables que si le schema les expose. - Gotcha 06-01 (convention) : Dans un plugin Nitro, importer depuis
app/utils/se fait via~/utils/...(et non~~/app/utils/...). Nuxt 4 mappe~/→app/par défaut. Vérifié par typecheck vert sur server/plugins/reading-time.ts. - Plan 06-02 shipped (2026-04-22) : i18n
nav.blog+ 3 clésa11y.blog*(avec interpolation{title}) + blocblog.*14 clés (title, subtitle, stats., readingTime, prevArticle/nextArticle, backToBlog, toc.title, emptyState., breadcrumb.*) ajoutés dans fr.json + en.json. AppHeader.vue navLinks :{ key: 'blog', path: '/blog' }inséré entre hytale et projects (ligne 11, ordre D-15 respecté).app/components/BlogCard.vuecréé (192 lignes, auto-importé Nuxt) : variantdefault(listing) avec cover conditional + tag UBadge + date Intl.DateTimeFormat + h2 + description line-clamp-2 + reading-time (minutes hook || useReadingTime fallback) + extra tags pills + full-card NuxtLink SEO + Schema.org BlogPosting markup ; variantcompact(prev/next, D-09/D-10) : no image + label row avec UIcon arrow directionnelle + h3 + date + NuxtLink aria-label interpoléa11y.blogPrev/a11y.blogNext. Typecheck exit 0. - Gotcha 06-02 (slug derivation) : Les articles @nuxt/content ont un
pathde forme/fr/blog/my-slug. Dans BlogCard.vue, on extrait le slug viaarticle.path.split('/').filter(Boolean).pop()puis on reconstruitlocalePath('/blog/' + slug)— locale-agnostique. Évite de demander un champslugexplicite dans le frontmatter (cohérent convention @nuxt/content : path dérivé du nom de fichier). - Plan 07-02 shipped (2026-04-22) :
app/utils/resolve-og-image.ts(préfixe https://killiandalcin.fr + fallback /og-blog-default.jpg) +public/og-blog-default.jpg(placeholder copié depuis og-image.png — design branded 1200×630 en follow-up backlog) +app/pages/blog/[slug].vueenrichi : imports KILLIAN_PERSON_ID+resolveOgImage, useAsyncData altExists (détecte pair bilingue FR/EN), computeds ogImage/canonicalUrl/publishedIso/modifiedIso/inLanguageTag, useSeoMeta étendu 5→14 clés (D-15 complet : ogImage, ogUrl, ogLocale, ogLocaleAlternate conditionnel, twitterCard, twitterImage, articlePublishedTime, articleModifiedTime, articleAuthor string[]), useSchemaOrg([defineArticle {author/publisher @id=#killian}, defineBreadcrumb 3 items]). Curl /fr/blog/{slug} valide : og:image absolu, article:published_time, JSON-LD Article + BreadcrumbList. - Gotcha 07-02 (typings nuxt-schema-org) :
defineArticle.inLanguageinféréComputedRef<MaybeFalsy<'fr-FR'>>(narrow) refuse une union'fr-FR' | 'en-US'. Cast localiséas unknown as ComputedRef<'fr-FR'>suffit — runtime émet correctement les deux valeurs selon locale.articleAuthorde useSeoMeta attendstring[], passtring(packaging @unhead récent).