# État des lieux — données visibles par stade & page

> Inventaire exhaustif des données affichées sur chaque page « data » de l'app, par section (stade) du pipeline SEO. **Pages Overview exclues** (vue agrégée, hors périmètre).
> Établi le 2026-06-09. Pages stub (« en construction ») signalées comme telles.

Conventions : chaque page indique sa **source** (query/action serveur), ses **colonnes de table**, ses **KPI**, ses **filtres/vues**, ses **actions**, et ses **panneaux/dialogs**. Toutes les pages « data » sont des Client Components hydratés par un **initial fetch RSC** (props depuis la `page.tsx` serveur, pas de fetch au montage — voir [docs perf / fix deadlock](#)).

---

## Keywords

Source commune : `fetchKeywordsForSiteAction(siteId)` (RSC initial, refetch client uniquement au changement de site actif). Le type `Keyword` (`src/lib/keywords/types.ts`) porte : `term`, `locale{lang,country}`, `theme`, `status`, `intent[]`, `stages[]`, `primarySource`, `sources[]`, `metrics{volume,kd,cpc,growthPct,competitiveDensity?,numberOfResults?,updatedAt}`, `trend[]` (volume mensuel), `serpFeatures[]`, `briefs{}`, `gsc{clicks,impressions,ctr,position}`.

### Reservoir — `/keywords/reservoir`
Tous les mots-clés du site actif. Import GSC réservé à l'owner.

**Colonnes** (`columns.tsx`) :

| id | Libellé | Contenu |
|----|---------|---------|
| select | (checkbox) | Sélection (shift-click = plage) |
| term | Terme | Drapeau pays + terme + badge `GSC` (tooltip position/clics/impr) + actions au survol |
| theme | Thème | Label de thème (renommable, depuis le contexte thèmes) |
| status | Statut | Badge (idea / watch / active / archived) |
| intent | Intent | Pills multi (Info / Commercial / Transac / Navig) |
| volume | Volume | Volume mensuel du `primarySource` (fr-FR) |
| kd | KD | Difficulté 0–100 (jauge anneau) |
| cpc | CPC | CPC USD (anneau coloré relatif min/max) |
| growthPct | Croissance | % croissance (vert ≥0 / rouge <0), N/A si null |
| trend | Tendance | Sparkline volume 12 mois |
| lang | Langue | Langue locale (masquée par défaut ; pilote le filtre langue) |
| updatedAt | MAJ | Date màj métriques (masquée) |
| competitiveDensity | Comp. Density | Densité concurrentielle Semrush (masquée) |
| numberOfResults | # Results | Nb résultats Semrush (masquée) |
| source | Source | Badge source primaire (mangools/ahrefs/gsc/semrush/manual) (masquée) |

**KPI :** aucun. **Filtres** (`filters-bar.tsx`) : recherche terme ; selects Thème / Statut / Intent / Langue (si >1 langue) ; popover « Plages » (min/max Volume, KD, CPC, Croissance) ; dropdown « Colonnes » (9 colonnes toggleables).
**Actions ligne :** copier le terme ; supprimer (confirm). **Actions masse** (`bulk-actions-bar.tsx`) : changer statut ; catégorie (assigner/créer/renommer thème) ; → Recherche (stage `research`) ; → Planning (stage `planning`) ; → Contenu (créer contenu) ; supprimer.
**Toolbar :** Ajouter (dialog), Importer CSV (Mangools/Semrush, preview, ≤1 Mo), Importer GSC (owner).
**Sheet (clic ligne) :** Statut/Thème/Intent éditables ; sparkline 12 mois ; métriques par source ; SERP features.

### Recherche — `/keywords/recherche`
Mots-clés filtrés sur `stages.includes("research")`. Sync GSC owner-only.

**Colonnes** = celles du Reservoir **+** : `gscPosition` (Pos. GSC moyenne + ⚡ quick-win 11–20) · `gscOpportunity` (tri seul) · `targets` (Pays ciblés : badges candidate/planned/live).
**Filtres** = Reservoir **+** select « Positionnement GSC » (Top 10 / Quick wins / Au-delà / Non positionné) + bouton « Synchroniser GSC » (owner) + toggle « Trier par opportunité ».
**Vues :** Liste / **Matrice** (`coverage-matrix.tsx` : lignes = termes, colonnes = pays cibles, cellule = statut par pays — non ciblé/candidat/planifié/en ligne).
**Action masse :** « Cibler » des pays sur la sélection. ⚠️ **État de ciblage client-only / éphémère** (non persisté — voir Findings).
**Sheet :** comme Reservoir **+** section **Briefs YTG** (import brief `.txt`, panneau par pays : objectifs, sujets, questions, ton, value props, top terms/entités/n-grams).

### Planning — `/keywords/planning` (Chambre de Briefing)
Reconstruit le 2026-06-09 (pipeline Data-to-Content). **Source :** `getPlanningKeywords(siteId)` → mots-clés portant le **stage `planning`** (posé quand une cellule de ciblage passe à `planned` en Recherche), avec readiness (`buildContentAnalysis`) + targets par pays.
- **Table** : Terme · Pays ciblés (drapeaux) · pastilles **SERP / On-page / Brief** · action.
- **Tiroir d'enrichissement** (clic ligne, `Sheet`) : réutilise `SerpPanel` (import SERP URLs + crawl on-page) et `BriefPanel` (Briefs YTG) branchés sur le mot-clé.
- **Action** : « Préparer l'article → » (quand 3 pastilles vertes) vers la vue Dispatch.

### Dispatch — `/keywords/dispatch/[id]`
Nouveau (2026-06-09). Vue plein écran de validation avant transmutation. **Source :** `getDispatchData(siteId, keywordId)` → mot-clé + 1 entrée par pays `planned` `{ snapshot SERP, analysis readiness }`.
- **Onglets pays** ; zone gauche `SerpPanel` + `BriefPanel` du pays actif ; rail droit **Titre · Échéance · Assigné**.
- **« Envoyer en production »** (gardé sur readiness `ok`) → `createContentItemAction(keywordId, country, …)` : crée 1 `ContentItem` `brief`, passe le target `planned → live`, redirige vers `/content/planning`.

> _Page **Import/Export** (`/keywords/export-import`) supprimée : stub ; l'import réel (CSV / GSC / briefs) vit dans les dialogs Reservoir & Recherche._

---

## Backlink

### Reservoir — `/backlink/reservoir`
Reservoir **global de sources de liens au niveau organisation** (le ciblage par site vit dans le `footprint`).
**Source :** `getReservoir(activeOrgId)` → `{ sources: LinkSource[], footprint: Record<sourceId, siteId[]> }`.

**Colonnes** (`columns.tsx` + `source-badges.tsx`, `da-badge.tsx`) :

| id | Libellé | Contenu |
|----|---------|---------|
| select | (checkbox) | Sélection / tout sélectionner |
| platform | Platform | Plateforme (gras) + domaine cliquable (`url` en `_blank`) ; cible du search |
| kind | Kind | `KindBadge` (16 types : directory, ai-directory, media, community, subreddit, review-site, launch-platform, blog, forum, marketplace, newsletter, design, platform, jobs, tools, other) |
| da | DA | `DaBadge` jauge 0–100 (haut = vert = fort), null → « — » ; filtre par plage |
| linkType | Link Type | dofollow / nofollow / mixed |
| effort | Effort | low / medium / high (icône barres) |
| footprint | Ciblé | Badges 2-lettres des sites ciblant la source (site actif surligné), « — » sinon |
| language | Langue | `LanguageFlag` (drapeau depuis code langue) |
| themes | Thèmes | Liste, « — » — **masquée par défaut** (vide à ce stade) |
| tags | Tags | Liste, « — » — **masquée par défaut** |
| flags | Flags | `FlagIcons` (toxic / disavow), « — » sinon |

**KPI :** aucun. **Filtres** (`backlink-filters-bar.tsx`) : recherche plein-texte ; selects Kind / Link Type / Effort ; popover DA (min/max) ; dropdown Colonnes. Pas de pagination (rendu intégral, ~380 sources — voir Findings).
**Actions :** barre de sélection → « Ajouter à <site actif> » (`addSourcesToSiteAction`, footprint optimiste) ; toolbar → Ajouter une source (dialog), Importer CSV (preview, garde 1 Mo, gabarit).

### Profil — `/backlink/profile`
**Profil de backlinks entrants du site actif** (dimension site). Pagination + filtres serveur, import Semrush.
**Source :** `getDomainProfileStats(siteId)` + `getDomainBacklinksPage(siteId, {page,pageSize:50})`. Container `key`é par `siteId`.

**KPI** (6 `StatCard`, fr-FR) : **Backlinks** · **Ref. domains** · **% dofollow** · **% sitewide** · **Nouveaux** (`isNew`) · **Perdus** (`isLost`).

**Colonnes** (`domain-profile-columns.tsx`) :

| id | Libellé | Contenu |
|----|---------|---------|
| sourceDomain | Domaine source | Lien (`sourceUrl`, `_blank`) → `sourceDomain` |
| anchor | Ancre | Texte d'ancre tronqué, « — » si vide |
| authority | Autorité | Score numérique, « — » si null |
| dofollow | Dofollow | ✓ / ✗ (depuis `nofollow`) |
| placement | Placement | text / image / frame / form |
| firstSeen | 1ère vue | Date `firstSeen` (fr-FR) |
| status | État | Badge « Perdu » / « Nouveau » / « — » |

Champs présents mais **non affichés** : `sourceTitle`, `targetUrl`, `sponsored`, `ugc`, `sitewide`, `lastSeen` (alimente les stats).
**Filtres :** segmented Tous / Dofollow / Nouveaux / Perdus (reset page, refetch serveur) + pagination « Page X / Y · N liens » (50/page).
**Action :** Importer export Semrush (POST `/backlink/profile/import`, statut « Importé : N liens · M domaines »).

---

## Contenu

### Planning — `/content/planning`
**Source :** `getContentItemsForSiteAction(siteId)` → `ContentItem[]`. Toggle **Kanban / Tableau** (défaut = **Tableau**).

**Stades du pipeline** (colonnes Kanban / statuts contenu, `CONTENT_STATUSES`, comptés par colonne) : `idea` (Idée) · `brief` (Brief) · `writing` (Rédaction) · `review` (Relecture) · `published` (Publié). Tri intra-colonne : `dueDate` croissant (nulls en bas) puis `createdAt` récent. Carte Kanban : titre, mot-clé (drapeau + terme), icône brief si `hasBrief`, assigné, échéance (rouge si en retard & non publié).

**Colonnes table** (`content-columns.tsx`) : Titre · Statut (badge) · Mot-clé (drapeau + terme) · Assigné · Échéance (rouge si en retard) · URL (lien `↗`) · **SERP** · **On-page** · **Brief**.

**Dots d'analyse (tri-state)** — pastille colorée résumant la prépa SEO de l'article (vert = `ok`, ambre = `partial`, rouge = `none`) :
- **SERP** — export SERP concurrents. `ok` = export présent (tooltip : N concurrents, SERP features, backlinks médians, autorité médiane) ; `none` = aucun export.
- **On-page** — crawl des concurrents SERP. `none` = pas de SERP à crawler ; `partial` = SERP importé non crawlé ; `ok` = crawlé (tooltip : date, images moy., % JSON-LD).
- **Brief** — disponibilité du brief. `ok` = brief pour le pays de l'article ; `partial` = brief existant mais autre locale ; `none` = pas de brief.

**Sheet (ouvrir un article)** — dialog 80vw×80vh. Éditables : Titre, Statut, Échéance, Assigné, URL. Embarque : panneau **SERP/Concurrents** (import SERP URLs ≤1 Mo, « Crawler on-page » ; table concurrents : #, Domaine, Backlinks, Ref.dom, Page AS, Trafic, Meta, Img, Alt, JSON-LD ; ligne crawlée dépliable → titre/lang/types JSON-LD + table img→alt) et panneau **Brief** (par pays). Actions : Supprimer, Enregistrer.
**Dialog nouveau contenu :** Titre (requis), Mot-clé cible (select). **Actions :** drag-and-drop carte → `updateContentStatusAction` (optimiste) ; clic → sheet ; + Nouvel article.

### Online — `/content/online`
Contenu **publié** avec performance GSC. **Source :** `getPublishedContentAction(siteId)` → `OnlineRow[]`. Vide → lien vers Planning.

**KPI** (`online-kpis.tsx`) : **Publiés** (count) · **Clics totaux** (Σ clicks) · **Impressions** (Σ impressions) · **Position moyenne** (moyenne, « — » si aucune).

**Colonnes** (`online-table.tsx`) : Titre · URL (lien externe) · Mot-clé (drapeau + terme) · Pos. · Clics (fr-FR) · Impr. (fr-FR) · CTR (% 1 décimale).
**Filtres / vues / actions :** aucun (vue lecture seule, métriques GSC).

---

## Audit — corrections appliquées (a11y, sûres)

Passées via les skills `vercel-react-best-practices` + `web-design-guidelines`. Périmètre : a11y évidente, sans changement de logique. `tsc` + `biome` OK.

- **`backlink/domain-profile-container.tsx`** — boutons pagination `◀`/`▶` → `aria-label` (Page précédente/suivante), glyphe `aria-hidden` ; message d'import → `aria-live="polite"`.
- **`backlink/domain-profile-columns.tsx`** — colonne Dofollow : `sr-only` « Dofollow »/« Nofollow » + `title` (sens non porté par la couleur/symbole seuls).
- **`content/content-columns.tsx`** — lien URL icon-only → `aria-label` + `title` (nouvel onglet).
- **`content/content-dot.tsx`** — pastille tri-state → `role="img"` + `aria-label` (Complet/Partiel/Absent).

## Audit — findings corrigés (2026-06-09)

- ✅ **Recherche : ciblage pays persisté** — nouvelle table `keyword_targets` (PK `keyword_id`+`country`, FK cascade), migration `0011`, actions `getTargetsForSiteAction` / `setKeywordTargetAction` / `setKeywordTargetsBulkAction` (never-downgrade), fetch RSC initial + persistance optimiste sur matrice & bulk « Cibler ». Seed démo supprimé. _Vérifié par round-trip DB._
- ✅ **Stubs supprimés** — pages keywords `Planning` + `Import/Export` retirées (routes + nav).
- ✅ **Feedback mutations (toasts)** — infra `sonner` (`<Toaster/>` au root) ; `toast.error`/`success` sur les mutations Keywords (statut/thème/stage/contenu/suppression), Recherche (sync GSC, ciblage), Backlink (ajout au site + rollback optimiste, import Semrush), Contenu (changement de statut + création).
- ✅ **Reservoir backlink paginé** — `DataTable` accepte un prop optionnel `pageSize` (gardé, additif) ; reservoir = 50/page + footer. Autres tables inchangées.
- ✅ **Deep-linking URL (nuqs)** — Reservoir/Recherche keywords (`q`, status/theme/intent, view, gsc) + Reservoir/Profil backlink (`q`, kind/linkType/effort, filter, page). État partageable & persistant au refresh.
- ✅ **A11y** — lignes concurrents SERP rendues navigables clavier (`<button aria-expanded>`) ; filtres Profil groupés (`role="group"` + `aria-pressed`).

## Audit — findings restants (mineurs, différés)

- **`TooltipProvider` par ligne** (keywords `columns.tsx`, content `content-dot.tsx`) — à hisser au niveau table (perf mineure).
- **Opérabilité clavier des lignes `DataTable`** (`onRowClick`, composant partagé `ui/data-table`) — à vérifier/durcir.
- **Chrome incohérent** — `backlink/reservoir` sans `DashboardHeader`/`Breadcrumb` contrairement aux autres pages.
- **Test DB bloqué** — `db:migrate:test` échoue (Zod env « expected string, received undefined » sous `NODE_ENV=test`) → les tests DB des keywords ne tournent pas (pré-existant, infra `.env.test`).
