# Design — Module "Data-to-Content Flow"

> Spec issue du brainstorming du 2026-06-09, à partir du Cahier des Charges
> (`~/Documents/seo insight/Cahier des Charges  Module Data-to-Content Flow.md`).
> Maquettes : `.superpowers/brainstorm/<session>/content/{planning-layout,dispatch-layout}.html`.

## 1. Vision

Flux de travail d'expert ("Tour de Contrôle") reliant la recherche de mots-clés à
la production éditoriale : chaque article est adossé à un reverse-engineering SERP
et à un brief validé. Pas d'automatisation de masse — promotion d'état contrôlée,
mot-clé par mot-clé, marché par marché.

## 2. Périmètre

**Dans cette spec** — le pipeline éditorial (Phases 1→5) sur la **stack actuelle** :
Next.js 16 (RSC + Server Actions), Postgres + Drizzle, `useOptimistic`, toasts
(sonner) et URL-state (nuqs) **déjà en place**.

**Hors spec (tracks séparés)** :
- Migration **DuckDB (moteur analytique, lecture) + API Hono + workers Rust** —
  Postgres reste la source de vérité transactionnelle du flow ; DuckDB sera branché
  plus tard pour l'agrégation/croisement des métriques massives. Décision :
  *"moteur analytique, Postgres garde le flow"*.
- Les **correctifs §4 du CDC** (virtualisation→pagination réservoir backlink, infra
  toasts, nav clavier SERP, deep-linking URL, `role=group`/`aria-pressed`) sont
  **déjà livrés** (commits du 2026-06-09).

## 3. Modèle d'état (hybride : stage gate + ciblage par pays)

Unité de travail = **paire (mot-clé × pays)**.

