# SEO i18n — décisions, logique & risques

**Date :** 2026-06-21 · **Branche :** `fix/seo-i18n` · **Scope :** sitemap + canonical/hreflang « parfaits » et *truthful*, locales `en`/`fr` (`defaultLocale = en`).

## Objectif

Rendre les signaux SEO internationaux **conformes Google ET véridiques** : ce qu'on annonce
(sitemap, `rel=canonical`, `hreflang`) doit refléter le contenu **réellement** traduit.

## Règles Google appliquées (références)

- Chaque version liste **elle-même + toutes les locales disponibles + `x-default`** ; liens
  **bidirectionnels** ; URLs **absolues** ; même URL servie que la canonique (pas de trailing
  slash divergent). — [Localized versions / hreflang](https://developers.google.com/search/docs/specialty/international/localized-versions)
- **`rel=canonical` = même langue (self par locale)**. Pointer `/fr` → `/en` est l'erreur
  classique qui contredit `hreflang`. — [Consolidate duplicate URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
- Sitemap : uniquement des **URLs canoniques 200** (pas de redirections, pas de `noindex`),
  chaque `<url>` portant le **jeu complet** d'alternates (self inclus).

## Décisions prises (validées avec l'utilisateur)

1. **Traduire le corps FR** des 4 pages `/services/*` + `/contact` → elles deviennent
   réellement bilingues, donc `hreflang`/canonical/sitemap bilingues sont *truthful*.
2. **Source unique** `lib/seo/locale-availability.ts` (`STATIC_ROUTES`) qui pilote
   `hreflang` + canonical + sitemap pour les routes statiques. Blog/docs continuent de dériver
   leur disponibilité du contenu (déjà *truthful*).
3. **Racine nue** `https://seoinsight.app` (307 → /en|/fr) **retirée du sitemap** (on garde les
   home `/en` & `/fr` + `x-default` → `/en`).
4. **`/about`** (`noindex`, stub) **exclue du sitemap** ; son `noindex` provient désormais du
   registre (plus en dur dans la page).

## Logique cible (résumé)

| Surface | Disponibilité | Canonical | hreflang | Sitemap |
|---|---|---|---|---|
| Home, pricing, services, privacy, terms, blog index | en+fr (dico/legal réels) | self | en, fr, x-default→en | oui (par locale) |
| 4 services détaillées, contact | en+fr (après trad) | self | en, fr, x-default→en | oui |
| about | en+fr mais **noindex** | self | (ignoré car noindex) | **non** |
| docs `/docs/*` | en+fr (`en.mdx`+`fr.mdx`) | self | en, fr, x-default | oui |
| blog post traduit | en+fr | self | en, fr, x-default | oui (2 entrées) |
| blog post EN-only | en seulement | self /en | en, x-default→en | oui (/en uniquement) ; `/fr` **404** |
| racine `/` | — | — | — | **non** (307) |

## Risques & points de vigilance

### Risques de contenu (traduction)
- **Qualité rédactionnelle FR** : les bodies services/contact sont traduits par sous-agents.
  Ton marketing/SEO globalement bon, mais **relecture humaine recommandée** (idiomes, FAQ,
  testimonials, jeux de mots). Les noms propres/marques (Shopify, WordPress, Core Web Vitals,
  LCP/INP/CLS…) et toutes les données structurelles (icônes JSX, chiffres, couleurs, hrefs)
  sont **conservés à l'identique** — seul le texte visible est traduit.
- **Testimonials traduits** : les citations clients sont traduites ; si ce sont de vrais
  verbatims, traduire un témoignage peut être discutable juridiquement/éthiquement. Ici ce sont
  des placeholders (« replace with real data »), donc OK — mais à revoir quand le contenu réel
  arrivera.

### Risques techniques / SEO
- **Cohérence sitemap ↔ on-page** : sitemap et balises `hreflang` sont désormais censés dériver
  de la **même** source (`STATIC_ROUTES` + résolveurs blog/docs). Vérifier qu'aucun chemin ne
  diverge (script de cross-check dans la section Vérification).
- **`/fr` blog EN-only = 404 (volontaire)** : conforme (on n'annonce pas d'URL non traduite),
  mais un lien interne pointant un `/fr/blog/<post-EN>` créerait un 404. Les listings blog FR
  ne lient que des posts présents dans la locale courante → OK ; à re-vérifier si on ajoute des
  liens « cross-locale ».
- **`x-default` → `/en`** partout : choix standard et accepté. Pour la home auto-redirigée,
  Google tolère `x-default` pointant la version par défaut.
- **Handle Twitter `@seoinsight`** (`lib/seo/pages.ts`) : **supposé**, à confirmer/corriger.
- **`metadataBase`** (root layout) résout les URLs OG/Twitter relatives ; les canoniques/
  alternates sont, eux, **absolus** (exigence Google) — ne pas les rendre relatifs.
- **Trailing slash** : `next.config.ts` laisse `trailingSlash:false`. La normalisation dans
  `localizedAlternates` retire tout slash final → si un jour on active `trailingSlash:true`,
  il faudra réaligner canonical/alternates ET le sitemap.

### Hors-scope (à traiter séparément)
- **Labels du formulaire `ContactForm`** + wiring email (QA F-C1 / B-C1) : périmètre
  Frontend/Backend. Ici on traduit le **body** de `/contact`, pas le formulaire.
- **Coordonnées de contact placeholder** (adresse/téléphone/email fictifs) : données à remplacer.
- **Données structurées globales** (Organization `sameAs`/`contactPoint`, etc.) : périmètre
  Backend (QA B-H1/B-H2).

## Vérification

1. `bun run build` → 0 erreur, toutes pages prerendered.
2. d3k : `curl` toutes les routes EN **et** FR → 200 ; `/fr/services/*` & `/fr/contact` servent
   du **FR** ; `/fr/blog/<post-EN>` reste 404 ; `/about` non listée au sitemap.
3. **hreflang on-page** : parser `<link rel="alternate" hreflang>` sur un échantillon (home,
   1 service, contact, 1 post traduit, 1 post EN-only, 1 doc) en EN+FR → self + locales dispo +
   `x-default`, bidirectionnel, absolu, sans trailing slash, canonical same-language.
4. **sitemap.xml** : pas de racine nue, pas de `/about`, aucune URL 404/redirigée ; chaque
   `<url>` a le jeu complet `xhtml:link` (self inclus) ; namespace `xmlns:xhtml` présent.
5. **Cross-check** : ensemble `(locale, path)` du sitemap == ce qu'annonce le `hreflang`
   on-page. Zéro divergence.
6. (Optionnel) Validateur hreflang / Google Rich Results sur l'URL de preview.
