/* Page: Keyword rankings + page mapping (SERanking-style centerpiece) */ const { useState: useStateR } = React; function RankingsView({ client }) { const { data, loading, error } = useApi( `/api/clients/${client.id}/rankings`, [client.id] ); const [selectedKw, setSelectedKw] = useStateR(null); const [tab, setTab] = useStateR('keywords'); const [groupBy, setGroupBy] = useStateR('flat'); if (loading) return
Loading rankings...
; if (error) return
Failed to load rankings: {error}
; const KEYWORDS = data?.keywords || []; const PAGES = data?.pages || []; if (!selectedKw && KEYWORDS.length) { // Can't call setSelectedKw here (would be a side effect in render) // The table will render and first-row click sets the selection } const activeKw = selectedKw || KEYWORDS[0] || null; const visibility = 38.4; const tracked = KEYWORDS.length * 12; // pretend total is bigger return ( <>
Rankings · 142 keywords across 38 pages

Keyword rankings

Tracked keywords mapped to landing pages. Position history, SERP features, intent. Drill into any keyword for daily detail.
Visibility score {visibility}% +4.6pp Semrush · weighted
Top-3 38 +11 142 tracked · PH desktop
Top-10 76 +18 vs prev 30d
Avg position 11.4 −2.7 Lower is better
SERP features 28 Map pack · 14 · Snippet · 6 Semrush · daily
Tracked keywords
setTab('keywords')}>By keyword setTab('pages')}>By page setTab('intent')}>By intent
⌘K
{/* Left: keyword table */}
{KEYWORDS.map((k, i) => { const sel = activeKw?.kw === k.kw; const posClass = k.pos <= 3 ? 'top3' : k.pos <= 10 ? 'top10' : k.pos <= 30 ? 'top30' : 'beyond'; const delta = k.posPrev - k.pos; // positive = improved return ( setSelectedKw(k)} style={{cursor:'pointer', background: sel ? 'var(--gold-dim)' : undefined}}> ); })}
Keyword Mapped page Pos Δ Target History · 90d Vol KD SERP features
{k.kw} {k.intent} · {k.tags.join(' · ')}
{k.page} {k.pos} {delta !== 0 ? ( 0 ? 'up' : 'down'}`}> 0 ? 'arrowUp' : 'arrowDown'} size={10}/> {Math.abs(delta)} ) : } ≤{k.target} {k.vol.toLocaleString()} {k.kd}
{k.serp.length === 0 ? : k.serp.map((s, j) => ( {s} ))}
{/* Right: keyword detail panel */}

{activeKw?.kw || ''}

Position · today
{activeKw?.pos || 0}
0 ? 'up' : (activeKw?.posPrev || 0) - (activeKw?.pos || 0) < 0 ? 'down' : 'flat'}`} style={{marginTop: 6}}> {(activeKw?.posPrev || 0) - (activeKw?.pos || 0) > 0 ? : (activeKw?.posPrev || 0) - (activeKw?.pos || 0) < 0 ? : null} {(activeKw?.posPrev || 0) - (activeKw?.pos || 0) === 0 ? 'No change' : `${Math.abs((activeKw?.posPrev || 0) - (activeKw?.pos || 0))} from prev week`}
Best · 90d
{(activeKw?.history?.length ? Math.min(...activeKw.history) : 0)}
Target · ≤{activeKw?.target || 0}
Position history · 90d
Mapped page
SERVICE
karatenation.ph{activeKw?.page || ''}
38 keywords · 4 820 sessions/mo
SERP composition
{(activeKw?.serp || []).length === 0 && No SERP features detected.} {(activeKw?.serp || []).map((s, i) => ( {s} ))} 10 organic 3 ads
Top-10 competitors
{[ { d: 'karatenation.ph', p: activeKw?.pos || 0, you: true }, { d: 'manilamartialarts.com', p: 1 }, { d: 'philippinekarate.org', p: 2 }, { d: 'fitnessfirst.com.ph', p: 4 }, { d: 'rappler.com', p: 6 }, ].sort((a,b) => a.p - b.p).map((c, i) => (
{c.p} {c.d} {c.you && YOU}
))}
); } /* Position history line — for the detail panel */ function PosHistoryChart({ values }) { const w = 420, h = 140; const padL = 28, padR = 8, padT = 8, padB = 22; const innerW = w - padL - padR; const innerH = h - padT - padB; const max = Math.max(20, Math.max(...values)); const min = 1; const stepX = innerW / (values.length - 1); // Top of chart = best position (1) const yFor = (v) => padT + ((v - min) / (max - min)) * innerH; const pts = values.map((v, i) => [padL + i * stepX, yFor(v)]); const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const last = values[values.length - 1]; const lastColor = last <= 3 ? 'var(--gold)' : last <= 10 ? 'var(--accent)' : 'var(--text-muted)'; const yTicks = [1, 3, 10, 20].filter(t => t <= max); return ( {yTicks.map((t, i) => { const y = yFor(t); return ( {t} ); })} {pts.map((p, i) => ( ))} 90d ago today ); } window.RankingsView = RankingsView;