diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 3f045ca..aaf1175 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -161,10 +161,10 @@ Plans: 5. La page article contient un JSON-LD `BreadcrumbList` : Accueil → Blog → Titre de l'article **Plans:** 4 plans Plans: -- [ ] 06-01-PLAN.md — Content schema Zod extension (draft/wordCount/minutes) + Nitro reading-time hook + draft:true sur test articles -- [ ] 06-02-PLAN.md — i18n keys blog.*/nav.blog/a11y.blog* + lien Blog dans AppHeader + BlogCard.vue unifié (default + compact) -- [ ] 06-03-PLAN.md — Page listing app/pages/blog/index.vue (hero + grid + empty state, SSR bilingue) -- [ ] 06-04-PLAN.md — BlogToc.vue + BlogPrevNext.vue + enrichissement app/pages/blog/[slug].vue (breadcrumb + header + TOC + surround) +- [ ] 07-01-PLAN.md — Install nuxt-schema-org + schema updated + definePerson/defineWebSite global + sitemap.sources +- [ ] 07-02-PLAN.md — resolveOgImage helper + og-blog-default.jpg + [slug].vue useSeoMeta enrichi + defineArticle/defineBreadcrumb +- [ ] 07-03-PLAN.md — index.vue useSeoMeta enrichi + defineWebPage(CollectionPage) + defineBreadcrumb +- [ ] 07-04-PLAN.md — server/api/__sitemap__/urls.ts (bilingue, draft:false, alternates hreflang, lastmod=updated||date) ### Phase 8: Content & Cocon Semantique **Goal**: Le blog est lance avec au moins 2 articles Hytale de qualite, et un visiteur qui arrive sur /hytale decouvre les articles recents — le cocon semantique entre blog et page hytale est etabli diff --git a/.planning/phases/07-seo-blog/07-01-PLAN.md b/.planning/phases/07-seo-blog/07-01-PLAN.md new file mode 100644 index 0000000..6531a23 --- /dev/null +++ b/.planning/phases/07-seo-blog/07-01-PLAN.md @@ -0,0 +1,213 @@ +--- +phase: 07-seo-blog +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - pnpm-lock.yaml + - nuxt.config.ts + - content.config.ts + - app/app.vue + - app/utils/seo-person.ts +autonomous: true +requirements: [SEO-11, SEO-12] +must_haves: + truths: + - "nuxt-schema-org est installé et chargé comme module Nuxt" + - "Schema Zod blog_fr/blog_en accepte `updated` (ISO string) en plus de `image`" + - "Une identité Person Killian globale (definePerson) + defineWebSite est émise dans chaque page SSR" + - "nuxt.config.ts référence /api/__sitemap__/urls dans sitemap.sources" + artifacts: + - path: "app/utils/seo-person.ts" + provides: "KILLIAN_PERSON_ID const + killianPerson object (dérivé de siteConfig)" + contains: "export const KILLIAN_PERSON_ID" + - path: "content.config.ts" + provides: "blogSchema étendu avec updated.optional()" + contains: "updated: z.string().optional()" + - path: "nuxt.config.ts" + provides: "module nuxt-schema-org + sitemap.sources" + contains: "nuxt-schema-org" + - path: "app/app.vue" + provides: "useSchemaOrg global (definePerson + defineWebSite)" + contains: "useSchemaOrg" + key_links: + - from: "app/app.vue" + to: "app/utils/seo-person.ts" + via: "import killianPerson" + pattern: "killianPerson" + - from: "nuxt.config.ts" + to: "/api/__sitemap__/urls" + via: "sitemap.sources" + pattern: "sitemap.*sources.*__sitemap__/urls" +--- + + +Fondation SEO Blog : installer `nuxt-schema-org`, étendre le schema Zod `blog_fr`/`blog_en` avec `updated`, déclarer l'identité Killian globale (Person + WebSite) dans `app.vue`, et brancher le sitemap dynamique sur un endpoint Nitro (déclaration uniquement — l'endpoint est créé plan 07-04). + +Purpose: Aucun des plans Wave 2 ne peut fonctionner sans (a) le module `nuxt-schema-org` présent dans `modules[]`, (b) le champ `updated` queryable, (c) l'identité Person disponible par `@id` global, (d) `sitemap.sources` wiré. +Output: package installé, 1 fichier utilitaire créé, 3 fichiers config/racine modifiés. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/07-seo-blog/07-CONTEXT.md +@.planning/phases/07-seo-blog/07-RESEARCH.md +@.planning/phases/07-seo-blog/07-PATTERNS.md +@nuxt.config.ts +@content.config.ts +@app/app.vue +@app/data/site.ts + + +Depuis app/data/site.ts : +- `siteConfig.url` = 'https://killiandalcin.fr' +- `siteConfig.social` = tableau avec entrées Gitea, LinkedIn, Discord, Email (reprendre `url` pour `sameAs`) + +Depuis content.config.ts (existant) : +```ts +const blogSchema = z.object({ + title: z.string(), + description: z.string(), + date: z.string(), + tags: z.array(z.string()).optional(), + image: z.string().optional(), // DÉJÀ présent (D-14 #2 = no-op) + draft: z.boolean().optional().default(false), + wordCount: z.number().optional(), + minutes: z.number().optional(), +}) +``` + +Depuis app/app.vue (existant) : `useHead` + `useLocaleHead({ seo: true })` — NE PAS remplacer, APPEND. + +Auto-imports nuxt-schema-org (une fois module ajouté) : `useSchemaOrg`, `definePerson`, `defineWebSite`, `defineArticle`, `defineBreadcrumb`, `defineWebPage`. + + + + + + + Task 1: Installer nuxt-schema-org + étendre content.config.ts (schema updated) + package.json, pnpm-lock.yaml, content.config.ts + + - package.json (vérifier absence de nuxt-schema-org) + - content.config.ts (schéma actuel, ligne 3-12) + - .planning/phases/07-seo-blog/07-RESEARCH.md §Standard Stack (version cible ^6.0.4) + - .planning/phases/07-seo-blog/07-RESEARCH.md Pitfall 8 (cache invalidation) + - .planning/phases/07-seo-blog/07-PATTERNS.md §content.config.ts (modify) + + + 1. Installer : `pnpm add -D nuxt-schema-org@^6.0.4` (D-01, D-04 — NE PAS installer `@nuxtjs/seo` umbrella). + 2. Dans `content.config.ts`, modifier `blogSchema` : ajouter exactement la ligne `updated: z.string().optional(),` entre `date: z.string(),` et `tags: z.array(z.string()).optional(),` (D-13, D-14). Ne PAS toucher aux autres champs (`image` déjà présent). + 3. Vider les caches pour forcer la re-ingestion : `rm -rf node_modules/.cache/content .nuxt` (Pitfall 8 RESEARCH). + + + grep -q '"nuxt-schema-org"' package.json && grep -q 'updated: z.string().optional()' content.config.ts && pnpm typecheck + + nuxt-schema-org^6.0.4 dans devDependencies, `updated: z.string().optional()` présent dans blogSchema, caches vidés, typecheck exit 0. + + + + Task 2: Enregistrer module + sitemap.sources dans nuxt.config.ts, créer app/utils/seo-person.ts, brancher useSchemaOrg global dans app/app.vue + nuxt.config.ts, app/utils/seo-person.ts, app/app.vue + + - nuxt.config.ts (lignes 1-82 entier, surtout modules[] 5-13) + - app/app.vue (10 lignes entier) + - app/data/site.ts (lignes 5-43 — source url + social) + - .planning/phases/07-seo-blog/07-PATTERNS.md §seo-person.ts, §nuxt.config.ts, §app.vue + - .planning/phases/07-seo-blog/07-RESEARCH.md §Pattern 1 (Global Schema Identity) + + + 1. **nuxt.config.ts** : + - Ajouter `'nuxt-schema-org'` dans `modules[]` après `'@nuxtjs/sitemap'` (ligne ~12). + - Ajouter, au même niveau d'indentation que `site:` et `i18n:`, le bloc : + ```ts + sitemap: { + sources: ['/api/__sitemap__/urls'], + }, + ``` + - Ne PAS modifier `site`, `i18n`, `content`, `runtimeConfig`, `gtag`, `vite`. + 2. **Créer `app/utils/seo-person.ts`** avec le contenu exact (pattern `app/utils/countWords.ts` : JSDoc top + export nommé + const typé) : + ```ts + /** + * Global Person identity for schema.org (Killian Dal-Cin). + * Consumed by: app/app.vue (definePerson global) and app/pages/blog/[slug].vue (author/publisher @id ref). + * Derives URLs from siteConfig — single source of truth. + */ + import { siteConfig } from '~/data/site' + + export const KILLIAN_PERSON_ID = '#killian' + + export const killianPerson = { + '@id': KILLIAN_PERSON_ID, + name: "Killian' Dal-Cin", + url: siteConfig.url, + jobTitle: siteConfig.jobTitle, + sameAs: siteConfig.social + .filter((s) => s.name !== 'Email') + .map((s) => s.url), + } as const + ``` + 3. **app/app.vue** : APPEND (ne pas remplacer) après le bloc `useHead({...})` existant, AVANT la fermeture `` : + ```ts + import { killianPerson } from '~/utils/seo-person' + + useSchemaOrg([ + definePerson(killianPerson), + defineWebSite({ + name: "Killian' Dal-Cin — Hytale Plugin Developer", + inLanguage: ['fr-FR', 'en-US'], + }), + ]) + ``` + Ne pas toucher au `