# Recherche — pays du site + Shortlist (F8) + Saisonnier (F7)

**Date:** 2026-06-09
**Source de vérité:** `~/Documents/seo insight/docs/specs/keywords/recherche.md` (F4/F7/F8) + `_transverse/conventions.md`.
**Lot 1** d'un chantier plus large ; **F6 (gaps/fan-out inter-pays) reporté** (sans valeur tant qu'un site est mono-pays — Acieb cible `["FR"]`).

Construit sur la `/keywords/recherche` réalignée (vue du Réservoir + matrice de ciblage `keyword_target`).

---

## A. Prérequis — la matrice cible les pays DU SITE

**Problème:** `TARGET_COUNTRIES` (`src/lib/keywords/targets.ts`) est un set **fixe proto** = 🇺🇸 US / 🇬🇧 GB / 🇨🇦 CA / 🇦🇺 AU — sans rapport avec les sites. Acieb cible `["FR"]`. La matrice/ciblage doivent utiliser **`site.targetCountries`**.

**Décision:** **strictement `site.targetCountries`** (cadré ; pour cibler un autre pays, l'ajouter aux pays du site via Settings — hors scope ici).

- La page `recherche/page.tsx` lit le site actif (déjà dispo via `getSitesForOrg`) et passe **`countries: string[]`** (= `site.targetCountries`) à `ResearchContainer`.
- `ResearchContainer` transmet `countries` à **`TargetingBar`**, **`CoverageMatrix`**, **`research-columns`** (colonne « Pays ciblés ») — qui prennent un prop `countries` au lieu d'importer `TARGET_COUNTRIES`.
- Affichage d'un pays : `countryFlag(code)` (`src/lib/keywords/country-flag.ts`) + le code (ex. « 🇫🇷 FR »). Helper local `toCountryOption(code) → { code, flag }`.
- **Site sans pays cible** (`[]`) : la matrice et la `TargetingBar` affichent un message « Configure les pays cibles de ce site (Settings) » au lieu des colonnes.
- **Legacy:** les `keyword_target` sur des pays hors `site.targetCountries` (seed proto US/GB/CA/AU) ne s'affichent plus — comportement attendu (nettoyage DB optionnel, noté backlog).
- `TARGET_COUNTRIES` reste exporté (utilisé par la page **staged** legacy `/keywords/recherche-staged`, qu'on ne touche pas) — on **ne le supprime pas**.

---

## B. F8 — Shortlist (panier de triage, serveur)

**But (CdC F8):** construire une sélection de travail (sur plusieurs filtres/pages) avant de tout pousser d'un coup. **Persistance serveur** (durable, par site).

- **Table** `keyword_shortlist` (`src/lib/db/domain-schema.ts`) : `siteId` (FK→sites cascade), `keywordId` (FK→keywords cascade), `createdAt`. PK composite `(siteId, keywordId)`. Migration Drizzle (`db:generate` + `db:migrate` + `db:migrate:test`, JAMAIS `db:push`).
- **Query/actions** (`src/lib/keywords/shortlist.ts` + `actions.ts`) :
  - `getShortlistForSite(siteId): Promise<string[]>` (ids) — seed RSC.
  - `addToShortlistAction(siteId, ids): { count }` — insert `onConflictDoNothing`.
  - `removeFromShortlistAction(siteId, ids): { count }` — delete.
- **UI** (sur `/keywords/recherche`) :
  - Barre de sélection (`TargetingBar`) : bouton **« + Shortlist »** → ajoute la sélection cochée.
  - Toolbar : bouton **« Shortlist (N) »** (N = taille) → ouvre un **`Sheet`** latéral listant les mots-clés du panier (terme + flag), chacun avec **retrait** ; pied du panneau : actions bulk **« Cibler pays »** (réutilise le ciblage) et **« Vider »**.
  - `shortlist: string[]` (ids) en state, seedé depuis `initialShortlist` (RSC), mis à jour optimiste + refetch sur add/remove.
- Toasts sonner sur add/remove/clear.

---

## C. F7 — Suggestion saisonnière (badge/colonne)

**But (CdC F7):** à partir de la série mensuelle, proposer de planifier un keyword **avant son pic**.

- **Util pur** `seasonalSuggestion(trend, now)` (`src/lib/keywords/seasonal.ts` + `.test.ts`, **TDD**) :
  - Entrée : `trend: MonthlyVolume[]` (`{month:"YYYY-MM", volume}`) + `now: Date`.
  - Garde-fous : `null` si < **6** points, ou si pas de **pic net** (max d'une moyenne par mois-calendaire < **1.5×** la moyenne globale).
  - Sinon : agrège la série par **mois calendaire** (1–12, moyenne), trouve le mois de pic, calcule **`monthsUntil`** = mois jusqu'à la prochaine occurrence de ce mois (à partir de `now`).
  - Retourne `{ peakMonth: 1–12, monthsUntil: number } | null` ; suggestion seulement si **`1 ≤ monthsUntil ≤ 4`** (fenêtre d'anticipation).
- **Colonne** « Saisonnier » dans `columns.tsx` (partagée) : `accessorFn` = `monthsUntil ?? 99` (tri : plus proche d'abord) ; cellule = **« 🌟 pic dans N mois »** si suggestion, sinon « — ». Ajoutée à `TOGGLEABLE_COLUMNS`, **masquée par défaut** (`DEFAULT_COLUMN_VISIBILITY`) — opt-in.
- Heuristique assumée (récurrence annuelle depuis l'historique). ~129/430 keywords Acieb ont du trend → les autres = « — ».

---

## Découpage technique
- `src/lib/db/domain-schema.ts` + migration — `keywordShortlist`.
- `src/lib/keywords/seasonal.ts` (+ `.test.ts`) — util pur saisonnier.
- `src/lib/keywords/shortlist.ts` (query `getShortlistForSite`) + `src/lib/keywords/actions.ts` (add/remove actions + `fetchShortlistAction`).
- `src/components/keywords/columns.tsx` — colonne « Saisonnier ».
- `src/components/keywords/research/targeting-bar.tsx` — prop `countries` + bouton « + Shortlist ».
- `src/components/keywords/research/coverage-matrix.tsx` — prop `countries`.
- `src/components/keywords/research/research-columns.tsx` — colonne « Pays ciblés » via `countries`.
- `src/components/keywords/research/shortlist-sheet.tsx` (nouveau) — panneau panier.
- `src/components/keywords/research/research-container.tsx` — props `countries` + `initialShortlist` ; state shortlist + handlers ; câblage Sheet/boutons. **Ne pas casser `stageFilter`.**
- `src/app/(app)/keywords/recherche/page.tsx` — passe `countries` + `initialShortlist`.
- (La page **staged** garde le comportement actuel : on lui passe aussi `countries` = `site.targetCountries` pour cohérence, mais elle reste sur `stageFilter`.)

## Tests
- **Pur (TDD):** `seasonal.test.ts` (sous-6-points → null, pic net dans la fenêtre → suggestion, pic hors fenêtre → null, série plate → null, calcul `monthsUntil` autour de l'année).
- **DB:** `shortlist` add/remove + `getShortlistForSite` (étendre `actions.test.ts`).
- **UI/live (d3k):** matrice = colonne FR (pas US/GB/CA/AU) ; « + Shortlist » → « Shortlist (N) » → Sheet liste/retrait ; colonne Saisonnier opt-in affiche les pics.

## Hors scope (backlog)
- **F6** — gaps + fan-out inter-pays (multi-pays).
- Ajout libre de pays hors `site.targetCountries` dans la matrice.
- Nettoyage des `keyword_target` legacy (US/GB/CA/AU).
- Re-check Google round-trip, métriques par pays réelles (F5).
