# Keywords Planning — readiness table + prompt generator (Slice 1)

**Date:** 2026-06-18
**Status:** design approved (sections), pending spec review
**Scope:** Slice 1 of a larger "production cockpit" effort on `keywords/planning`.

## Context & goal

`keywords/planning` is today a "Chambre de Briefing" (`BriefingContainer`): a table
of keywords in the `planning` stage with a side `Sheet` exposing `SerpPanel` +
`BriefPanel` + promote-to-content.

The goal of this slice is to turn Planning into a **production cockpit**: per
keyword, show how *ready* the gathered data is (SERP / on-page / brief), and
generate a **ready-to-paste prompt** that hands an external agent (Claude Code,
Claude Desktop, ChatGPT, Gemini) or a human writer everything needed for
competitor-based writing — grammages and terms-to-include (YourTextGuru), SERP
top-10, metrics, intent/PAA.

Driven by two Claude-Design mockups (`Planning & Overview.html`,
`Modal Sources & IA.html`), **adapted to our needs** (the user's directive):
faithful to the intent, not pixel-perfect; we diverge where our data/architecture
require it.

## What already exists (reuse, do not rebuild)

- **YourTextGuru / Lumenia brief**: `ytg-parse.ts` + `ytg-types.ts` parse the
  `premium-*.txt` (TOP TERMS, 2/3-grams, NAMED ENTITIES) and `*-brief.txt`
  (intent, objectives, questions, tone). Stored in `keywords.briefs` jsonb
  (`KeywordBrief` per country). Imported via `import-brief-dialog.tsx`.
- **Readiness model**: `buildContentAnalysis(serp, briefs, locale)` already
  returns `ContentAnalysis` with `serp.state` (ok/none), `onpage.state`
  (none/partial/ok, from `snapshot.crawledAt` + competitor crawl), `brief.state`
  (ok/other-locale/none).
- **Per-keyword data in DB** (`keywords` table): `metrics` (`SourceMetrics`:
  volume, kd, cpc, growthPct) per source, `trend`, `serp` (`SerpSnapshot` top-10
  competitors with backlinks/refDomains/pageAuthority + crawl metadata: title,
  metaDescription, images, hasJsonLd…) per country, `gsc`/`gscByCountry`,
  `briefs` (YTG), `sources[]`, `primarySource`.
- **Queries/actions**: `getPlanningKeywords(siteId)` → `PlanningRow[]`,
  `removeKeywordFromStageAction`, `add-keyword-dialog`, promote-to-content.
- **MCP server**: `src/app/api/mcp/route.ts` (`seo-insight-checklist`) — exposes
  checklist tools only; authenticated by API key. No keyword/brief tools yet.

## Design adaptations from the mockups

1. **Overview = centered modal, not a right-side drawer.** Consistent with the
   app's other modals (e.g. Sources & IA). The mockup's `.drawer` becomes a
   `Dialog`.
2. **Prompt data source is target-dependent.** Inline-embedded data for Claude
   Desktop / ChatGPT / Gemini; **MCP tool references** for Claude Code
   (`seo-insight.getKeyword`, `getBrief`, `fetch`) — which requires adding those
   read-only MCP tools (in scope, see below).
3. **Mono-keyword only.** The bulk bar ("Prompt IA" / "Générer briefs") and the
   full Sources & IA modal (import auto-detection + connectors grid) are later
   slices.

## Out of scope (later slices)

- Bulk multi-keyword prompt generation.
- Sources & IA modal: import dropzone auto-detection + connectors grid.
- New source ingestion (Screaming Frog on-page CSV, real top-10 competitor
  markdown/word-count scraping).
- **MCP billing/metering policy** for the new read tools — they *do* count toward
  the monthly MCP metering (0032) by default. A future decision may exempt
  read-only `getKeyword`/`getBrief`; **noted for future implementation**, not
  changed here.

## Architecture

### UI

**`keywords/planning/page.tsx` (RSC)** — unchanged in spirit. Loads
`getPlanningKeywords(siteId)`; the row projection carries what the table needs:
`term, country, vol, kd, intent[], serpN, states {serp,onpage,brief}, sources[]`
(states straight from `ContentAnalysis`). Renders `<PlanningTable>`.

**`PlanningTable` (client)** — replaces `BriefingContainer`'s table UI.
- Columns: ☐ · **Terme** (term + "intent · SERP top N") · **Pays** (flag) ·
  **Vol** · **KD** (colored) · **SERP** ● · **On-page** ● · **Brief** ● ·
  **Sources** (mini logos) · ✕.
- Dot mapping: `ok → prêt (green)`, `partial`/`other-locale → en cours (amber)`,
  `none → manquant (red)`.
- Toolbar: search + country filter + status filter + legend.
- Header actions: **Importer** (wired to existing import flows; full Sources & IA
  modal is slice 2) + **Ajouter un mot-clé** (reuse `add-keyword-dialog`).
- ✕ = `removeKeywordFromStageAction`. Row click → opens `KeywordOverviewModal`.
- Row selection (☐) kept for the future bulk slice; no bulk action bar yet.
- Promote-to-content stays reachable (row action or a button inside the modal).

**`KeywordOverviewModal` (client, centered `Dialog`)** — adapted from the drawer.
- On open: calls `getKeywordPromptBundle(keywordId)` → loading → content.
- **Complétude**: % + bar (from `computeReadiness`).
- **Vérification des sources**: rows for Métriques (primary source) · GSC ·
  SERP/Concurrents · Crawl on-page · Brief YTG — each with a status pill + a
  *navigational* action (Importer → import dialog; Connecter GSC →
  `/settings/integrations`; Crawler/Récupérer → existing SERP/crawl action if
  present; Brief → `import-brief-dialog`). "Tout revérifier" re-fetches the bundle.
- **Prompt à injecter**: target selector (Claude Code / Desktop / ChatGPT /
  Gemini) + task toggle (brief / rédaction / optim) + preview + Régénérer +
  **Copier**. Built client-side via the shared pure builder → instant switching.
- Warnbox preserved: incomplete sources → "the prompt uses available data and
  flags what's missing".

### Backend

**`getKeywordPromptBundle(keywordId)`** — server action,
`src/lib/keywords/prompt/actions.ts`.
- Org guard: keyword must belong to a site in the active org (reuse
  `assert-site`/`access`).
- Returns `KeywordPromptBundle`:
  `{ term, country, lang, intent[], primarySource, vol, kd, trend?,
     serp: SerpCompetitor[] (top-10), brief: KeywordBrief | null,
     readiness: { serp, onpage, brief }, sources[] }`.

**`build-prompt.ts`** — shared **pure** module, `src/lib/keywords/prompt/`.
- `buildKeywordPrompt(bundle, { target, task }) → string` — deterministic, no I/O.
- `target` ∈ `claude-code | claude-desktop | chatgpt | gemini`;
  `task` ∈ `brief | redaction | optim`.
- Claude Code: references MCP tools (`seo-insight.getKeyword`, `getBrief`,
  `fetch`) + "write the page into the project" framing.
  Others: inline data (metrics, SERP top-10 tabulated, YTG grammages/terms,
  intent/PAA).
- `computeReadiness(bundle) → { pct, items: {label, state}[] }` — pure.

**MCP read tools** — extend `src/app/api/mcp/route.ts`.
- `getKeyword({ term | id })` → metrics + SERP. `getBrief({ term | id })` →
  `KeywordBrief`. Read-only, scoped to the API key's org. Reuse existing API-key
  auth. (Metering: counts by default — see Out of scope.)

## Data flow

1. Page (RSC) → `getPlanningKeywords` → light table rows.
2. Row click → modal → `getKeywordPromptBundle(keywordId)` → bundle.
3. Modal renders completeness + source verification from `computeReadiness(bundle)`.
4. `buildKeywordPrompt(bundle, {target, task})` client-side → preview; target/task
   switch re-runs the builder instantly; Copier → clipboard.
5. Claude Code target → prompt references the seo-insight MCP tools instead of
   embedding the full payload.

## Error handling

- Bundle fetch failure → modal error state + Réessayer.
- Missing data (no brief/SERP) → builder emits "non disponible / à générer",
  completeness < 100 %, warnbox shown.
- Keyword outside active org → action throws → modal error.
- Bundle is a read response (not an upload) → no Server Action 1 MB upload limit
  concern.

## Testing

- `build-prompt.test.ts`: full data / missing brief / missing SERP × 4 targets ×
  3 tasks — assert key lines; assert Claude Code emits MCP tool references and the
  others embed inline data.
- `compute-readiness.test.ts`: state weighting → pct + item labels.
- `actions.test.ts` (`getKeywordPromptBundle`): org isolation (cannot fetch
  another org's keyword) + bundle shape.
- MCP tools: `getKeyword`/`getBrief` return expected shape; auth required.

## Implementation fan-out (subagents)

Once this spec is approved, implementation parallelizes across independent units:
1. Pure builder + `computeReadiness` + tests (no deps).
2. `getKeywordPromptBundle` action + tests.
3. `PlanningTable` (table + toolbar + dots + sources).
4. `KeywordOverviewModal` (consumes builder + action).
5. MCP read tools + tests.

Shared types (`KeywordPromptBundle`, target/task enums) land first as a thin
contract so the units can proceed against it in parallel.
