# CDC v1.3 — Analyse Sémantique SERP Pondérée

> Statut : **VERSION COURANTE DE RÉFÉRENCE** (remplace v1.2, consolidée — intègre v1.0 + v1.1 + v1.2)
> Évolution majeure : résolution des 8 conflits identifiés sur la v1.2 (scoring BM25F, extraction par archétype, mobile-first, fallback headless, intention auto, multilinguisme, curation d'URLs, guide d'écriture)
> Périmètre : **fonctionnel uniquement**. Stack, orchestration, architecture → document de design technique séparé (Phase B).

---

## 0. Changements v1.2 → v1.3

| # | Conflit v1.2 | Résolution v1.3 | Section |
| --- | --- | --- | --- |
| 1 | Formule linéaire ET BM25 « en parallèle » (vulnérable au stuffing) | **BM25F** : saturation appliquée sur la fréquence pondérée par zone ; le linéaire disparaît | §2.2 |
| 2 | Extraction Readability aveugle (purge les grilles e-commerce) | Extraction **par archétype de page** (éditorial vs transactionnel) | §3.0 |
| 3 | Zones imbriquées non tranchées (`<h2><a>`) | Zones **mutuellement exclusives**, le poids max l'emporte, jamais de cumul | §2.2 |
| 4 | User-Agent non spécifié (angle mort mobile-first) | **UA mobile type Googlebot Smartphone par défaut** | §3.0 |
| 5 | Pages JS exclues + « Top 3 incomplet = unreliable » = combo mortel | **Fallback headless ciblé Top 5** | §3.0, §5 |
| 6 | Courbe d'intention choisie manuellement | **Auto-détection** par archétypes Schema.org, choix manuel = override | §2.1 |
| 7 | Stemming/stop-words non liés à la langue | Pilotés par `<html lang>` (+ détection fallback) | §3.7 |
| 8 | Slug absent, prominence DOM ambiguë, pas de garde-fou d'intention au Gap | Zone **slug** (1.5), métrique renommée `kw_first_position_dom` et assumée, **empreinte d'archétype + alerte `intent_mismatch`** | §2.2, §3.7, §4.2 |
| + | (ajouts produit) | Curation manuelle des URLs, politique de rétention 6 h, guide d'écriture type « brief YTG » | §6, §3.8, §4.3 |

---

## 1. Vision Produit

Un outil qui ingère une SERP (mot-clé + URLs concurrentes + positions) et produit un **profil sémantique et structurel pondéré** de ce qu'il faut pour ranker, avec un mode **Gap Analysis** comparant la page du client à ce profil, et un **guide d'écriture** actionnable par un rédacteur.

**Principe fondateur :** ce qui compte est ce que font les pages les mieux classées. Chaque donnée extraite est pondérée par la position SERP de sa source.

**Utilisateurs cibles :** consultants SEO, équipes contenu, intégration dans un dashboard analytique existant.

---

## 2. Modèle de Scoring

### 2.1 Pondération par position (le « Modèle de Puissance »)

| Position SERP | CTR estimé | Poids normalisé |
| ------------- | ---------- | --------------- |
| 1             | ~31.7%     | 1.00            |
| 2             | ~24.7%     | 0.77            |
| 3             | ~18.6%     | 0.58            |
| 4–5           | 13–9%      | 0.40–0.28       |
| 6–10          | < 6%       | 0.15–0.05       |

- Fallback mathématique : $W_P = 1/\log_2(P+1)$
- **Mode calibré** par données Search Console du client (position/CTR réels)

**Sélection de la courbe (nouveau v1.3) — auto-détection, override manuel :**

- Par défaut, l'intention est **inférée** depuis les archétypes de page extraits (§3.0) :
  - ≥ 60 % du Top 10 en archétype transactionnel (`Product`, `Offer`, `CollectionPage`, `ItemList`) → courbe **transactionnelle** ;
  - dominance `Article` / `NewsArticle` / `BlogPosting` / `FAQPage` + questions dans les Hn → courbe **informationnelle** ;
  - aucune dominance ≥ 60 % → courbe **standard** + flag `mixed_intent` dans le rapport.