| Mécanisme | Rôle |
|---|---|
| `keyword.stages` contient `planning` | **Porte d'entrée** de la Chambre de Briefing (filtre grossier). |
| `keyword_targets(keyword_id, country, status)` | Statut **par pays**. Enum existant **`candidate · planned · live`** (pas d'ajout `in_production`). |

- **`planned`** = engagé, prêt à enrichir/dispatcher. **`live`** = un `ContentItem`
  existe pour ce (mot-clé, pays) (≈ "in_production" du CDC).
- **Pont Recherche → Briefing** : passer une cellule à `planned` **ajoute aussi** le
  stage `planning` au mot-clé (une action, deux effets) — c'est ce qui le fait
  apparaître dans la Chambre.
- **Readiness (3 pastilles tri-state)** : SERP / On-page / Brief, calculés depuis les
  données déjà portées par le mot-clé (`serp`, on-page, `briefs`). Réutilise la
  logique tri-state existante (`content-dot` / `buildContentAnalysis`), appliquée au
  mot-clé au lieu du `ContentItem`.

## 4. Écrans & flux par phase

### Phase 1 — `/keywords/recherche` (existe déjà)
Matrice `coverage-matrix` + ciblage **persisté** (livré : `keyword_targets` +
`getTargetsForSiteAction`/`setKeywordTargetAction`/`setKeywordTargetsBulkAction`).
- Ajout : clic cellule "Non ciblé → Planifié" pose `planned` via
  `updateKeywordTargetAction` (= `setKeywordTargetAction` enrichi du **pont stage**),
  avec `useOptimistic` + toast. `revalidatePath('/keywords')`.

### Phase 2 — `/keywords/planning` — **À RECONSTRUIRE** (stub supprimé le 2026-06-09)
La "Chambre de Briefing". **Layout A** (validé) : *table dense + tiroir
d'enrichissement*.
- **Filtre** : mots-clés avec `stage = planning` (⇒ ≥ 1 pays `planned`). Fetch RSC
  initial (props + garde `loadedSite`, pas de refetch au montage).
- **Table** : terme · pays ciblés (drapeaux) · 3 pastilles · readiness.
- **Tiroir latéral** (clic ligne) — réutilise les composants existants, branchés sur
  le **mot-clé** : `serp-panel` (import SERP URLs, crawl on-page Firecrawl) +
  `brief-panel` (Briefs YTG par pays, import `.txt`).
- Pastilles évoluent `none → partial → ok` au fil de l'enrichissement.
- Bouton **« Préparer l'article »** (actif quand prêt) → `/keywords/dispatch/[id]`.

### Phase 3 — `/keywords/dispatch/[id]` — **NOUVEAU**
Vue plein écran. **Layout A** (validé) : *rail de préparation persistant à droite*.
- **RSC fetch** (lecture) : compile le mot-clé (SERP crawlés, brief YTG validé,
  targets, readiness).
- **Onglets pays** en haut (les pays `planned`).
- **Zone gauche (défilante, ~70 %)** : dashboard SERP concurrents + Brief YTG du pays
  actif.
- **Rail droit (fixe)** : Titre · Échéance · Assigné + **« Envoyer en production »**.

### Phase 4 — Transmutation (Dispatch → Content Kanban)
`createContentItemAction(keywordId, country, { title, dueDate, assignee })` —
**NOUVEAU** (remplace/adapte `createContentFromKeywordsAction`, plus grossier) :
1. crée **1 `ContentItem`** `status = brief`, lié au `keywordId`, `lang/country` du
   pays choisi ;
2. passe le `keyword_targets(keyword, country)` de `planned → live`.
- Fermeture de la vue + toast de confirmation.

### Phase 5 — `/content/planning` → `/content/online` (existe déjà)
Le `ContentItem` arrive en colonne `brief`, progresse
`brief → writing → review → published` (`updateContentStatusAction`), puis alimente
`/content/online` (réconciliation GSC : impressions, clics, CTR, position).

## 5. Server Actions

| Action | État |
|---|---|
| `updateKeywordTargetAction(keywordId, country, status)` | Adapter `setKeywordTargetAction` : `planned` ⇒ ajoute stage `planning`. |
| `setKeywordTargetsBulkAction` | Existe (never-downgrade). |
| Enrichissement SERP / on-page / brief | Réutiliser les actions existantes, appliquées au mot-clé. |
| `createContentItemAction(keywordId, country, payload)` | **Nouveau** (transmutation). |
| `updateContentStatusAction` | Existe. |

## 6. Robustesse & tests

- Toutes les mutations : `useOptimistic` + toast (infra OK) ; gardes RSC (jamais de
  read server action au montage — sinon deadlock de navigation, cf.
  `backlink-nav-deadlock`).
- Tests : logique pure (calcul readiness, transition `planned→live`, dérivation 1
  `ContentItem`/pays) en unitaire ; round-trip DB des nouvelles actions ; vérif nav
  via harnais Playwright `/debug`.

## 7. Décisions actées (résumé)
1. Fondation = Postgres maintenant ; DuckDB/Hono = track analytique séparé.
2. Unité = (mot-clé × pays) ; stage `planning` = gate, `keyword_targets` = statut prod.
3. Vocabulaire `candidate/planned/live` (pas de `in_production`) ; `live` = ContentItem créé.
4. Pont : `planned` ⇒ ajoute stage `planning`.
5. `createContentItemAction` remplace `createContentFromKeywordsAction`.
6. Layouts : Chambre de Briefing = A (table + tiroir) ; Dispatch = A (rail persistant).

## 8. Précisions actées
- **Pays du `ContentItem`** = le **pays ciblé** (onglet Dispatch / `keyword_targets`),
  **pas** `keyword.locale.country`. Ce dernier reste le marché natif du mot-clé et est
  indépendant : un même mot-clé peut engendrer un article 🇫🇷 et un article 🇧🇪,
  chacun son `ContentItem` (`country` = le target), son brief YTG et son cycle Kanban.

## 9. Différé (track séparé)
- Migration **DuckDB/Hono** (moteur analytique) : spec dédiée ultérieure.
