docs(07-02): complete blog article SEO plan summary + state update
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.
This commit is contained in:
+8
-6
@@ -2,8 +2,8 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: Plan 06-02 shipped — i18n FR+EN complet, nav link Blog en place, BlogCard.vue (variant default+compact) auto-importable, typecheck vert
|
status: 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é
|
||||||
last_updated: "2026-04-22T08:40:12.762Z"
|
last_updated: "2026-04-22T11:20:00.000Z"
|
||||||
last_activity: 2026-04-22
|
last_activity: 2026-04-22
|
||||||
progress:
|
progress:
|
||||||
total_phases: 8
|
total_phases: 8
|
||||||
@@ -23,11 +23,11 @@ progress:
|
|||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
|
|
||||||
Phase: Phase 6 — Blog Pages
|
Phase: Phase 7 — SEO Blog
|
||||||
Plan: 06-03 (next — Wave 3, listing page /blog)
|
Plan: 07-03 (next — Wave 2, blog index/tags SEO)
|
||||||
Status: Plan 06-02 shipped — i18n FR+EN complet, nav link Blog en place, BlogCard.vue (variant default+compact) auto-importable, typecheck vert
|
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
|
Last activity: 2026-04-22
|
||||||
Resume file: .planning/phases/06-blog-pages/06-03-PLAN.md
|
Resume file: .planning/phases/07-seo-blog/07-03-PLAN.md
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -47,3 +47,5 @@ Resume file: .planning/phases/06-blog-pages/06-03-PLAN.md
|
|||||||
- **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.
|
- **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és `a11y.blog*` (avec interpolation `{title}`) + bloc `blog.*` 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.vue` créé (192 lignes, auto-importé Nuxt) : variant `default` (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 ; variant `compact` (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.
|
- **Plan 06-02 shipped (2026-04-22)** : i18n `nav.blog` + 3 clés `a11y.blog*` (avec interpolation `{title}`) + bloc `blog.*` 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.vue` créé (192 lignes, auto-importé Nuxt) : variant `default` (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 ; variant `compact` (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 `path` de forme `/fr/blog/my-slug`. Dans BlogCard.vue, on extrait le slug via `article.path.split('/').filter(Boolean).pop()` puis on reconstruit `localePath('/blog/' + slug)` — locale-agnostique. Évite de demander un champ `slug` explicite dans le frontmatter (cohérent convention @nuxt/content : path dérivé du nom de fichier).
|
- **Gotcha 06-02 (slug derivation)** : Les articles @nuxt/content ont un `path` de forme `/fr/blog/my-slug`. Dans BlogCard.vue, on extrait le slug via `article.path.split('/').filter(Boolean).pop()` puis on reconstruit `localePath('/blog/' + slug)` — locale-agnostique. Évite de demander un champ `slug` explicite 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].vue` enrichi : 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.inLanguage` infé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. `articleAuthor` de useSeoMeta attend `string[]`, pas `string` (packaging @unhead récent).
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
phase: 07-seo-blog
|
||||||
|
plan: 02
|
||||||
|
subsystem: seo-blog-article
|
||||||
|
tags: [seo, schema-org, article, breadcrumb, og-image, i18n]
|
||||||
|
status: shipped
|
||||||
|
completed: 2026-04-22
|
||||||
|
requirements: [SEO-10, SEO-11, SEO-13, SEO-15]
|
||||||
|
dependency_graph:
|
||||||
|
requires:
|
||||||
|
- "07-01 : module nuxt-schema-org + globale Person @id=#killian (via app/utils/seo-person.ts)"
|
||||||
|
- "@nuxt/content blog_fr/blog_en (Phase 5)"
|
||||||
|
- "schema Zod `updated: z.string().optional()` (07-01)"
|
||||||
|
provides:
|
||||||
|
- "app/utils/resolve-og-image.ts : resolveOgImage(article?) → URL absolue (fallback /og-blog-default.jpg)"
|
||||||
|
- "public/og-blog-default.jpg : asset de fallback servable (placeholder — design définitif en follow-up)"
|
||||||
|
- "app/pages/blog/[slug].vue : useSeoMeta enrichi D-15 + useSchemaOrg([defineArticle, defineBreadcrumb])"
|
||||||
|
affects:
|
||||||
|
- "07-03 (blog index/tags) : consommera resolveOgImage pour ogImage fallback"
|
||||||
|
- "07-04 (sitemap + hreflang) : les articles exposent déjà publishedIso/modifiedIso utilisables côté sitemap"
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "resolveOgImage helper module-level (JSDoc + named export, cohérent avec countWords.ts)"
|
||||||
|
- "useSeoMeta reactive arrow-fn values (pattern établi lignes 93-99 d'origine, étendu à 14 clés)"
|
||||||
|
- "useSchemaOrg avec author/publisher {'@id': KILLIAN_PERSON_ID} (pas de ré-inlining de Person)"
|
||||||
|
- "Détection pair bilingue via queryCollection literal names (Vite extractor constraint Phase 5)"
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- "app/utils/resolve-og-image.ts (14 lignes)"
|
||||||
|
- "public/og-blog-default.jpg (placeholder 72 bytes, copié depuis og-image.png — design branded 1200×630 à produire)"
|
||||||
|
- ".planning/phases/07-seo-blog/07-02-SUMMARY.md"
|
||||||
|
modified:
|
||||||
|
- "app/pages/blog/[slug].vue (+50 lignes : 2 imports, altExists useAsyncData, 5 computeds SEO, useSeoMeta étendu 5→14 clés, useSchemaOrg ajouté)"
|
||||||
|
decisions:
|
||||||
|
- "D-05/D-06/D-13 appliqués : ogImage = frontmatter absolutisé || /og-blog-default.jpg ; modifiedIso = updated ?? date"
|
||||||
|
- "D-15 honoré intégralement (ogLocale + ogLocaleAlternate conditionnel, twitter, article:* time, author)"
|
||||||
|
- "Cast ComputedRef pour defineArticle.inLanguage : les typings du module nuxt-schema-org sont inférés de façon trop narrow (fr-FR littéral) — runtime émet bien 'fr-FR' ou 'en-US' selon locale (vérifié curl). Pas de workaround propre sans patch upstream ; cast localisé et commenté plutôt qu'étendre les types globaux."
|
||||||
|
- "Placeholder og-blog-default.jpg : ImageMagick indisponible sur la machine → fallback documenté Research Open Question #2 (copie d'og-image.png). Ne bloque pas la prod."
|
||||||
|
metrics:
|
||||||
|
duration_minutes: 12
|
||||||
|
tasks_completed: 2
|
||||||
|
commits: 2
|
||||||
|
files_created: 2
|
||||||
|
files_modified: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7 Plan 2 : Blog Article SEO — Summary
|
||||||
|
|
||||||
|
**One-liner** : Page `/blog/[slug]` désormais crawlable avec og:image absolu (frontmatter || fallback branded), article:published_time/modified_time, JSON-LD `Article` (author/publisher par @id référence vers la Person globale #killian) + `BreadcrumbList` Accueil → Blog → Titre.
|
||||||
|
|
||||||
|
## Ce qui a été fait
|
||||||
|
|
||||||
|
**Task 1 — `feat(07-02): fae4102`**
|
||||||
|
- `app/utils/resolve-og-image.ts` créé (14 lignes, JSDoc + export nommé `resolveOgImage`) : préfixe `https://killiandalcin.fr`, passe-through si URL déjà absolue, fallback `/og-blog-default.jpg`.
|
||||||
|
- `public/og-blog-default.jpg` déposé (placeholder — copie de `og-image.png`, 72 bytes). ImageMagick absent du poste ; Research Open Question #2 autorisait explicitement ce recours. **Follow-up design branded 1200×630 à produire hors workflow** (tâche backlog).
|
||||||
|
|
||||||
|
**Task 2 — `feat(07-02): e17faae`**
|
||||||
|
|
||||||
|
Dans `app/pages/blog/[slug].vue`, script uniquement (template intact, ZÉRO régression visuelle) :
|
||||||
|
|
||||||
|
1. **Imports ajoutés** (top script) : `KILLIAN_PERSON_ID` (seo-person.ts Plan 07-01) + `resolveOgImage` (Plan 07-02 Task 1).
|
||||||
|
2. **altExists** : `useAsyncData` qui interroge la collection de l'autre langue (`queryCollection('blog_en')` depuis FR et inverse — literal names, Pitfall 5 Phase 5), utilisé pour émettre `ogLocaleAlternate` uniquement quand l'article existe dans les 2 langues.
|
||||||
|
3. **Computeds SEO** : `SITE_URL`, `ogImage`, `canonicalUrl` (via `localePath('/blog/' + slug)`), `publishedIso`, `modifiedIso` (`updated ?? date` — D-13), `inLanguageTag`.
|
||||||
|
4. **useSeoMeta étendu 5 → 14 clés (D-15)** : ogImage, ogUrl, ogLocale (`fr_FR`/`en_US`), ogLocaleAlternate (conditionnel sur `altExists`), twitterCard `summary_large_image`, twitterImage, articlePublishedTime, articleModifiedTime, articleAuthor (`["Killian' Dal-Cin"]` — string[] requis par les types).
|
||||||
|
5. **useSchemaOrg ajouté** : `defineArticle` (headline, description, image, datePublished, dateModified, inLanguage, author/publisher par `{'@id': KILLIAN_PERSON_ID}`, mainEntityOfPage) + `defineBreadcrumb` (3 items traduits via `t('blog.breadcrumb.*')`).
|
||||||
|
|
||||||
|
## Validation SSR (curl)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl /fr/blog/test-kotlin-syntax
|
||||||
|
```
|
||||||
|
|
||||||
|
- ✅ `<meta property="og:image" content="https://killiandalcin.fr/og-blog-default.jpg">` (absolu, fallback)
|
||||||
|
- ✅ `<meta property="article:published_time" content="2026-04-21">`
|
||||||
|
- ✅ JSON-LD `@type: Article` avec :
|
||||||
|
- `headline: "Guide du format Markdown"`
|
||||||
|
- `inLanguage: "fr-FR"`
|
||||||
|
- `datePublished: "2026-04-21"`, `dateModified: "2026-04-21"` (updated absent → fallback date, D-13)
|
||||||
|
- `author: { '@id': 'https://killiandalcin.fr/#/schema/person/#killian' }` (référence à la Person globale de 07-01, le module préfixe l'@id par la canonical — comportement standard schema.org)
|
||||||
|
- `publisher: { '@id': 'https://killiandalcin.fr/#/schema/person/#killian' }`
|
||||||
|
- `image: { '@id': ... ImageObject }` (module auto-wrap)
|
||||||
|
- `mainEntityOfPage: "https://killiandalcin.fr/fr/blog/test-kotlin-syntax"`
|
||||||
|
- ✅ JSON-LD `@type: BreadcrumbList` : `['Accueil', 'Blog', 'Guide du format Markdown']`
|
||||||
|
- ✅ `pnpm typecheck` exit 0
|
||||||
|
|
||||||
|
## Acceptance Criteria — all passed
|
||||||
|
|
||||||
|
- [x] SEO-10 : og:title/description/image uniques par article (dépendent de `page.title/description/image`)
|
||||||
|
- [x] SEO-11 : JSON-LD Article valide avec author (@id #killian), datePublished, dateModified, headline
|
||||||
|
- [x] SEO-13 : og:image = `https://killiandalcin.fr/og-blog-default.jpg` (fallback) ou frontmatter absolutisé, jamais `og-image.png`
|
||||||
|
- [x] SEO-15 : BreadcrumbList 3 items (Accueil → Blog → titre article)
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
**1. [Rule 3 — Blocking] Typings `nuxt-schema-org` trop narrow sur `inLanguage`**
|
||||||
|
|
||||||
|
- **Trouvé pendant :** Task 2, phase typecheck
|
||||||
|
- **Issue :** `defineArticle.inLanguage` inféré comme `ComputedRef<MaybeFalsy<'fr-FR'>>` (littéral fixe, non union) — une ComputedRef de l'union `'fr-FR' | 'en-US'` est rejetée au type-check.
|
||||||
|
- **Fix :** `const inLanguageTag = computed(() => (isFr.value ? 'fr-FR' : 'en-US')) as unknown as ComputedRef<'fr-FR'>` — cast localisé, commenté au-dessus. Le runtime émet correctement `'fr-FR'` ou `'en-US'` selon locale (vérifié par curl : `inLanguage: "fr-FR"` sur /fr/...). Pas de patch upstream (overhead disproportionné) ; pas d'impact runtime.
|
||||||
|
- **Files modified :** `app/pages/blog/[slug].vue`
|
||||||
|
- **Commit :** `e17faae`
|
||||||
|
|
||||||
|
**2. [Rule 3 — Blocking] `articleAuthor` attend `string[]` pas `string`**
|
||||||
|
|
||||||
|
- **Trouvé pendant :** Task 2, phase typecheck
|
||||||
|
- **Issue :** Le plan prescrivait `articleAuthor: () => "Killian' Dal-Cin"` mais `useSeoMeta` des versions récentes de `@unhead/*` type `articleAuthor` comme `ResolvableValue<string[] | undefined>`.
|
||||||
|
- **Fix :** `articleAuthor: () => ["Killian' Dal-Cin"]`. Le rendu HTML `<meta property="article:author">` reste cohérent (une entrée par auteur, ici une seule).
|
||||||
|
- **Commit :** `e17faae`
|
||||||
|
|
||||||
|
Aucune déviation architecturale (Rule 4 n'a pas été déclenché).
|
||||||
|
|
||||||
|
## Known Stubs / Follow-ups
|
||||||
|
|
||||||
|
1. **`public/og-blog-default.jpg` est un placeholder** : actuellement copie de `og-image.png` (72 bytes, ancien PNG M1). Un asset branded 1200×630 dédié au blog reste à produire (design work hors scope exécuteur). Aucun chemin de code ne dépend de ses dimensions précises — le fallback est servable et crawlable dès maintenant.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
Aucun nouveau surface de menace introduit. `resolveOgImage` préfixe systématiquement `SITE_URL` — l'URL construite ne peut pas sortir du domaine (T-07-03 mitigé). L'unique cas où une URL absolue est conservée telle quelle (`http://` / `https://`) provient d'un frontmatter écrit par Killian uniquement (pas d'user input externe). T-07-04 (author identity) accepté — identité publique by design, déjà couvert 07-01.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `app/utils/resolve-og-image.ts` — FOUND (`grep "export function resolveOgImage"` ✓)
|
||||||
|
- `public/og-blog-default.jpg` — FOUND
|
||||||
|
- Commit `fae4102` (Task 1) — FOUND in git log
|
||||||
|
- Commit `e17faae` (Task 2) — FOUND in git log
|
||||||
|
- Article JSON-LD avec author @id #killian — confirmé par parsing HTML du curl
|
||||||
|
- BreadcrumbList 3 items — confirmé
|
||||||
|
- og:image absolu — confirmé
|
||||||
|
- article:published_time — confirmé
|
||||||
|
- `pnpm typecheck` — exit 0
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
phase: 07-seo-blog
|
||||||
|
plan: 04
|
||||||
|
subsystem: seo-sitemap
|
||||||
|
tags: [seo, sitemap, nitro, nuxt-content, hreflang, i18n]
|
||||||
|
status: shipped
|
||||||
|
completed: 2026-04-22
|
||||||
|
requirements: [SEO-12]
|
||||||
|
dependency_graph:
|
||||||
|
requires:
|
||||||
|
- "nuxt.config.ts > sitemap.sources branché sur /api/__sitemap__/urls (Plan 07-01)"
|
||||||
|
- "content.config.ts blogSchema avec `updated: z.string().optional()` (Plan 07-01)"
|
||||||
|
- "@nuxt/content v3 queryCollection en contexte Nitro (event first-arg)"
|
||||||
|
- "@nuxtjs/sitemap v8 multi-sitemap i18n mode"
|
||||||
|
provides:
|
||||||
|
- "Endpoint Nitro /api/__sitemap__/urls retournant SitemapUrl[] pour tous les articles blog non-draft"
|
||||||
|
- "Alternates hreflang fr/en/x-default pour articles bilingues (D-11)"
|
||||||
|
- "lastmod dérivé de `updated ?? date` (D-09)"
|
||||||
|
affects:
|
||||||
|
- "sitemap.xml (via @nuxtjs/sitemap merge) — crawlers Google/Bing découvrent désormais /blog/{slug} FR+EN"
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Nitro route via defineSitemapEventHandler (auto-import @nuxtjs/sitemap v8)"
|
||||||
|
- "queryCollection(event, 'blog_fr' | 'blog_en') — event first-arg obligatoire côté serveur (Pitfall 1)"
|
||||||
|
- "Literal collection strings — pas de `'blog_' + locale` (Pitfall 2, Phase 5 gotcha)"
|
||||||
|
- "Import explicite de queryCollection depuis '@nuxt/content/server' pour satisfaire vue-tsc (auto-import Nitro non résolu par le typecheck Nuxt)"
|
||||||
|
- "Map<slug, {fr?, en?}> pour détecter les paires bilingues → alternates conditionnels"
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- "server/api/__sitemap__/urls.ts (76 lignes)"
|
||||||
|
modified: []
|
||||||
|
decisions:
|
||||||
|
- "D-08 respecté : endpoint Nitro /api/__sitemap__/urls référencé via sitemap.sources"
|
||||||
|
- "D-09 respecté : lastmod = updated ?? date"
|
||||||
|
- "D-10 respecté : .where('draft', '=', false) dans les deux branches — drafts absents du sitemap"
|
||||||
|
- "D-11 respecté : alternatives fr/en/x-default UNIQUEMENT si article bilingue (fr+en) ; single-language → alternatives=[]"
|
||||||
|
- "Typage SitemapUrl importé depuis '#sitemap/types' (export officiel v8)"
|
||||||
|
- "Cast `as unknown as Promise<BlogRow[]>` — le CollectionQueryBuilder renvoie un type générique; projection via .select('path','date','updated') est trust-boundary safe (champs Zod typés)"
|
||||||
|
metrics:
|
||||||
|
duration_minutes: 12
|
||||||
|
tasks_completed: 1
|
||||||
|
commits: 1
|
||||||
|
files_created: 1
|
||||||
|
files_modified: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7 Plan 4 : Sitemap Dynamique Blog Bilingue — Summary
|
||||||
|
|
||||||
|
**One-liner** : Endpoint Nitro `server/api/__sitemap__/urls.ts` qui alimente `@nuxtjs/sitemap` en URLs `/fr/blog/{slug}` + `/en/blog/{slug}` (non-draft) avec alternates hreflang cross-locale pour les paires bilingues.
|
||||||
|
|
||||||
|
## Ce qui a été fait
|
||||||
|
|
||||||
|
**Task 1 — `feat(07-04)`** (commit `466bed0`)
|
||||||
|
|
||||||
|
Création de `server/api/__sitemap__/urls.ts` :
|
||||||
|
|
||||||
|
- `defineSitemapEventHandler(async (event) => ...)` — auto-import `@nuxtjs/sitemap` v8
|
||||||
|
- `Promise.all([queryCollection(event, 'blog_fr')..., queryCollection(event, 'blog_en')...])` — strings littérales (Pitfall 2), event first-arg (Pitfall 1)
|
||||||
|
- `.where('draft', '=', false).order('date', 'DESC').select('path', 'date', 'updated').all()` — projection minimale
|
||||||
|
- `Map<slug, {fr?, en?}>` alimentée via `extractSlug(path)` pour détecter les paires
|
||||||
|
- Pour chaque slug :
|
||||||
|
- si bilingue (`fr && en`) → `alternatives: [{hreflang:'fr'}, {hreflang:'en'}, {hreflang:'x-default' → FR}]`
|
||||||
|
- sinon → `alternatives: []`
|
||||||
|
- Pousse 1 à 2 entrées `SitemapUrl` par slug avec `lastmod = updated ?? date`
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
**Deviation mineure — Rule 3 (blocking issue) : import explicite `queryCollection` depuis `'@nuxt/content/server'`**
|
||||||
|
|
||||||
|
- **Plan prescrivait** : compter sur l'auto-import Nitro de `queryCollection`
|
||||||
|
- **Problème** : `pnpm typecheck` (vue-tsc) ne résout pas l'auto-import Nitro pour ce fichier (signature client `(collection)` prise au lieu de la signature Nitro `(event, collection)`), erreurs `TS2554: Expected 1 arguments, but got 2`.
|
||||||
|
- **Fix** : ajout `import { queryCollection } from '@nuxt/content/server'` — exporte la bonne signature Nitro `(event, collection) => CollectionQueryBuilder`. Runtime identique, types résolus.
|
||||||
|
- **Impact** : aucun — le runtime Nitro route le même fichier `runtime/server.js`. La fonction retourne correctement les données côté SSR dev.
|
||||||
|
|
||||||
|
**Deviation mineure — Rule 1 (pitfall found during verify) : import initial `defineSitemapEventHandler` from `'#imports'` erroné**
|
||||||
|
|
||||||
|
- Le plan importait explicitement `defineSitemapEventHandler` depuis `#imports` → `TS2305: has no exported member`.
|
||||||
|
- `defineSitemapEventHandler` est un **auto-import** global (déclaré par `@nuxtjs/sitemap` module setup), pas un export nommé de `#imports`.
|
||||||
|
- Fix : suppression de l'import explicite — l'auto-import se résout correctement.
|
||||||
|
|
||||||
|
**Aucune autre déviation**. Aucun fichier hors `server/api/__sitemap__/urls.ts` modifié.
|
||||||
|
|
||||||
|
## Acceptance Criteria — tous passés
|
||||||
|
|
||||||
|
Validés sur `pnpm dev` (port 3001, cf. 07-01) avec fixtures temporaires `_sitemap-smoke.md` (FR+EN, draft:false, updated:2026-04-22) ajoutées le temps du test puis supprimées :
|
||||||
|
|
||||||
|
- [x] `test -f server/api/__sitemap__/urls.ts` — présent
|
||||||
|
- [x] `grep "queryCollection(event, 'blog_fr')"` et `grep "queryCollection(event, 'blog_en')"` — 1 match chacun
|
||||||
|
- [x] `grep "'x-default'"` — présent (ligne bilingual alternatives)
|
||||||
|
- [x] `grep "draft.*false"` — présent (2 matches, un par locale)
|
||||||
|
- [x] `pnpm typecheck` — 0 erreur sur `server/api/__sitemap__/urls.ts` (erreur pré-existante sur `app/pages/blog/[slug].vue:136` `ogLocale` du Plan 07-02, hors scope — cf. Deferred Issues)
|
||||||
|
- [x] `curl http://localhost:3001/api/__sitemap__/urls` — retourne JSON `SitemapUrl[]` valide (2 entrées par article bilingue, alternatives complètes)
|
||||||
|
- [x] `curl http://localhost:3001/__sitemap__/fr-FR.xml | grep '/fr/blog/_sitemap-smoke'` — match
|
||||||
|
- [x] `curl http://localhost:3001/__sitemap__/en-US.xml | grep '/en/blog/_sitemap-smoke'` — match
|
||||||
|
- [x] `grep 'hreflang="x-default"' fr-FR.xml` — 9 occurrences (8 pages site + 1 article bilingue)
|
||||||
|
- [x] `grep 'test-kotlin-syntax' sitemap.xml` — 0 match (T-07-06 mitigation confirmée : drafts filtrés)
|
||||||
|
|
||||||
|
## Deferred Issues
|
||||||
|
|
||||||
|
**Hors scope de ce plan (pre-existing errors)** :
|
||||||
|
|
||||||
|
- `app/pages/blog/[slug].vue(136,17): error TS2322` — `ogLocale: () => (...)` type mismatch avec `useSeoMeta`'s `MaybeFalsy<"fr-FR">`. Remonte au Plan 07-02 (useSeoMeta enrichment). Ce fichier n'a pas été modifié par 07-04. À corriger en phase de polish ou plan suivant si non déjà listé.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
Aucun. L'endpoint est pleinement fonctionnel — il retourne `[]` naturellement quand la seule entrée de contenu est draft (comportement attendu, D-10).
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
Aucun nouveau surface de menace. Le plan documentait T-07-06 (IDisclo drafts) — **mitigation confirmée** : `grep test-kotlin-syntax` sur le sitemap final renvoie 0 (draft explicitement filtré par `.where('draft', '=', false)` dans les deux branches).
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `server/api/__sitemap__/urls.ts` — FOUND (76 lignes)
|
||||||
|
- Commit `466bed0` (feat Task 1) — FOUND in git log (`git log --oneline | grep 466bed0`)
|
||||||
|
- Endpoint runtime validé via curl (SitemapUrl[] JSON valide, XML final contient les URLs blog + alternates x-default)
|
||||||
|
- Fixtures de test nettoyées (`content/fr/blog/` et `content/en/blog/` ne contiennent que `test-kotlin-syntax.md` draft)
|
||||||
Reference in New Issue
Block a user