- Les features SERP propagées par l'API d'entrée (PAA, shopping pack, local pack…) affinent la détection quand disponibles.
- Le choix manuel du profil (`standard` / `informationnelle` / `transactionnelle` / `navigationnelle` / courbe personnalisée) reste possible en **override**.
- Le rapport indique toujours l'intention retenue et son mode : `inferred` / `manual` / `calibrated`.

### 2.2 Scoring BM25F (rang × zone, saturation intégrée)

Le score d'un terme est un **BM25F** (variante par champs de BM25) — la formule linéaire de la v1.2 est supprimée :

1. **Fréquence pondérée par zone** : $ftf(T, C_i) = \sum_z Freq(T, C_i, z) \times W_z$
2. **Saturation + normalisation de longueur** (BM25F : paramètre $k_1$, normalisation sur la longueur pondérée du document). Le keyword stuffing d'un concurrent plafonne au lieu d'exploser — il ne contamine plus le profil ni le Gap Analysis.
3. **Pondération par rang, puis agrégation** : $Score(T) = \sum_i W_{P_i} \times BM25F(T, C_i)$

La **densité brute** et les **fréquences brutes par zone** restent exportées comme indicateurs descriptifs (table `terms`, guide d'écriture §4.3) — jamais comme score.

**Zones mutuellement exclusives (résolution des imbrications) :** chaque occurrence de texte est comptée **une seule fois**, dans la zone au poids le plus fort qui la contient, selon la précédence `Title > H1 > H2/H3 > alt > ancre > meta description > body`. Exemple : `<h2><a>mot-clé</a></h2>` → compté en H2 (×1.5) uniquement. L'ancre alimente toujours l'analyse de maillage (§3.3) : **analyse ≠ scoring**, pas de double comptage.

| Zone | Poids par défaut (paramétrable) |
| --------------------- | ------------------------------- |
| Title                 | 3.0                             |
| H1                    | 2.5                             |
| H2 / H3               | 1.5                             |
| **Slug URL** (nouveau) | **1.5** — tokenisé (split `-`, `/`, `_`) ; sémantiquement fort mais court, un poids supérieur surpondérerait mécaniquement 3-4 tokens |
| Attributs `alt`       | 1.3                             |
| Ancres de liens       | 1.2                             |
| Meta description      | 1.2                             |
| Body                  | 1.0                             |
| Meta keywords         | **0.0** (extraite, jamais scorée) |

### 2.3 Fondations statistiques

- **Médiane pondérée** en plus de la somme ; détection et flag des **outliers** sémantiques
- Comparatif systématique **Top 3 vs Top 10** sur toutes les métriques

---

## 3. Matrice d'Extraction de Données

### 3.0 Fetch & stratégie d'extraction (nouveau v1.3)

**Mobile-first.** Le fetch utilise un **User-Agent mobile type Googlebot Smartphone par défaut** (paramétrable ; mode `desktop` disponible en config, jamais par défaut). Word count, arbre des Hn, lazy-loading : tout reflète ce que Google évalue réellement (indexation mobile-first).

**Fallback headless ciblé.**

- Fetch léger (HTTP simple) pour toutes les pages.
- Page détectée `js_rendered_suspected` sur une position **Top 5** → second passage en **rendu headless** (même UA mobile). Le HTML rendu remplace le HTML thin ; la page est scorée normalement, flaggée `js_rendered: true`.
- Positions 6-10 suspectées JS : pas de rendu — exclusion du scoring + flag (leur poids est faible, le coût d'un rendu ne se justifie pas).
- **Contrainte de conception — interchangeabilité** : le rendu headless est spécifié comme une interface (URL + UA → HTML rendu + flags), implémentable en local (navigateur piloté) ou via API de pre-rendering, sans impact sur le fonctionnel.

**Extraction par archétype de page.** Le pipeline détecte d'abord un **archétype**, puis choisit sa stratégie :

- **Détection** (dans cet ordre) : Schema.org dominant (`Product`/`CollectionPage`/`ItemList`/`Offer` → transactionnel ; `Article`/`NewsArticle`/`BlogPosting`/`FAQPage` → éditorial), puis heuristique structurelle en fallback (blocs répétitifs image+lien+prix, densité de `<article>` vs grilles de `<li>`).
- **Éditorial** → extraction type Readability stricte (nav/footer/sidebar exclus).
- **Transactionnel/listing** → extracteur large : la grille de produits **est** le contenu principal ; titres de fiches, prix, labels de filtres et onglets d'avis sont conservés. Nav/footer exclus par sélecteurs structurels (`<nav>`, `<footer>`, `role=`), pas par score de lisibilité.
- L'archétype est exporté dans `PageAnalysis` (champ `page_archetype`) — il alimente la détection d'intention (§2.1) et le garde-fou du Gap Analysis (§4.2).

### 3.1 Métadonnées

| Donnée | Détail | Usage |
| --- | --- | --- |
| Title | texte brut | zone de scoring (×3.0) |
| Meta description | texte brut | zone de scoring (×1.2) |
| Meta keywords | si présente | informatif uniquement, poids 0 |
| Canonical | + comparaison avec l'URL réelle | alerte si canonicalise ailleurs (mauvaise page analysée) |
| Robots meta | noindex, max-snippet, max-image-preview | signaux contextuels |
| Open Graph | og:title, og:description, og:type, og:image | complétude sociale |
| Langue | `<html lang>` + hreflang | pilote stemming/stop-words (§3.7) + contrôle de cohérence SERP (§5) |
| Slug de l'URL finale | tokenisé | zone de scoring (×1.5) |

### 3.2 Structure & lisibilité du contenu

| Donnée | Détail |
| --- | --- |
| **Word count du contenu principal** | hors nav/footer/scripts, selon la stratégie d'archétype (§3.0) — métrique de cadrage n°1 |
| Arbre des Hn complet | avec contenu textuel, profondeur max, nombre de H2 |
| Présence du mot-clé dans les Hn | par niveau |
| **Questions dans les Hn** | H2/H3 commençant par comment/pourquoi/quel/où/quand… → détection intention PAA |
| Paragraphes | nombre, longueur moyenne des phrases |
| Listes | nombre de `<ul>` / `<ol>` (corrélé aux featured snippets) |
| Tableaux | nombre de `<table>` |

### 3.3 Liens

> ⚠️ Les **backlinks** (liens entrants externes) ne sont **pas extractibles depuis la page**. Ils nécessitent une API tierce (Majestic, Ahrefs, DataForSEO) → **backlog / intégration future**.

| Donnée | Détail |
| --- | --- |
| Liens **internes** (même domaine) | nombre + ancres → stratégie de maillage |
| Liens **externes sortants** | nombre, ancres, `rel` (dofollow/nofollow/sponsored/ugc), domaines cibles |
| Attribut `alt` | si le lien enveloppe une `<img>` |
| Ratios | liens / 1000 mots (comparable entre pages), % dofollow externe |

### 3.4 Médias

| Donnée | Détail |
| --- | --- |
| Images | nombre total |
| Couverture `alt` | ratio avec/sans, contenu des alt intégré au scoring sémantique (×1.3) |
| Optimisation | formats (webp/avif vs jpeg/png), présence de `loading="lazy"` |

### 3.5 Données structurées

| Donnée | Détail |
| --- | --- |
| **Types Schema.org parsés** | Article, FAQPage, HowTo, Product, BreadcrumbList, VideoObject, Organization… |
| JSON-LD brut | conservé pour audit (seule matière brute persistée, cf. §3.8) |
| Fallback | microdata et RDFa (vieux sites sans JSON-LD) |
| Livrable type | « 7/10 concurrents ont un FAQPage, vous non » |

### 3.6 Fraîcheur & signaux techniques (gratuits au fetch)

| Donnée | Détail |
| --- | --- |
| Dates | datePublished / dateModified (JSON-LD), balises `<time>`, date visible → détection SERP « freshness » |
| TTFB | temps de réponse serveur |
| Poids | taille HTML, nombre de nœuds DOM |
| CMS / framework | détection par patterns (WordPress, Shopify, Next.js…) — contextuel |

### 3.7 Analyse sémantique

| Donnée | Détail |
| --- | --- |
| Unigrammes | fréquence par zone, stop-words filtrés, stemming |
| **N-grammes 2 et 3 mots** | « assurance auto pas cher » comme unité d'intention |
| **Co-occurrences** | termes systématiquement associés chez le Top 3 → analyse d'entités low-cost |
| Position du 1er usage du mot-clé | **`kw_first_position_dom`** : ordre de lecture dans le contenu principal extrait — approximation DOM assumée (l'ordre visuel CSS Grid/Flexbox peut différer ; la vraie prominence visuelle va au backlog) |
| Écarts Title/Body | termes présents dans title/H1 mais absents du body et inversement (clickbait vs contenu) |

**Multilinguisme (nouveau v1.3).** Stemming et stop-words sont **pilotés par la langue** :

- Le dictionnaire de stop-words et le stemmer sont chargés dynamiquement selon `<html lang>` (§3.1).
- Langue de référence du job = langue majoritaire du Top 10. Page dans une autre langue → flag `lang_mismatch` : **exclue du scoring sémantique**, conservée dans le comparatif structurel.
- `lang` absent ou incohérent → détection de langue sur le texte extrait, en fallback.
- Langue non supportée par le stemmer → repli **sans stemming** + flag. Jamais d'application du stemmer d'une autre langue.

### 3.8 Politique de rétention (nouveau v1.3)

Deux niveaux de données, sort différent :

| Niveau | Contenu | Sort |
| --- | --- | --- |
| **Artefacts bruts** | HTML fetché, DOM rendu par le headless | **Staging éphémère, TTL 6 h, purge automatique.** Jamais persistés en base — ils n'existent que le temps du processing et d'un éventuel re-run rapproché |
| **Résultats du processus** | objets `PageAnalysis`, tables `pages` / `terms`, rapport, guide d'écriture | Seuls persistés. Le JSON-LD brut (quelques Ko, utile à l'audit) est la seule matière brute conservée |

Conséquence assumée : relancer une analyse après la purge = re-fetch complet (acceptable, la SERP bouge de toute façon).

---

## 4. Livrables & Sorties

### 4.1 Rapport SERP (mode standard)

- Scores pondérés par terme (somme + médiane), unigrammes et n-grammes
- Profil structurel type : longueur cible, nombre de H2, listes/tableaux, schémas présents
- Comparatifs Top 3 vs Top 10 sur chaque métrique
- Intention détectée + mode (`inferred` / `manual` / `calibrated`), flag `mixed_intent` le cas échéant
- Indice de couverture et flags de fiabilité (cf. §5)

### 4.2 Gap Analysis (mode client)

- Input additionnel : URL (ou HTML) de la page cliente
- **Garde-fou d'intention (nouveau v1.3)** : l'archétype de la page cliente est comparé à l'intention dominante de la SERP. En cas de discordance (ex. `Article` face à une SERP à 70 % `CollectionPage`), le rapport ouvre sur une alerte **`intent_mismatch`** : les recommandations terme-à-terme ne combleront pas un écart de typologie de page. Les deltas restent calculés, mais jamais présentés comme suffisants.
- Delta terme par terme : score attendu vs score actuel, **trié par manque à gagner**
- Delta structurel : longueur, Hn, listes, schémas, liens

### 4.3 Guide d'écriture — rapport type « brief YTG » (nouveau v1.3)

En plus des scores (notre formule → analyse et priorisation), l'outil produit un **livrable rédactionnel** fondé sur les fréquences brutes, exploitable directement pour l'aide à l'écriture :

- **Termes à employer** : pour chaque terme retenu (top N par score BM25F, N paramétrable — défaut 50), une **fourchette d'occurrences cible** dérivée des statistiques du Top 3 (min / médiane pondérée / max) et la **zone recommandée** (title, Hn, body — selon où les concurrents le placent).
- **N-grammes à placer tels quels** (unités d'intention).
- **Questions à traiter** : issues des Hn interrogatifs du Top 10 (intention PAA).
- **Objectifs structurels** : longueur cible, nombre de H2, listes/tableaux, schémas à implémenter.
- Format lisible par un rédacteur (Markdown + JSON), indépendant des tables analytiques.
- En mode Gap Analysis, le guide devient une **liste d'ajustements** : termes à renforcer (avec l'écart d'occurrences), sections à ajouter.

### 4.4 Schéma de sortie fonctionnel

Objet par page :

```
PageAnalysis {
  url, final_url, serp_position, weight, fetch_status, reliability_flags[],
  page_archetype, js_rendered,
  meta:        { title, description, keywords, canonical, robots, og, lang, dates, slug_tokens[] },
  structure:   { headings_tree, word_count, paragraphs, lists, tables, questions_in_hn },
  links:       { internal[], external[], ratios },
  media:       { images_count, alt_coverage, alt_texts[] },
  schema:      { types[], raw_jsonld },
  performance: { ttfb_ms, html_size, dom_nodes },
  semantics:   { unigrams{}, bigrams{}, trigrams{}, kw_first_position_dom }
}
```

**Deux tables d'export** (format analytique, Parquet/JSON Lines) :

| Table | Grain | Contenu |
| --- | --- | --- |
| `pages` | 1 ligne / URL | tous les agrégats de la page |
| `terms` | 1 ligne / terme × page × zone | fréquence brute + score pondéré |

> La table longue `terms` rend le Gap Analysis trivial (simple jointure entre profil SERP et page cliente), l'exploitation dashboard directe, et alimente le guide d'écriture (§4.3).

### 4.5 Question tranchée : contexte SERP

Si l'input provient d'une API SERP (DataForSEO, Serper…), les **features de la SERP** (featured snippet, People Also Ask, image pack, local pack) présentes dans la réponse API sont **conservées et propagées** dans le rapport — elles conditionnent l'interprétation de la courbe CTR et affinent la détection d'intention (§2.1). L'outil ne les scrape pas lui-même (v1.x).

---

## 5. Politiques de Fiabilité (fonctionnelles)

| Règle | Comportement v1.3 |
| --- | --- |
| Concurrent inaccessible | jamais d'exclusion silencieuse : indice de couverture explicite (`coverage: 9/10, missing: [pos 1]`) + renormalisation des poids + flag |
| **Exclusion par l'utilisateur** (nouveau) | renormalisation des poids, cause `excluded_by_user` distincte de `missing`, **jamais `unreliable`** (c'est un choix, pas une défaillance) |
| Thin HTML (JS suspecté) — **Top 5** | **fallback headless** → si succès : page scorée + flag `js_rendered` ; si échec : exclusion + flag |
| Thin HTML (JS suspecté) — pos. 6-10 | exclusion du scoring + flag `js_rendered_suspected`, mention dans le rapport |
| Top 3 incomplet | `unreliable` (défaut) ou abandon (paramétrable) — **seulement après échec du fallback headless** (vrai mur : anti-bot, timeout) |
| `lang_mismatch` | exclusion du scoring sémantique, conservation dans le comparatif structurel |
| SERP mixte | flag `mixed_intent`, courbe standard appliquée |
| Faux 200 | détection soft-404, redirection home, mur RGPD, challenge anti-bot (heuristique : longueur contenu + présence du mot-clé) |
| Principe général | **l'utilisateur ne reçoit jamais une analyse dégradée sans le savoir** |

---

## 6. Entrées

| Input | Obligatoire | Format |
| --- | --- | --- |
| Mot-clé + URLs + positions | ✅ | JSON ou CSV |
| **Sélection des URLs** (nouveau) | optionnel | chaque URL peut être désactivée avant lancement (décocher Wikipedia, Reddit, YouTube…). Les positions d'origine des URLs restantes sont **conservées** pour W_P (le n°3 ne devient pas n°2) ; poids renormalisés ; couverture affichée (`coverage: 7/10, excluded: [pos 2, 5], missing: []`) |
| Réponse d'API SERP complète | optionnel | JSON (features SERP propagées) |
| URL / HTML de la page cliente | optionnel (active le Gap Analysis) | URL ou fichier |
| Export Search Console | optionnel (active le mode calibré) | CSV |
| Configuration | optionnel | profils CTR (override de l'auto-détection), poids de zones, seuils de fiabilité, stop-words custom, UA (mobile par défaut) |

---

## 7. Backlog (hors périmètre v1.x)

- Backlinks via API tierce (Majestic / Ahrefs / DataForSEO)
- Rendu headless **systématique** (v1.3 ne rend que le Top 5 suspecté JS)
- Prominence **visuelle** du mot-clé (layout réel via headless) — v1.3 assume l'approximation DOM `kw_first_position_dom`
- Scraping des features SERP en direct
- Analyse d'entités via NLP avancé (embeddings)
- Suivi temporel (évolution du profil SERP dans le temps)

---

*Prochaine étape → **Phase B** : design technique d'intégration à SEO Insight (stack, crate Rust, pipeline, stockage, branchement UI).*
