From 680bbfbbe683398f8d16d68cd213c7913776741b Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Wed, 22 Apr 2026 10:25:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(07):=20capture=20phase=20context=20?= =?UTF-8?q?=E2=80=94=20SEO=20blog=20(JSON-LD=20via=20nuxt-schema-org,=20og?= =?UTF-8?q?:image=20hybride,=20sitemap=20Nitro=20endpoint,=20hreflang=20al?= =?UTF-8?q?ternates)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/phases/07-seo-blog/07-CONTEXT.md | 136 ++++++++++++++++++ .../phases/07-seo-blog/07-DISCUSSION-LOG.md | 126 ++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 .planning/phases/07-seo-blog/07-CONTEXT.md create mode 100644 .planning/phases/07-seo-blog/07-DISCUSSION-LOG.md diff --git a/.planning/phases/07-seo-blog/07-CONTEXT.md b/.planning/phases/07-seo-blog/07-CONTEXT.md new file mode 100644 index 0000000..e2dd974 --- /dev/null +++ b/.planning/phases/07-seo-blog/07-CONTEXT.md @@ -0,0 +1,136 @@ +# Phase 7: SEO Blog - Context + +**Gathered:** 2026-04-22 +**Status:** Ready for planning + + +## Phase Boundary + +Rendre chaque page blog (article + listing) parfaitement indexable par les moteurs de recherche : meta tags complets et uniques par article, JSON-LD `Article` + `BreadcrumbList` valides côté article, JSON-LD `Blog` simple côté listing, sitemap incluant `/blog/[slug]` FR+EN avec alternates hreflang. Aucun JavaScript client requis pour que le crawl fonctionne (SSR pur). + +**Hors scope :** JSON-LD `WebSite`/`Person` global sur la home, refonte SEO des autres pages (projets, hytale, contact), liens internes /hytale ↔ articles (= SEO-14, Phase 8 cocon sémantique). + + + + +## Implementation Decisions + +### Génération JSON-LD +- **D-01:** Installer le module `nuxt-schema-org` (famille Nuxt SEO). API `defineArticle()` / `defineBreadcrumb()` typée, auto-merge avec `site.url`, locale-aware FR/EN. Évite le hand-rolled `useHead({ script: [...] })` répétitif et le drift schema.org. +- **D-02:** Sur `/blog/[slug]` → `useSchemaOrg([defineArticle(...), defineBreadcrumb(...)])`. Champs Article : `headline`, `description`, `image`, `datePublished`, `dateModified`, `author` (Person Killian), `publisher` (Person Killian), `inLanguage` (fr-FR / en-US), `mainEntityOfPage`. +- **D-03:** Sur `/blog` (listing) → `useSchemaOrg([defineCollectionPage(...)])` ou équivalent `Blog` minimal (pas de `BlogPosting[]` exhaustif — coût/bruit). Breadcrumb Accueil → Blog. +- **D-04:** Ne PAS installer le bundle `@nuxtjs/seo` umbrella — doublonne avec `@nuxtjs/sitemap` déjà présent et embarque modules non désirés (link-checker, robots déjà géré). Cherry-pick `nuxt-schema-org` (+ éventuellement `nuxt-og-image` reporté en Phase 8 si besoin). + +### og:image +- **D-05:** Stratégie hybride frontmatter → fallback statique. Si l'article a `image:` en frontmatter (chemin relatif depuis `public/`) → utilisé tel quel. Sinon → fallback branded statique `/og-blog-default.jpg` (1200×630, à créer une fois sous `public/`, design : logo Killian' + accent typographique "Blog · killiandalcin.fr"). +- **D-06:** Composable ou helper `resolveOgImage(article)` qui retourne le chemin absolu (préfixé `site.url`) — utilisé à la fois par `useSeoMeta({ ogImage })` ET par `defineArticle({ image })` pour cohérence. +- **D-07:** Génération dynamique via `nuxt-og-image` (Satori) explicitement reportée — coût (asset à designer + runtime edge) > bénéfice tant qu'on n'a pas validé le ratio articles publiés × engagement social. + +### Sitemap +- **D-08:** Endpoint Nitro `server/api/__sitemap__/urls.ts` qui query `blog_fr` et `blog_en` (where `draft = false`), retourne pour chaque article `{ loc, lastmod, alternatives: [{ hreflang, href }] }`. Référencé dans `nuxt.config.ts > sitemap.sources`. Pattern officiel `@nuxtjs/sitemap` + i18n. +- **D-09:** `lastmod` = `dateModified` de l'article (= `updated` frontmatter si présent, sinon `date`). +- **D-10:** Drafts (`draft: true`) **EXCLUS** du sitemap — cohérent avec le filtrage des listings (Phase 6 D-14). Restent accessibles par URL directe pour preview. +- **D-11:** Alternates hreflang générés par paire de slugs : si `mon-slug.md` existe en FR ET EN → entrées sitemap déclarent `xhtml:link rel="alternate" hreflang="fr"` et `hreflang="en"` croisés (+ `x-default` pointant vers FR, locale par défaut). Si l'article n'existe que dans une langue → pas d'alternate. + +### Article metadata +- **D-12:** `author` et `publisher` : constante globale Killian (single Person identity), définie dans un helper partagé (ex: `app/utils/seo-person.ts`) ou directement dans la config schema-org globale (`useSchemaOrg` au niveau app.vue avec `defineWebSite` + `definePerson` Killian, hérité par les Article enfants). Pas de support frontmatter `author:` override (pas de guest authors planifiés). +- **D-13:** `dateModified` source : champ `updated` optionnel dans le frontmatter (Zod `updated.optional()` à ajouter au schema `blog_fr`/`blog_en`). Si absent → `dateModified = date`. Pas de git mtime (casse en build Docker sans .git layer). + +### Schema content extension +- **D-14:** Étendre les collections `blog_fr` / `blog_en` (config @nuxt/content) avec : + - `updated: z.string().optional()` (ISO date, alimente dateModified) + - `image: z.string().optional()` (déjà présent en pratique frontmatter, formaliser dans le schema) + +### useSeoMeta enrichissement +- **D-15:** `[slug].vue` `useSeoMeta` complété avec : `ogImage` (résolu via D-06), `ogUrl` (URL canonique localisée), `ogLocale` (`fr_FR` / `en_US`), `ogLocaleAlternate` (l'autre locale si l'article existe dans les deux), `twitterCard: 'summary_large_image'`, `twitterImage` (= ogImage), `articlePublishedTime`, `articleModifiedTime`, `articleAuthor`. +- **D-16:** `/blog` index : `useSeoMeta` enrichi avec `ogImage` (= fallback statique `/og-blog-default.jpg`), `ogType: 'website'`, `ogLocale`, `ogLocaleAlternate`. + +### Claude's Discretion +- Naming exact du composable/helper de résolution og:image (D-06) +- Format précis de la `description` du JSON-LD `Blog`/`CollectionPage` du listing (D-03) +- Choix entre déclarer Killian en `definePerson` global au niveau `app.vue` vs en `author` inline dans chaque `defineArticle` — selon ce que `nuxt-schema-org` recommande (à confirmer en research/plan) +- Design exact de `/og-blog-default.jpg` (juste un fallback branded, pas critique tant que ≠ `og-image.png` M1 générique) + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Specs Phase 7 — sources internes +- `.planning/REQUIREMENTS.md` §SEO-10 → SEO-13, SEO-15 — exigences acceptance pour cette phase +- `.planning/ROADMAP.md` §"Phase 7: SEO Blog" — Success Criteria (5 critères curl) + +### Décisions héritées des phases précédentes +- `.planning/phases/03-seo-i18n/03-CONTEXT.md` — décisions SEO M1 (siteConfig, baseUrl, useLocaleHead pattern) +- `.planning/phases/05-nuxt-content-setup-renderer/05-CONTEXT.md` — schémas Zod blog_fr/blog_en, conventions @nuxt/content v3 +- `.planning/phases/06-blog-pages/06-CONTEXT.md` — D-14 (drafts accessibles direct URL mais filtrés des listings), conventions BlogCard / breadcrumb +- `.planning/phases/06-blog-pages/06-04-SUMMARY.md` — état actuel useSeoMeta sur `[slug].vue` + +### Code existant à étendre +- `app/pages/blog/[slug].vue` — useSeoMeta minimal à enrichir + ajout useSchemaOrg (D-02, D-15) +- `app/pages/blog/index.vue` — useSeoMeta minimal à enrichir + JSON-LD listing (D-03, D-16) +- `app/app.vue` — useLocaleHead({ seo: true }) déjà présent ; potentiellement y ajouter le definePerson/defineWebSite global (D-12) +- `nuxt.config.ts` — `site`, `i18n`, `@nuxtjs/sitemap` config existante ; ajouter `nuxt-schema-org` au modules array + `sitemap.sources` +- `server/plugins/reading-time.ts` — pattern Nitro hook `content:file:afterParse` (référence pour ajouter d'autres injections schema si nécessaire) +- `app/data/site.ts` (ou équivalent siteConfig) — source identité Killian pour Person/publisher + +### Docs externes (officielles) +- `nuxt-schema-org` docs : https://nuxtseo.com/schema-org — defineArticle, defineBreadcrumb, defineWebSite, definePerson +- `@nuxtjs/sitemap` docs : https://nuxtseo.com/sitemap — sources config, multi-sitemap i18n, alternates hreflang +- `@nuxt/content v3` queryCollection API — déjà maîtrisé Phase 5/6 +- schema.org/Article — champs requis Google : headline, image, datePublished, author, publisher (Organization OR Person) +- Google Search Central — Article structured data : https://developers.google.com/search/docs/appearance/structured-data/article + + + + +## Existing Code Insights + +### Reusable Assets +- `useSeoMeta()` (Nuxt auto-import) : déjà utilisé sur `[slug].vue` et `index.vue` — étendre, ne pas réécrire +- `useLocaleHead({ seo: true })` (`@nuxtjs/i18n`) : déjà géré au niveau `app.vue` pour les hreflang globaux et og:locale — ne pas dupliquer côté pages +- `queryCollection('blog_fr' | 'blog_en')` : pattern figé Phase 5/6, à réutiliser pour le sitemap source endpoint +- `useReadingTime()` composable + champs `minutes` / `wordCount` Phase 6 : disponibles si on veut les exposer en JSON-LD `wordCount` +- `siteConfig` / `app/data/site.ts` (à confirmer chemin) : source de vérité identité Killian (nom, URL, social) pour Person + +### Established Patterns +- Locale via `useI18n()` + `localePath()` partout — toute URL canonique doit passer par `localePath` pour respecter `prefix` strategy +- `useAsyncData` keys incluent `${locale.value}` pour invalidation correcte au switch FR/EN +- Schema Zod content : extension via `.optional()` pattern (cf. Phase 6 D-01 pour `wordCount`/`minutes`) — appliquer même approche pour `updated`/`image` +- Convention og:image M1 explicite : **jamais** réutiliser `og-image.png` générique sur les pages blog + +### Integration Points +- `nuxt.config.ts > modules[]` : ajouter `'nuxt-schema-org'` (ordre indifférent, mais cohérent à côté de `@nuxtjs/sitemap`) +- `nuxt.config.ts > sitemap` : ajouter `sources: ['/api/__sitemap__/urls']` et confirmer config i18n auto-detection +- `server/api/__sitemap__/urls.ts` : nouveau fichier — pattern Nitro server route, retourne `SitemapUrlInput[]` +- `content.config.ts` (ou bloc équivalent) : étendre les schémas `blog_fr`/`blog_en` avec `updated`, `image` +- `public/og-blog-default.jpg` : nouvel asset 1200×630 à créer + + + + +## Specific Ideas + +- Killian = Person unique (pas d'Organization) — portfolio personnel freelance, pas une marque collective +- Articles bilingues = même slug FR et EN doivent rester appairables (cohérent avec convention Phase 5/6 : nom de fichier identique entre `content/fr/blog/` et `content/en/blog/`) +- Validation finale doit pouvoir se faire en pur `curl` sans navigateur (cf. Success Criteria ROADMAP) — donc tout le SEO doit être SSR, jamais hydraté côté client + + + + +## Deferred Ideas + +- **og:image dynamique via nuxt-og-image (Satori)** — reportée. À reconsidérer si traction social mesurée justifie l'investissement design + runtime edge. +- **JSON-LD WebSite + Person globaux sur la home** — relève d'une phase SEO globale du portfolio, pas SEO blog. À ajouter si Phase 8 ou audit SEO ultérieur le demande. +- **Liens internes structurés /hytale ↔ articles (SEO-14)** — explicitement Phase 8 (Cocon Sémantique). +- **git mtime pour dateModified** — non retenu (casse Docker sans .git). À reconsidérer si on ajoute un layer git ou un build-time stamping en CI. +- **JSON-LD `BlogPosting[]` exhaustif sur /blog** — bruit pour Google, pas standard pour les listings. Si besoin de richesse listing, préférer `ItemList` minimal en Phase 8. + + + +--- + +*Phase: 07-seo-blog* +*Context gathered: 2026-04-22* diff --git a/.planning/phases/07-seo-blog/07-DISCUSSION-LOG.md b/.planning/phases/07-seo-blog/07-DISCUSSION-LOG.md new file mode 100644 index 0000000..4c392ef --- /dev/null +++ b/.planning/phases/07-seo-blog/07-DISCUSSION-LOG.md @@ -0,0 +1,126 @@ +# Phase 7: SEO Blog - 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:** 07-seo-blog +**Areas discussed:** JSON-LD strategy, og:image fallback, Sitemap source, Périmètre listing, Author/publisher, dateModified, Drafts in sitemap, hreflang alternates + +--- + +## JSON-LD strategy + +| Option | Description | Selected | +|--------|-------------|----------| +| nuxt-schema-org (Recommended) | Module Nuxt SEO. defineArticle/defineBreadcrumb typés, locale-aware. | ✓ | +| Hand-rolled via useHead | Construction manuelle JSON-LD. Zero dep mais répétitif et risque drift. | | +| @nuxtjs/seo (umbrella) | Bundle complet — doublonne avec @nuxtjs/sitemap. | | + +**User's choice:** nuxt-schema-org +**Notes:** Recommandation suivie — typage + auto-merge site.url + cohérence Nuxt SEO ecosystem. + +--- + +## og:image fallback + +| Option | Description | Selected | +|--------|-------------|----------| +| Frontmatter image OR static fallback (Recommended) | image: frontmatter sinon /og-blog-default.jpg statique. KISS, zero runtime. | ✓ | +| nuxt-og-image (Satori, runtime) | Génération dynamique. Joli mais build-time + edge runtime + design. | | +| Frontmatter only, fail si absent | Strict, bloque les articles texte-only. | | + +**User's choice:** Hybride frontmatter + fallback statique +**Notes:** nuxt-og-image reporté en deferred ideas (à reconsidérer si traction social). + +--- + +## Sitemap source + +| Option | Description | Selected | +|--------|-------------|----------| +| Endpoint Nitro /api/__sitemap__/urls.ts (Recommended) | Server route query collections, retourne loc+lastmod+alternates. | ✓ | +| Auto-discovery via prerender hooks | Marche en SSG uniquement. | | +| Liste statique régénérée à chaque build | Pas reactive aux nouveaux articles post-build. | | + +**User's choice:** Endpoint Nitro +**Notes:** Pattern officiel @nuxtjs/sitemap + i18n. Compatible SSR pur (déploiement Docker actuel). + +--- + +## Périmètre listing + +| Option | Description | Selected | +|--------|-------------|----------| +| Articles + listing minimal (Recommended) | /blog reçoit useSeoMeta enrichi + JSON-LD Blog simple ; /blog/[slug] le pack complet. | ✓ | +| Articles uniquement | Plus rapide mais ranking listing affaibli. | | +| Articles + listing + page d'accueil / | Scope creep — relève d'une phase SEO globale. | | + +**User's choice:** Articles + listing minimal +**Notes:** WebSite/Person globaux home reportés en deferred. + +--- + +## Author/publisher + +| Option | Description | Selected | +|--------|-------------|----------| +| Constante globale Killian (Recommended) | Single Person identity dans config. Pas de frontmatter override. | ✓ | +| Frontmatter author override + fallback Killian | Flexibilité guest-posts non planifiée. | | + +**User's choice:** Constante globale Killian +**Notes:** Pas de guest authors prévus — over-engineering évité. + +--- + +## dateModified + +| Option | Description | Selected | +|--------|-------------|----------| +| Frontmatter `updated` optionnel, fallback `date` (Recommended) | Schema Zod enrichi updated.optional(). Semantically correct. | ✓ | +| Toujours = date | Perte signal SEO si article révisé. | | +| git mtime du fichier .md | Hook git — casse en build Docker sans .git layer. | | + +**User's choice:** updated optional + fallback date +**Notes:** git mtime déféré — à reconsidérer si on ajoute un layer git ou stamping CI. + +--- + +## Drafts in sitemap + +| Option | Description | Selected | +|--------|-------------|----------| +| Non, draft=false uniquement (Recommended) | Cohérent avec Phase 6 D-14. Drafts accessibles direct URL only. | ✓ | +| Oui, tous articles + drafts | Risque indexation drafts (test-kotlin-syntax.md). | | + +**User's choice:** Drafts exclus +**Notes:** Cohérence avec filtrage listings établi Phase 6. + +--- + +## hreflang alternates + +| Option | Description | Selected | +|--------|-------------|----------| +| Oui, par paire de slugs (Recommended) | xhtml:link rel='alternate' hreflang='fr/en' croisés + x-default FR. | ✓ | +| Non, sitemap par locale indépendant | Risque duplicate content vu par Google. | | + +**User's choice:** Alternates par paire de slugs +**Notes:** Fr = locale par défaut → x-default pointe sur FR. + +--- + +## Claude's Discretion + +- Naming exact composable/helper résolution og:image +- Format précis description JSON-LD Blog/CollectionPage du listing +- Choix definePerson global app.vue vs author inline par defineArticle (à confirmer en research) +- Design exact /og-blog-default.jpg + +## Deferred Ideas + +- og:image dynamique via nuxt-og-image (Satori) +- JSON-LD WebSite + Person globaux sur la home +- Liens internes /hytale ↔ articles (SEO-14, déjà planifié Phase 8) +- git mtime pour dateModified +- JSON-LD BlogPosting[] exhaustif sur /blog