# Keyword Overview — Design

- **Date**: 2026-06-06
- **Status**: approved (design), going straight to plan + subagents
- **Section**: keywords / overview
- **Depends on**: keywords (vol/kd/cpc + GSC overlay), backlink reservoir — on `dev`.

## Goal

A per-site **keyword overview dashboard** that takes the best of Semrush / Mangools / Ahrefs but is grounded in the data we actually own: keyword portfolio (vol/kd/cpc), the real GSC positioning overlay, intent/theme/stages, and the backlink reservoir. No invented metrics (no Domain Authority, Ahrefs Rank, competitor traffic, or time-series trends — we have no history yet).

## Core principles

- **Active site** (client `useActiveSite`, like Reservoir/Recherche).
- **Real data only** — no `+%` vs last month (no history), no DA/Ahrefs/competitors.
- **Aggregation is a pure, tested function**; the page is a thin client container + focused presentational modules.
- **Project design system** (shadcn/base-ui), NOT the Stitch theme — stay consistent with Reservoir/Recherche (same KD/CPC pills, badges, flags).

## Best-of-3 mapping

- **Semrush** → position distribution buckets (top3 / 4-10 / 11-20 / 21-50 / 51-100 / non-positionné).
- **Ahrefs** → prioritized opportunities (quick-wins by opportunity score).
- **Mangools** → KD distribution + intent/theme breakdown + simplicity.

## Layout (active site)

```
HERO — Performance GSC (28j): positionnés (n + %), pos. moyenne, clics, impressions,
       CTR ; + horizontal position-distribution bar with legend.
KPI row (6, responsive 4→2→1): Mots-clés · Positionnés · Quick-wins · Volume cumulé ·
       Clics GSC 28j · KD moyen.
Row: Opportunités (quick-wins) [~2/3]  |  Répartition Intent + Thème [~1/3].
Row: Distribution KD  |  Top mots-clés par volume.
Row: Pipeline (Reservoir → Recherche → Planning)  |  Backlinks (reservoir) card.
```

## Data model — the aggregate

`src/lib/keywords/overview.ts`:
```ts
import type { SearchIntent } from "./types";
import type { BacklinkStatus } from "@/lib/db/domain-schema";

export type PositionBucket =
  | "top3" | "top10" | "quickwin" | "deep" | "far" | "none";

export interface CountBy<K> { key: K; count: number }

export interface OpportunityRow {
  id: string; term: string; volume: number; position: number;
  kd: number; impressions: number; score: number;
}
export interface TopVolumeRow {
  id: string; term: string; volume: number; position: number | null;
}

export interface KeywordOverview {
  totalKeywords: number;
  positioned: number;          // gsc != null
  positionedPct: number;       // 0..100, rounded
  avgPosition: number | null;  // mean gsc.position over positioned
  totalClicks: number;         // Σ gsc.clicks
  totalImpressions: number;    // Σ gsc.impressions
  avgCtr: number | null;       // totalClicks / totalImpressions (0..1)
  quickWins: number;           // positionBucket === "quickwin"
  totalVolume: number;         // Σ metrics[primarySource].volume
  avgKd: number | null;        // mean metrics[primarySource].kd (present only)
  positionDistribution: CountBy<PositionBucket>[]; // all 6 buckets, in order
  kdDistribution: CountBy<string>[];   // "0-20","21-40","41-60","61-80","81-100"
  byIntent: CountBy<SearchIntent>[];   // 4 intents, desc by count
  byTheme: CountBy<string>[];          // theme slug ("—" for null), top 6 by count
  topByVolume: TopVolumeRow[];         // top 10 by volume desc
  opportunities: OpportunityRow[];     // top 8 by opportunityScore desc (>0)
  stages: { reservoir: number; research: number; planning: number };
  backlinks: { total: number; byStatus: CountBy<BacklinkStatus>[] } | null;
}
```

