Explore the Realm of Digital Marketing and Tech News

Rank Tracker Sample

Nov 15, 2025 | Uncategorized | 0 comments

Written By

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>

Written By

undefined

Explore More Insights

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *