68 · Locale parity audit, FR + EN (2026-05-19)
TL;DR
Architecture i18n du site : routage via src/proxy.ts, header x-locale
injecté, aucun fichier miroir sous src/app/fr/* (sauf 3 sitemap routes).
Pattern shared-component-by-header : src/app/**/page.tsx lit headers().get("x-locale")
puis branche le rendu sur le dictionnaire src/lib/i18n/dictionaries/{en,fr}.json.
Verdict: Parité de routes 100 % (49 / 49 EN+FR 200 OK). Hreflang +
html[lang] corrects sur 10 / 10 samples. Seul gap reel: la LANDING_PATHS
du sitemap omettait 19 pages publiques indexables, parité-symétrique mais
sous-représentation SEO. Corrigé.
1. Architecture (rappel)
| Aspect | Implémentation |
|---|---|
| EN par défaut | aucun préfixe (/about/) |
| FR | préfixe /fr/ (/fr/about/) |
| Détection | getLocaleFromPath() dans src/proxy.ts |
| Rewrite | /fr/foo → rewrite interne vers /foo + header x-locale: fr |
| Geo redirect | / racine + IP FR/BE/LU/MC/CA-QC → 307 vers /fr/, cookie locale-pref 1 an |
| Crawlers | exemptés du geo redirect, voient toujours EN racine canonique |
| Hreflang fallback | src/app/layout.tsx émet en/fr/x-default pour toutes les pages |
| Hreflang précis | generateMetadata().alternates.languages par page (override layout) |
Conséquence : pas besoin de double arborescence. Une seule page.tsx sert
EN et FR. Audit "EN-only vs FR-only" inapplicable au sens fichier.
2. Inventaire routes publiques (60)
src/app/**/page.tsx hors admin/, account/, api/, fr/sitemap-* :
60 entrées. 49 statiques, 11 dynamiques ([slug], [market], [role],
[key], [n], [id]).
3. Audit prod (https://insiders-trades.com)
3.1 Codes HTTP, EN + FR symétriques
49 routes statiques curl en parallèle (xargs -P 12). 49 / 49 EN = 200,
49 / 49 FR = 200. Aucun 404. Aucune asymétrie.
Échantillon :
| Path | EN | FR (/fr$path) |
|---|---|---|
/ |
200 | 200 |
/about/ |
200 | 200 |
/pricing/ |
200 | 200 |
/companies/ |
200 | 200 |
/compare/tipranks/ |
200 | 200 |
/methodologie/ |
200 | 200 |
/docs/competitive/ |
200 | 200 |
/use-cases/quant-fund/ |
200 | 200 |
3.2 html[lang] correct
Échantillon 10 pages, UA non-crawler :
/→ suit le 307 geo vers/fr/, rendlang="fr"(comportement attendu, edge node CDG1 → IP FR-inferred)./avec UAGooglebot/2.1→lang="en"(canonique préservé, ok).- Toutes les autres pages EN →
lang="en". Toutes les FR →lang="fr".
3.3 Hreflang complétude
Layout src/app/layout.tsx lignes 326-328 émet systématiquement :
<link rel="alternate" hreflang="en" href="…">
<link rel="alternate" hreflang="fr" href="…/fr/…">
<link rel="alternate" hreflang="x-default" href="…">
10 / 10 pages samplées contiennent les 3 tags. /companies/ et
/methodologie/ ont 2 occurrences (layout + page-level alternates) :
acceptable (Google déduplique), mais redondant. Non-bloquant.
3.4 Contenu FR effectivement traduit
Spot-check titres <title> + <h1> :
| Path | EN title | FR title |
|---|---|---|
/pricing/ |
"Pricing · Free, Pro €19/mo…" | "Tarifs · Free, Pro 19 €/mois…" |
/compare/tipranks/ |
"Sigma vs TipRanks, insider trading comparison (2026)" | "Sigma vs TipRanks, comparatif insider trading (2026)" |
/how-it-works/ |
"How it works · …" | "Comment ça marche · …" |
Translations effectives. Pas de fallback EN sur les pages FR samplées.
4. Gap identifié : sitemap incomplet
src/lib/sitemap-shared.ts → LANDING_PATHS listait 41 chemins. Diff
contre les 49 routes statiques publiques : 19 pages indexables
manquantes (et symétriquement manquantes côté FR puisque le sitemap les
émet par locale).
Pages publiques avec <meta robots="index, follow"> mais absentes du
sitemap :
- Marketing/conversion :
/about/,/how-it-works/,/pricing/,/privacy/,/terms/,/newsletter/,/use-cases/quant-fund/ - Competitive :
/compare/,/compare/{gurufocus,openinsider,quiver-quant,tipranks,whalewisdom}/ - Discovery/signal :
/clusters/,/clusters/recent/,/earnings-radar/,/heatmap/,/historical-archive/,/leaderboard/insiders/,/sectors/,/top-movers/,/recommendations/ - Docs :
/docs/competitive/
Pages légitimement exclues (auth privé, redirect, app interne) :
/auth/*, /portfolio/, /upgrade/, /status/, /companies/add/,
/contact/ (déjà présent).
Fix : 19 entrées ajoutées à LANDING_PATHS avec changefreq + priority
calibrés. Chaque entrée est rendue 2× dans le sitemap (EN + FR) via la
boucle for lang of LOCALES dans sitemap-static/[lang]/index.xml/route.ts.
5. Compteurs
| Métrique | Avant | Après |
|---|---|---|
| Routes publiques EN | 49 | 49 |
| Routes publiques FR | 49 | 49 |
| Routes EN-only (file) | 0 | 0 |
| Routes FR-only (file) | 0 | 0 |
| Pages manquantes côté EN prod | 0 | 0 |
| Pages manquantes côté FR prod | 0 | 0 |
| Pages indexables hors sitemap | 19 | 0 |
| Entrées sitemap publiques (par locale) | 41 | 60 |
Mirrors src/app/fr/* ajoutés |
0 | 0 (non applicable) |
6. Recos suivantes (non-bloquant)
- Dédupliquer hreflang : layout émet déjà en/fr/x-default. Les pages
qui définissent
generateMetadata().alternates.languagesdoublonnent dans le<head>. Soit retirer la couche layout, soit retirer la couche page. Google tolère mais la signal est plus propre sans duplication. /contact/priorité 0.4 dans LANDING_PATHS : page importante pour conversion B2B, envisager 0.5+.- Trailing-slash + ?market filter :
/insiders/?market=fretc. sont actuellement noindex. Si certains filtres deviennent canoniques, prévoir un slug propre (/insiders/by-market/fr/) déjà couvert mais à monitorer. - Pagination companies/insiders : page 1 =
/companies/+ pages 2-N =/companies/page/N/. Le sitemap émet bien la suite (N >= 2), parité OK. - Audit i18n dictionnaire :
src/lib/i18n/dictionaries/{en,fr}.jsonà diff systématique en CI (clés FR manquantes → fallback EN sur page FR). À mettre danslint:i18nfutur.
7. Bloc preuve
Prod 200 EN: 49/49 (curl HEAD, UA Mozilla/5.0, 2026-05-19T11:15Z)
Prod 200 FR: 49/49 (idem)
html[lang]: 10/10 samples corrects (crawler UA pour racine)
Hreflang: 10/10 samples contiennent en+fr+x-default
Sitemap: 19 entrées ajoutées à LANDING_PATHS
TS: tsc --noEmit OK
Lint: lint:emdash OK, lint:emoji OK (443 fichiers)
8. Fichiers modifiés
src/lib/sitemap-shared.ts— +19 entréesLANDING_PATHS.docs/method-review/68-locale-parity-2026-05-19.md— ce document.