Helpers (pure, in `gsc-positioning.ts`, reusing `opportunityScore`):
```ts
export function positionBucket(gsc: GscTracking | null | undefined): PositionBucket {
  if (!gsc) return "none";
  if (gsc.position <= 3) return "top3";
  if (gsc.position <= 10) return "top10";
  if (gsc.position <= 20) return "quickwin";
  if (gsc.position <= 50) return "deep";
  return "far";
}
```
(`quickwin` here matches `gscBand`'s 11–20 band, so `quickWins` stays consistent.)

`buildKeywordOverview(keywords: Keyword[], backlinks: { status: BacklinkStatus }[] | null): KeywordOverview` — the single pure aggregator. Reads `k.metrics[k.primarySource]?.{volume,kd}` (guarded), `k.gsc`, `k.intent`, `k.theme`, `k.stages`.

## Flow

`getKeywordOverviewAction(siteId)` (server action):
1. `getKeywordsForSite(siteId)` (existing query) → `Keyword[]`.
2. Query backlink status counts for the site (`select status from backlinks where site_id = …`), or null if the table/rows are absent.
3. `return buildKeywordOverview(keywords, backlinks)`.

`OverviewContainer` (client): `useActiveSite()` → load the action on `activeSiteId` (mirrors Reservoir's `load()` pattern) → render modules. Loading skeleton; empty-states.

## Components / files

```
src/lib/keywords/overview.ts            — KeywordOverview types + buildKeywordOverview (pure)
src/lib/keywords/overview.test.ts       — unit tests (aggregation, buckets, empty)
src/lib/keywords/gsc-positioning.ts     — add positionBucket (+ test)
src/lib/keywords/actions.ts             — getKeywordOverviewAction (or overview-actions.ts)
src/components/keywords/overview/overview-container.tsx   — client loader + grid
src/components/keywords/overview/gsc-performance-hero.tsx — hero + position-distribution
src/components/keywords/overview/position-distribution.tsx — stacked bar + legend
src/components/keywords/overview/overview-kpis.tsx        — 6 stat cards
src/components/keywords/overview/opportunities-card.tsx   — quick-wins table
src/components/keywords/overview/breakdown-card.tsx       — intent + theme bar lists
src/components/keywords/overview/kd-histogram.tsx         — KD bars
src/components/keywords/overview/top-volume-card.tsx      — top-by-volume list
src/components/keywords/overview/stages-funnel.tsx        — reservoir→research→planning
src/components/keywords/overview/backlinks-card.tsx       — reservoir backlink summary
src/components/keywords/overview/stat-card.tsx            — shared KPI primitive
src/components/keywords/overview/bar-list.tsx             — shared horizontal-bar list
src/app/(app)/keywords/overview/page.tsx                  — header + <OverviewContainer/>
```

Charts are CSS/flex bars (no chart lib — consistent with the custom SVG `Sparkline`). Reuse `KdBadge`/`CpcBadge`, `countryFlag`, theme labels where relevant.

## Error / empty states

- No keywords for the site → centered empty-state with CTAs (Importer / Importer depuis GSC).
- Keywords but no GSC data (`positioned === 0`) → hero shows a "Synchronise GSC" invite instead of the performance numbers; the rest (portfolio modules) still render.
- No backlinks → backlinks card shows "Aucune source" + link to the Backlink section.
- Action failure → the container shows a non-blocking error message (fire-and-forget convention).

## Testing

- **Unit** (`overview.test.ts`, pure): `buildKeywordOverview` over a fixture set — verifies counts, positionedPct, avgPosition/avgCtr/avgKd, all six position buckets, kd buckets, byIntent/byTheme ordering, topByVolume order, opportunities order (by score, >0 only), stages counts, backlinks null vs present. `positionBucket` boundary test (3, 10, 20, 50, 51, null).
- **Manual (d3k)**: active site with synced GSC → hero numbers + distribution match; KPIs correct; opportunities link to recherche; KD histogram + top-volume + funnel + backlinks render; empty-state on a blank site; `d3k errors` clean.

## Non-goals (YAGNI)

- Time-series / trend deltas (no GSC history) — future once snapshots are stored.
- Competitor traffic share, Domain Authority, Ahrefs Rank (data we don't have).
- Date-range picker, per-country/page dimensions.
- The Stitch dark theme (we keep the project's design system).
