Hello how are you doing today?
<!--
Paste this entire block into a Divi Code Module (or any HTML file).
Uses Chart.js from CDN. Mock data included. Replace `mockData` with your API response.
-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
/* Basic dashboard styling (adjust to match Divi theme) */
.csp-dashboard { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; color:#222; padding:18px; max-width:1200px; margin:0 auto; }
.csp-row { display:flex; gap:16px; margin-bottom:18px; flex-wrap:wrap; }
.card { background:#fff; border-radius:12px; box-shadow:0 6px 20px rgba(20,20,30,0.06); padding:16px; flex:1 1 220px; min-width:220px; }
.kpi { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.kpi .label { color:#666; font-size:13px; }
.kpi .value { font-weight:700; font-size:20px; }
.small-muted { color:#888; font-size:13px; margin-top:6px; }
/* Charts area */
#charts { display:grid; grid-template-columns: 1fr 360px; gap:16px; align-items:start; }
.chart-card { padding:12px; }
.legend-pill { display:inline-block; margin-right:8px; font-size:13px; color:#444; }
/* Winners/losers */
.wl { display:flex; gap:12px; }
.wl .list { flex:1; }
.wl .item { display:flex; align-items:center; justify-content:space-between; padding:8px 6px; border-radius:8px; }
.up { color:#107c10; font-weight:700; }
.down { color:#c32a2a; font-weight:700; }
/* Table */
table.csp-table { width:100%; border-collapse:collapse; margin-top:12px; font-size:14px; }
table.csp-table th, table.csp-table td { padding:8px 6px; text-align:left; border-bottom:1px solid #f0f0f2; }
.spark-canvas { width:100px; height:26px; }
/* Responsive tweaks */
@media (max-width:920px) {
#charts { grid-template-columns: 1fr; }
.card { min-width:100%; }
}
</style>
<div class="csp-dashboard">
<h2 style="margin:0 0 10px 0;">Rank Tracker — Snapshot</h2>
<!-- KPIs -->
<div class="csp-row">
<div class="card kpi">
<div>
<div class="label">Total Keywords</div>
<div class="value" id="kpi-total">—</div>
<div class="small-muted">Tracked keywords in project</div>
</div>
<div style="font-size:28px;color:#5b8cfe;"><i class="fa fa-chart-line"></i></div>
</div>
<div class="card kpi">
<div>
<div class="label">Improved (7d)</div>
<div class="value" id="kpi-up">—</div>
<div class="small-muted">Keywords that moved up</div>
</div>
<div style="font-size:28px;color:#2dbb6b;"><i class="fa fa-arrow-up"></i></div>
</div>
<div class="card kpi">
<div>
<div class="label">Dropped (7d)</div>
<div class="value" id="kpi-down">—</div>
<div class="small-muted">Keywords that lost positions</div>
</div>
<div style="font-size:28px;color:#ff6b6b;"><i class="fa fa-arrow-down"></i></div>
</div>
<div class="card kpi">
<div>
<div class="label">Avg. Position</div>
<div class="value" id="kpi-avg">—</div>
<div class="small-muted">Lower is better</div>
</div>
<div style="font-size:28px;color:#7c3aed;"><i class="fa fa-hashtag"></i></div>
</div>
</div>
<!-- Charts and side widgets -->
<div id="charts">
<div class="card chart-card">
<div style="display:flex; align-items:center; justify-content:space-between;">
<strong>Visibility & Avg. Position (30d)</strong>
<div class="legend-pill"><small>Visibility</small></div>
</div>
<canvas id="lineChart" height="160"></canvas>
<div class="csp-row" style="margin-top:12px;">
<div style="flex:1">
<div style="font-size:13px;color:#666">Visibility</div>
<div style="font-weight:700;font-size:18px" id="visibility-score">—</div>
</div>
<div style="flex:1">
<div style="font-size:13px;color:#666">30d Change</div>
<div style="font-weight:700;font-size:18px" id="change-30d">—</div>
</div>
</div>
</div>
<div class="card">
<strong>Distribution</strong>
<canvas id="donutChart" height="260"></canvas>
<div class="small-muted" style="margin-top:8px;">Keywords by position buckets</div>
</div>
</div>
<!-- Winners / Losers -->
<div class="csp-row" style="margin-top:14px;">
<div class="card" style="flex:0 0 60%;">
<strong>Top Keywords</strong>
<div class="wl" style="margin-top:10px;">
<div class="list" id="winners-list">
<!-- Winners injected here -->
</div>
<div class="list" id="losers-list">
<!-- Losers injected here -->
</div>
</div>
<table class="csp-table" id="keywords-table">
<thead><tr><th>Keyword</th><th>Rank</th><th>Change</th><th>Volume</th><th>Trend</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="card" style="flex:0 0 36%;">
<strong>Quick Insights (AI)</strong>
<div id="ai-insights" style="margin-top:10px; color:#444">Loading AI summary…</div>
<div style="margin-top:14px;">
<strong>Search Features</strong>
<div class="small-muted" style="margin-top:6px;">Featured snippet: <b id="feat-snip">2</b> • PAA: <b id="paa">5</b></div>
<div style="margin-top:10px;">
<strong>Top Competitors</strong>
<ul id="top-competitors" style="margin-top:8px; padding-left:18px;"></ul>
</div>
</div>
</div>
</div>
</div>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
/* -------------------------
MOCK DATA (replace with API results)
------------------------- */
const mockData = {
meta: { totalKeywords: 178, visibility: 23.8, avgPosition: 17.4, change30d: "+2.3%" },
trend: {
days: Array.from({length:30}, (_,i)=> {
const d = new Date(); d.setDate(d.getDate()- (29-i));
return d.toISOString().slice(0,10);
}),
visibility: [13,14,13.5,14.2,14.8,15,15.8,16.3,16.9,17.1,17.7,18.2,18.9,19.5,19.7,20.1,20.4,20.9,21.2,21.6,22.0,22.6,23.0,23.5,23.7,23.9,24.1,24.3,24.5,23.8],
avgPos: [20,19.8,20.1,19.6,19.4,19.2,18.9,18.7,18.6,18.4,18.2,18.1,18.0,17.9,17.8,17.7,17.6,17.5,17.5,17.4,17.4,17.3,17.2,17.1,17.1,17.2,17.3,17.4,17.4,17.4]
},
distribution: { top3: 12, top10: 44, top30: 80, beyond30: 42 },
winners: [
{k:"best budget smartphone 2025", pos:8, change:+6, vol:2900, trend:[12,11,10,9,8,8]},
{k:"how to fix seo issues", pos:3, change:+5, vol:900, trend:[7,6,5,4,3,3]},
{k:"best local seo tools india", pos:13, change:+4, vol:320, trend:[20,19,18,16,14,13]}
],
losers: [
{k:"wordpress speed tips", pos:45, change:-7, vol:1100, trend:[5,7,10,12,18,45]},
{k:"onpage seo checklist", pos:22, change:-6, vol:780, trend:[8,10,13,18,20,22]}
],
features: { featured_snippet:2, paa:5 },
competitors: ["competitorA.com", "competitorB.com", "competitorC.com"],
table: [
{k:"best budget smartphone 2025", pos:8, change:+6, vol:2900, trend:[12,11,10,9,8,8]},
{k:"how to fix seo issues", pos:3, change:+5, vol:900, trend:[7,6,5,4,3,3]},
{k:"wordpress speed tips", pos:45, change:-7, vol:1100, trend:[5,7,10,12,18,45]},
{k:"onpage seo checklist", pos:22, change:-6, vol:780, trend:[8,10,13,18,20,22]}
]
};
/* -------------------------
Render KPIs
------------------------- */
document.getElementById('kpi-total').innerText = mockData.meta.totalKeywords;
document.getElementById('kpi-up').innerText = mockData.winners.length;
document.getElementById('kpi-down').innerText = mockData.losers.length;
document.getElementById('kpi-avg').innerText = mockData.meta.avgPosition;
document.getElementById('visibility-score').innerText = mockData.meta.visibility + "%";
document.getElementById('change-30d').innerText = mockData.meta.change30d;
document.getElementById('feat-snip').innerText = mockData.features.featured_snippet;
document.getElementById('paa').innerText = mockData.features.paa;
/* -------------------------
Line Chart (Visibility + Avg pos)
------------------------- */
const lineCtx = document.getElementById('lineChart').getContext('2d');
const lineChart = new Chart(lineCtx, {
type: 'line',
data: {
labels: mockData.trend.days,
datasets: [
{
type: 'line',
label: 'Visibility',
data: mockData.trend.visibility,
yAxisID: 'A',
tension: 0.3,
pointRadius: 0,
borderWidth: 2,
fill: false
},
{
type: 'line',
label: 'Avg Position',
data: mockData.trend.avgPos,
yAxisID: 'B',
tension: 0.3,
pointRadius: 0,
borderDash: [6,4],
borderWidth: 1.6,
fill: false
}
]
},
options: {
interaction: { mode: 'index', intersect: false },
stacked: false,
plugins: { legend: { display:true, position:'bottom' } },
scales: {
A: { position: 'left', title: { display:true, text:'Visibility (%)' } },
B: { position: 'right', title: { display:true, text:'Average Position' }, reverse: true }
}
}
});
/* -------------------------
Donut Chart (distribution)
------------------------- */
const donutCtx = document.getElementById('donutChart').getContext('2d');
const donut = new Chart(donutCtx, {
type: 'doughnut',
data: {
labels: ['Top 3','Top 10','Top 30','>30'],
datasets: [{
data: [mockData.distribution.top3, mockData.distribution.top10, mockData.distribution.top30, mockData.distribution.beyond30]
}]
},
options: { plugins:{ legend:{position:'bottom'} } }
});
/* -------------------------
Winners / Losers lists
------------------------- */
function makeWLItem(item, isUp=true) {
const el = document.createElement('div');
el.className = 'item';
el.style.border = '1px dashed rgba(0,0,0,0.04)';
el.innerHTML = `<div style="max-width:70%"><strong style="display:block">${item.k}</strong><small class="small-muted">Vol: ${item.vol}</small></div>
<div style="text-align:right">
<div class="${isUp ? 'up' : 'down'}">${isUp ? '+'+item.change : item.change}</div>
<div style="font-size:13px;color:#555">Pos ${item.pos}</div>
</div>`;
return el;
}
const winnersWrap = document.getElementById('winners-list');
mockData.winners.forEach(w => winnersWrap.appendChild(makeWLItem(w,true)));
const losersWrap = document.getElementById('losers-list');
mockData.losers.forEach(l => losersWrap.appendChild(makeWLItem(l,false)));
/* -------------------------
Table with mini-sparklines (creates small canvas for each row)
------------------------- */
const tbody = document.querySelector('#keywords-table tbody');
mockData.table.forEach((row, idx) => {
const tr = document.createElement('tr');
const sparkId = 'spark-'+idx;
tr.innerHTML = `<td><strong>${row.k}</strong><div class="small-muted">${row.k.split(' ').slice(0,6).join(' ')}</div></td>
<td>${row.pos}</td>
<td style="color:${row.change>0? '#0a8b3a':'#c32a2a'}">${row.change>0? '+'+row.change:row.change}</td>
<td>${row.vol.toLocaleString()}</td>
<td><canvas id="${sparkId}" class="spark-canvas"></canvas></td>`;
tbody.appendChild(tr);
// sparkline chart
const ctx = document.getElementById(sparkId).getContext('2d');
new Chart(ctx, {
type: 'line',
data: { labels: row.trend.map((_,i)=>i+1), datasets:[{data: row.trend, tension:0.4, pointRadius:0, borderWidth:1.4}] },
options: { plugins:{legend:{display:false}}, scales:{x:{display:false}, y:{display:false}} }
});
});
/* -------------------------
AI summary (mock) — replace with real ChatGPT response when integrated
------------------------- */
document.getElementById('ai-insights').innerText = "Rank drop for some keywords appears due to competitor new backlinks and a small SERP volatility spike. Focus on improving content relevance for 'wordpress speed tips' and reclaim lost topical backlinks.";
/* -------------------------
Top competitors listing
------------------------- */
const compList = document.getElementById('top-competitors');
mockData.competitors.forEach(c => {
const li = document.createElement('li'); li.innerText = c; compList.appendChild(li);
});
/* -------------------------
Real API integration notes:
- Replace mockData with fetched JSON from your plugin AJAX endpoint:
fetch('/wp-json/csp/v1/rank-summary').then(r=>r.json()).then(data => { /* use data */ })
- Ensure server returns:
{ meta:{totalKeywords, visibility, avgPosition, change30d}, trend:{days,visibility,avgPos}, distribution, winners, losers, features, competitors, table }
- I can provide the WP REST endpoint code next.
------------------------- */
</script>


0 Comments