Three launch blockers from the Oct 17 direct-Stripe billing work: - verifyStripeCheckoutSession defaulted an unconfirmed checkout session to "active"/isPaying:true, and had no billing permission check, so an abandoned (unpaid) checkout marked the org as a paying subscriber for free. Now defaults to "incomplete" and only trusts a real subscription status or a confirmed paid payment_status; also pins org id to the caller's own organization instead of trusting session metadata. - The Stripe webhook fell back to parsing an unsigned request body as a real event whenever STRIPE_WEBHOOK_SECRET was unset, letting anyone POST a fake checkout.session.completed and grant any org a paid plan. STRIPE_WEBHOOK_SECRET is now required; unsigned requests are refused. Also maps Stripe's subscription.status properly (trialing counted as active) instead of collapsing anything but "active" to "canceled". - meterDataforseoCall and every credit-check in subscription.ts skipped all usage metering/capping whenever AUTUMN_SECRET_KEY was unset — which is exactly the mode the Oct 17 work introduced for self-host direct Stripe billing. Added a local per-org monthly usage ledger (usage_ledger table, sqlite + pg) checked against each plan's monthly data budget (src/shared/crawlerxPlanBudgets.ts, from the PRD's unit economics table) so DataForSEO spend is capped even without Autumn. Also: documented STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET/AUTUMN_SECRET_KEY in .env.example, and published the CrawlerX product demo as a static page at /demo.html (self-contained, no backend dependency). Verified: tsc --noEmit clean, schema-parity test green, full vitest suite green except two pre-existing unrelated failures (confirmed via stash), knip/oxlint show no new findings over the pre-change baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
521 lines
43 KiB
HTML
521 lines
43 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>CrawlerX Product Demo</title>
|
||
<meta name="claude:viewport-fit-sides" content="cover">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||
<style>
|
||
/* Layout: app shell — guided tour strip on top, left nav of 8 screens, working canvas on the right */
|
||
:root{
|
||
--bg:#F3F5F7; --surface:#FFFFFF; --sunk:#E9EDF1; --line:#D5DCE3;
|
||
--ink:#14202B; --muted:#5A6B7A;
|
||
--accent:#0E6E6A; --accent-soft:#D7EDEB; --accent-ink:#FFFFFF;
|
||
--mark:#C8811F; --mark-soft:#F8E8CF;
|
||
--good:#2E7D4F; --good-soft:#DCEFE3; --warn:#B7791F; --warn-soft:#F6EAD3; --bad:#B4413A; --bad-soft:#F5DEDB;
|
||
--f-display:"Bricolage Grotesque", "Segoe UI", system-ui, sans-serif;
|
||
--f-body:"IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
||
--f-mono:"IBM Plex Mono", ui-monospace, Consolas, monospace;
|
||
}
|
||
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
|
||
--bg:#0F161C; --surface:#162029; --sunk:#1D2933; --line:#2C3A46;
|
||
--ink:#E6EDF2; --muted:#94A6B5;
|
||
--accent:#3FB6AE; --accent-soft:#16383A; --accent-ink:#06201F;
|
||
--mark:#E3A44A; --mark-soft:#3A2C17;
|
||
--good:#5CC48A; --good-soft:#15301F; --warn:#E0A84B; --warn-soft:#352915; --bad:#E4776F; --bad-soft:#3A1E1C; color-scheme:dark}}
|
||
:root[data-theme="dark"]{
|
||
--bg:#0F161C; --surface:#162029; --sunk:#1D2933; --line:#2C3A46;
|
||
--ink:#E6EDF2; --muted:#94A6B5;
|
||
--accent:#3FB6AE; --accent-soft:#16383A; --accent-ink:#06201F;
|
||
--mark:#E3A44A; --mark-soft:#3A2C17;
|
||
--good:#5CC48A; --good-soft:#15301F; --warn:#E0A84B; --warn-soft:#352915; --bad:#E4776F; --bad-soft:#3A1E1C; color-scheme:dark}
|
||
*{box-sizing:border-box}
|
||
body{background:var(--bg);color:var(--ink);font-family:var(--f-body);font-size:14px;line-height:1.5;margin:0;
|
||
padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:16px 40px}
|
||
h1,h2,h3{font-family:var(--f-display);text-wrap:balance;margin:0;letter-spacing:-.01em}
|
||
h1{font-size:26px}h2{font-size:20px}h3{font-size:15px;font-family:var(--f-body);font-weight:600}
|
||
p{margin:0}
|
||
.mono,.num{font-family:var(--f-mono);font-variant-numeric:tabular-nums}
|
||
.muted{color:var(--muted)}
|
||
.wrap{max-width:1180px;margin:0 auto;display:grid;gap:14px}
|
||
button{font:inherit;cursor:pointer}
|
||
button:focus-visible,[tabindex]:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||
.label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600}
|
||
|
||
/* Header */
|
||
.top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}
|
||
.brand{display:flex;align-items:center;gap:10px}
|
||
.logo{width:34px;height:34px;border-radius:9px;background:var(--accent);display:grid;place-items:center}
|
||
.logo svg{width:20px;height:20px}
|
||
.brand small{display:block;color:var(--muted);font-size:12px}
|
||
.sample{font-size:12px;background:var(--mark-soft);color:var(--ink);border:1px solid var(--mark);padding:3px 10px;border-radius:999px}
|
||
|
||
/* Tour strip */
|
||
.tour{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px 16px;display:grid;gap:10px}
|
||
.tour-head{display:flex;flex-wrap:wrap;gap:10px;align-items:baseline;justify-content:space-between}
|
||
.tour-text{max-width:78ch}
|
||
.tour-text b{font-weight:600}
|
||
.dots{display:flex;gap:6px;flex-wrap:wrap}
|
||
.dot{width:28px;height:6px;border-radius:3px;background:var(--line);border:0;padding:0}
|
||
.dot.on{background:var(--accent)}
|
||
.dot.done{background:var(--accent-soft)}
|
||
.tour-nav{display:flex;gap:8px}
|
||
.btn{border:1px solid var(--line);background:var(--surface);color:var(--ink);padding:7px 14px;border-radius:8px;font-weight:500}
|
||
.btn:hover{background:var(--sunk)}
|
||
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
|
||
.btn.primary:hover{filter:brightness(1.08)}
|
||
.btn.mark{background:var(--mark);border-color:var(--mark);color:#1A1205}
|
||
.btn:disabled{opacity:.45;cursor:default}
|
||
|
||
/* App shell */
|
||
.app{display:grid;grid-template-columns:210px minmax(0,1fr);background:var(--surface);border:1px solid var(--line);border-radius:12px;overflow:hidden;min-height:620px}
|
||
.nav{background:var(--sunk);border-right:1px solid var(--line);padding:12px 8px;display:flex;flex-direction:column;gap:2px}
|
||
.nav .ws{padding:6px 10px 12px;font-size:12px;color:var(--muted)}
|
||
.nav .ws b{display:block;color:var(--ink);font-size:13px}
|
||
.nav button{display:flex;align-items:center;gap:10px;text-align:left;border:0;background:transparent;color:var(--ink);padding:8px 10px;border-radius:7px;font-size:13.5px}
|
||
.nav button .n{font-family:var(--f-mono);font-size:11px;color:var(--muted);width:16px}
|
||
.nav button:hover{background:var(--surface)}
|
||
.nav button.on{background:var(--surface);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
|
||
.nav .foot{margin-top:auto;padding:10px;font-size:12px;color:var(--muted)}
|
||
.meter{height:6px;background:var(--line);border-radius:3px;overflow:hidden;margin-top:4px}
|
||
.meter i{display:block;height:100%;background:var(--accent)}
|
||
.canvas{padding:20px;display:grid;gap:16px;align-content:start;min-width:0}
|
||
.screen-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:10px}
|
||
.screen-head p{color:var(--muted);margin-top:4px}
|
||
|
||
.grid{display:grid;gap:12px}
|
||
.g2{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||
.g3{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||
.g4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||
.panel{border:1px solid var(--line);border-radius:10px;padding:14px;min-width:0;display:grid;gap:10px;align-content:start}
|
||
.stat .v{font-family:var(--f-display);font-size:28px;font-weight:700;font-variant-numeric:tabular-nums}
|
||
.stat .d{font-size:12px}
|
||
.up{color:var(--good)}.down{color:var(--bad)}
|
||
.pill{display:inline-block;font-size:11.5px;padding:2px 8px;border-radius:999px;font-weight:500;white-space:nowrap}
|
||
.p-good{background:var(--good-soft);color:var(--good)}
|
||
.p-warn{background:var(--warn-soft);color:var(--warn)}
|
||
.p-bad{background:var(--bad-soft);color:var(--bad)}
|
||
.p-acc{background:var(--accent-soft);color:var(--accent)}
|
||
.p-mark{background:var(--mark-soft);color:var(--mark)}
|
||
|
||
.tbl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px}
|
||
table{border-collapse:collapse;width:100%;font-size:13px}
|
||
th,td{padding:9px 12px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
|
||
th{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;background:var(--sunk)}
|
||
tr:last-child td{border-bottom:0}
|
||
td.r,th.r{text-align:right}
|
||
tbody tr.click{cursor:pointer}
|
||
tbody tr.click:hover{background:var(--sunk)}
|
||
.health{display:flex;align-items:center;gap:8px}
|
||
.hbar{width:70px;height:6px;border-radius:3px;background:var(--line);overflow:hidden}
|
||
.hbar i{display:block;height:100%}
|
||
|
||
.actions{display:grid;gap:8px;counter-reset:a}
|
||
.act{display:grid;grid-template-columns:28px minmax(0,1fr) auto;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);border-radius:8px}
|
||
.act .k{font-family:var(--f-mono);font-size:12px;width:24px;height:24px;border-radius:6px;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-weight:500}
|
||
.act small{display:block;color:var(--muted)}
|
||
|
||
/* Onboarding */
|
||
.steps-on{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
|
||
.field{display:grid;gap:4px}
|
||
.field label{font-size:12px;color:var(--muted);font-weight:500}
|
||
.field input,.field select,textarea{font:inherit;border:1px solid var(--line);background:var(--surface);color:var(--ink);border-radius:8px;padding:8px 10px;width:100%}
|
||
textarea{font-family:var(--f-mono);font-size:12.5px;min-height:150px;resize:vertical}
|
||
.check{display:flex;gap:8px;align-items:center;font-size:13px}
|
||
.crawl-log{font-family:var(--f-mono);font-size:12px;background:var(--sunk);border-radius:8px;padding:10px;min-height:150px;display:grid;gap:2px;align-content:start}
|
||
|
||
/* Audit + fix */
|
||
.split{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:12px}
|
||
.issue{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);border-radius:8px;cursor:pointer;background:var(--surface);text-align:left;color:var(--ink);width:100%}
|
||
.issue:hover{background:var(--sunk)}
|
||
.issue.on{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
|
||
.issue.fixed{opacity:.6}
|
||
.sev{width:8px;height:32px;border-radius:4px}
|
||
.code{font-family:var(--f-mono);font-size:12px;background:var(--sunk);border-radius:8px;padding:10px;white-space:pre-wrap;word-break:break-word;overflow-x:auto}
|
||
.code.new{background:var(--accent-soft)}
|
||
.code del{color:var(--bad);text-decoration:line-through}
|
||
.prog{display:grid;gap:6px}
|
||
.pstep{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--muted)}
|
||
.pstep .b{width:16px;height:16px;border-radius:50%;border:2px solid var(--line);flex:none}
|
||
.pstep.go{color:var(--ink)}.pstep.go .b{border-color:var(--mark);border-top-color:transparent;animation:spin .8s linear infinite}
|
||
.pstep.ok{color:var(--ink)}.pstep.ok .b{border-color:var(--good);background:var(--good)}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.score{display:flex;align-items:center;gap:14px}
|
||
.ring{width:84px;height:84px}
|
||
|
||
/* Keywords */
|
||
.tabs{display:flex;gap:4px;flex-wrap:wrap}
|
||
.tab{border:1px solid var(--line);background:var(--surface);color:var(--ink);padding:6px 12px;border-radius:999px;font-size:13px}
|
||
.tab.on{background:var(--ink);color:var(--surface);border-color:var(--ink)}
|
||
.kd{display:inline-flex;align-items:center;gap:6px}
|
||
.kd i{width:9px;height:9px;border-radius:50%}
|
||
|
||
/* Geo grid */
|
||
.geo{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;max-width:330px;aspect-ratio:1;width:100%;position:relative}
|
||
.geo span{display:grid;place-items:center;border-radius:50%;font-family:var(--f-mono);font-size:12px;font-weight:500}
|
||
.legend{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--muted)}
|
||
.legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px;vertical-align:-1px}
|
||
.spark{width:100%;height:120px}
|
||
|
||
/* AI vis */
|
||
.yes{color:var(--good);font-weight:600}.no{color:var(--muted)}
|
||
.share{display:grid;gap:8px}
|
||
.srow{display:grid;grid-template-columns:150px minmax(0,1fr) 44px;gap:10px;align-items:center;font-size:13px}
|
||
.sbar{height:10px;background:var(--sunk);border-radius:5px;overflow:hidden}
|
||
.sbar i{display:block;height:100%;background:var(--muted)}
|
||
.sbar i.me{background:var(--accent)}
|
||
|
||
/* Report */
|
||
.report{border:1px solid var(--line);border-radius:10px;overflow:hidden}
|
||
.rhead{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 16px;background:var(--sunk);flex-wrap:wrap}
|
||
.agency-logo{font-family:var(--f-display);font-weight:700;font-size:17px}
|
||
.rbody{padding:16px;display:grid;gap:14px}
|
||
.phone{border:1px solid var(--line);border-radius:22px;padding:12px;background:var(--sunk);max-width:300px;display:grid;gap:8px}
|
||
.bubble{background:var(--surface);border-radius:10px;padding:10px;font-size:13px;border:1px solid var(--line)}
|
||
.bubble .t{font-size:11px;color:var(--muted);text-align:right}
|
||
|
||
/* Pricing */
|
||
.plans{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
|
||
.plan{border:1px solid var(--line);border-radius:10px;padding:14px;display:grid;gap:8px;align-content:start}
|
||
.plan.feat{border:2px solid var(--accent)}
|
||
.plan .price{font-family:var(--f-display);font-size:26px;font-weight:700}
|
||
.plan ul{margin:0;padding-left:18px;display:grid;gap:3px;font-size:13px}
|
||
.toggle{display:inline-flex;border:1px solid var(--line);border-radius:999px;overflow:hidden}
|
||
.toggle button{border:0;background:transparent;color:var(--ink);padding:6px 14px;font-size:13px}
|
||
.toggle button.on{background:var(--accent);color:var(--accent-ink)}
|
||
.ck{color:var(--good);font-weight:600}.x{color:var(--bad)}
|
||
|
||
.toast{position:fixed;left:50%;transform:translateX(-50%);bottom:calc(20px + env(safe-area-inset-bottom,0px));background:var(--ink);color:var(--surface);padding:10px 16px;border-radius:8px;font-size:13px;z-index:20;max-width:calc(100% - 32px)}
|
||
|
||
@media (max-width:900px){
|
||
.app{grid-template-columns:1fr}
|
||
.nav{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--line)}
|
||
.nav .ws,.nav .foot{display:none}
|
||
.nav button{white-space:nowrap}
|
||
.g4,.plans{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||
.split,.g2,.g3,.steps-on{grid-template-columns:1fr}
|
||
}
|
||
@media (max-width:480px){.g4,.plans{grid-template-columns:1fr}.canvas{padding:14px}.srow{grid-template-columns:110px minmax(0,1fr) 40px}}
|
||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header class="top">
|
||
<div class="brand">
|
||
<div class="logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5 21 21M7.5 10.5h6M10.5 7.5v6"/></svg></div>
|
||
<div><h1>CrawlerX</h1><small>Find it. Fix it. Prove it. SEO for 10 sites at ₹2,499/month.</small></div>
|
||
</div>
|
||
<span class="sample">Interactive demo · all sites and numbers are sample data</span>
|
||
</header>
|
||
|
||
<section class="tour" aria-live="polite">
|
||
<div class="tour-head">
|
||
<span class="label" id="tourStep">Step 1 of 8</span>
|
||
<div class="dots" id="dots"></div>
|
||
</div>
|
||
<p class="tour-text" id="tourText"></p>
|
||
<div class="tour-nav">
|
||
<button class="btn" id="prev">Back</button>
|
||
<button class="btn primary" id="next">Next step</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="app">
|
||
<nav class="nav" id="nav" aria-label="Product screens">
|
||
<div class="ws"><b>Pixelkraft Digital</b>Agency workspace · Pro plan</div>
|
||
</nav>
|
||
<main class="canvas" id="canvas"></main>
|
||
</div>
|
||
</div>
|
||
<div class="toast" id="toast" hidden></div>
|
||
|
||
<script>
|
||
const $ = s => document.querySelector(s);
|
||
const SCREENS = [
|
||
{id:'onboard', name:'Get started',
|
||
tour:'<b>Onboarding, under 5 minutes.</b> An agency signs up with Google or a phone OTP, pastes its client sites (or imports them from Search Console), and CrawlerX starts crawling all 10 at once. Press <b>Start crawl</b> to watch it.'},
|
||
{id:'dash', name:'Portfolio',
|
||
tour:'<b>One screen for every client site.</b> Health score, traffic from Google Search Console, tracked ranks and AI mentions per site, plus this week\'s <b>Next 5 actions</b> ranked by impact. Click a site or an action to open its audit.'},
|
||
{id:'audit', name:'Audit & fix',
|
||
tour:'<b>The core difference: we fix, not just find.</b> Pick an issue, read the AI-written fix, then <b>Approve and push</b>. CrawlerX writes it to WordPress or Shopify, re-crawls the page and confirms the fix is live. Watch the health score move.'},
|
||
{id:'kw', name:'Keywords',
|
||
tour:'<b>Keyword research built for India.</b> Volumes by city, intent and difficulty, in English and regional languages. Switch the language tabs, then add keywords to rank tracking in one click.'},
|
||
{id:'rank', name:'Ranks & local',
|
||
tour:'<b>Rank tracking plus a local map.</b> Weekly Google positions with a trend line, and a 7×7 geo-grid showing where the clinic ranks block by block across the city, the view Local Falcon and Synup sell separately.'},
|
||
{id:'ai', name:'AI visibility',
|
||
tour:'<b>Does ChatGPT recommend you?</b> CrawlerX asks the AI engines the questions customers ask, logs who gets mentioned and cited, and shows your share against competitors. Semrush sells this as a separate $199+ plan.'},
|
||
{id:'report', name:'Client report',
|
||
tour:'<b>White-label reporting.</b> The agency\'s own logo, a plain-language summary of what was fixed and what moved, sent as a PDF and a WhatsApp message every Monday.'},
|
||
{id:'price', name:'Plans',
|
||
tour:'<b>Priced for Indian agencies.</b> 10 sites for ₹2,499 a month with UPI Autopay and GST invoices, against $139.95 for one Semrush Pro seat. Toggle the currency and compare.'}
|
||
];
|
||
let cur = 0;
|
||
const nav = $('#nav');
|
||
SCREENS.forEach((s,i)=>{
|
||
const b=document.createElement('button');b.id='nav-'+s.id;b.innerHTML=`<span class="n">${i+1}</span>${s.name}`;
|
||
b.onclick=()=>go(i);nav.appendChild(b);
|
||
});
|
||
const foot=document.createElement('div');foot.className='foot';
|
||
foot.innerHTML='Usage this month<div class="meter"><i style="width:38%"></i></div><span class="num">171 / 450</span> keyword lookups';
|
||
nav.appendChild(foot);
|
||
const dots=$('#dots');
|
||
SCREENS.forEach((s,i)=>{const d=document.createElement('button');d.className='dot';d.setAttribute('aria-label','Go to step '+(i+1)+': '+s.name);d.onclick=()=>go(i);dots.appendChild(d)});
|
||
$('#prev').onclick=()=>go(cur-1);$('#next').onclick=()=>go(cur+1);
|
||
|
||
function toast(t){const el=$('#toast');el.textContent=t;el.hidden=false;clearTimeout(toast.t);toast.t=setTimeout(()=>el.hidden=true,2600)}
|
||
|
||
/* ---------- sample data ---------- */
|
||
const SITES=[
|
||
{d:'smileline-dental.in',c:'Pune',h:64,t:'+12%',k:'38/120',ai:'2 of 12',st:'warn'},
|
||
{d:'annapoorna-sweets.com',c:'Coimbatore',h:81,t:'+4%',k:'51/90',ai:'5 of 12',st:'good'},
|
||
{d:'zenyoga-studio.in',c:'Bengaluru',h:72,t:'+9%',k:'22/60',ai:'1 of 10',st:'warn'},
|
||
{d:'kraftleather.co',c:'Kanpur',h:49,t:'−6%',k:'14/80',ai:'0 of 10',st:'bad'},
|
||
{d:'brightpath-tuitions.in',c:'Jaipur',h:77,t:'+15%',k:'30/45',ai:'3 of 8',st:'good'},
|
||
{d:'greenleaf-organics.in',c:'Hyderabad',h:68,t:'+2%',k:'19/70',ai:'2 of 10',st:'warn'},
|
||
{d:'metroautoparts.in',c:'Chennai',h:58,t:'−3%',k:'27/110',ai:'1 of 12',st:'bad'},
|
||
{d:'lotus-ayurveda.in',c:'Kochi',h:85,t:'+7%',k:'44/60',ai:'6 of 10',st:'good'},
|
||
{d:'urbannest-realty.in',c:'Gurugram',h:71,t:'+1%',k:'16/50',ai:'1 of 8',st:'warn'},
|
||
{d:'chaicraft-cafe.in',c:'Indore',h:79,t:'+11%',k:'12/25',ai:'2 of 6',st:'good'}
|
||
];
|
||
const ISSUES=[
|
||
{id:'i1',sev:'bad',title:'Missing LocalBusiness schema',pages:'Homepage',impact:'High',why:'Google and AI engines can\'t read your address, hours and phone as structured facts. Clinics with schema are cited more often in AI answers.',
|
||
before:'\x3C!-- no structured data on page -->',
|
||
after:'\x3Cscript type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "Dentist",\n "name": "SmileLine Dental Clinic",\n "address": {\n "@type": "PostalAddress",\n "streetAddress": "12 FC Road, Shivajinagar",\n "addressLocality": "Pune", "postalCode": "411005",\n "addressCountry": "IN"\n },\n "telephone": "+91-20-0000-0000",\n "openingHours": "Mo-Sa 10:00-20:00"\n}\n\x3C/script>',gain:6},
|
||
{id:'i2',sev:'bad',title:'Title tags too long on 14 pages',pages:'14 service pages',impact:'High',why:'Titles over 60 characters get cut in results. The fix keeps the main keyword and city in the first 50 characters.',
|
||
before:'<title><del>Welcome to SmileLine Dental Clinic | Best Dentist | Root Canal Treatment | Teeth Whitening | Pune Maharashtra</del></title>',
|
||
after:'<title>Root Canal Treatment in Pune | SmileLine Dental</title>',gain:5},
|
||
{id:'i3',sev:'warn',title:'38 images without alt text',pages:'Gallery, Services',impact:'Medium',why:'Alt text helps image search and accessibility. The AI describes each image from its content and file context.',
|
||
before:'<img src="/img/IMG_4471.jpg">',
|
||
after:'<img src="/img/IMG_4471.jpg"\n alt="Dentist explaining a dental X-ray to a patient at SmileLine, Pune">',gain:3},
|
||
{id:'i4',sev:'warn',title:'Slow mobile LCP (4.1 s)',pages:'Homepage',impact:'Medium',why:'The hero image is a 2.4 MB JPEG. Serving WebP at the right size brings LCP under 2.5 s.',
|
||
before:'<img src="/hero.jpg" width="2400">',
|
||
after:'<img src="/hero-1200.webp" width="1200" height="640"\n fetchpriority="high" alt="SmileLine clinic reception">',gain:4},
|
||
{id:'i5',sev:'warn',title:'No FAQ block on service pages',pages:'6 service pages',impact:'Medium',why:'Question-and-answer content is what AI engines quote. The draft answers the 4 questions patients search most for each service.',
|
||
before:'\x3C!-- page ends after pricing table -->',
|
||
after:'<section class="faq">\n <h2>Root canal FAQs</h2>\n <h3>Is a root canal painful?</h3>\n <p>With local anaesthesia most patients feel pressure,\n not pain. Treatment takes 60–90 minutes.</p>\n ...\n</section>',gain:3}
|
||
];
|
||
let health=64; const fixed=new Set(); let sel='i1';
|
||
const KW={
|
||
en:[['dentist near me','Pune','40,500','Local',38,'good'],['root canal treatment cost pune','Pune','2,900','Commercial',22,'good'],['teeth whitening pune','Pune','1,600','Commercial',31,'good'],['best dental clinic in pune','Pune','3,600','Local',57,'warn'],['dental implants price','India','14,800','Commercial',64,'bad'],['braces for adults','India','6,600','Info',45,'warn']],
|
||
hi:[['दांत का डॉक्टर','India','9,900','Local',18,'good'],['दांत दर्द का इलाज','India','22,200','Info',26,'good'],['रूट कैनाल क्या है','India','4,400','Info',12,'good'],['दांतों की सफाई खर्च','India','1,300','Commercial',15,'good']],
|
||
ta:[['பல் மருத்துவர்','Tamil Nadu','6,600','Local',14,'good'],['பல் வலி தீர்வு','Tamil Nadu','3,600','Info',11,'good'],['பல் கிளினிக் அருகில்','Chennai','1,900','Local',20,'good']]
|
||
};
|
||
let kwLang='en';
|
||
|
||
/* ---------- screens ---------- */
|
||
const R={};
|
||
R.onboard=()=>`
|
||
<div class="screen-head"><div><h2>Add your client sites</h2><p>Paste up to 10 domains, one per line, or import them from Google Search Console.</p></div></div>
|
||
<div class="steps-on">
|
||
<div class="panel"><span class="label">1 · Account</span>
|
||
<div class="field"><label for="ob-name">Agency name</label><input id="ob-name" value="Pixelkraft Digital"></div>
|
||
<div class="field"><label for="ob-phone">Mobile (OTP login)</label><input id="ob-phone" value="+91 98xxx xxx21"></div>
|
||
<div class="field"><label for="ob-lang">Interface language</label><select id="ob-lang"><option>English</option><option>हिन्दी</option><option>தமிழ்</option></select></div>
|
||
</div>
|
||
<div class="panel"><span class="label">2 · Sites</span>
|
||
<textarea id="ob-sites" aria-label="Site list">${SITES.map(s=>s.d).join('\n')}</textarea>
|
||
<label class="check"><input type="checkbox" id="ob-gsc" checked> Connect Google Search Console for real clicks</label>
|
||
<label class="check"><input type="checkbox" id="ob-gbp" checked> Connect Google Business Profiles</label>
|
||
</div>
|
||
<div class="panel"><span class="label">3 · First crawl</span>
|
||
<div class="crawl-log" id="log"><span class="muted">Waiting to start…</span></div>
|
||
<button class="btn primary" id="startCrawl">Start crawl</button>
|
||
</div>
|
||
</div>`;
|
||
R.dash=()=>{
|
||
const avg=Math.round(SITES.reduce((a,s)=>a+(s.d==='smileline-dental.in'?health:s.h),0)/SITES.length);
|
||
return `
|
||
<div class="screen-head"><div><h2>Portfolio · 10 sites</h2><p>Week of 12 Oct 2026 · data from your crawls, Search Console and tracked SERPs</p></div><span class="pill p-acc">Next crawl: Mon 19 Oct, 03:00 IST</span></div>
|
||
<div class="grid g4">
|
||
<div class="panel stat"><span class="label">Avg health score</span><span class="v num">${avg}</span><span class="d up">+5 since last month</span></div>
|
||
<div class="panel stat"><span class="label">Clicks (GSC, 28 days)</span><span class="v num">48,210</span><span class="d up">+8.4% vs previous 28 days</span></div>
|
||
<div class="panel stat"><span class="label">Keywords in top 10</span><span class="v num">273</span><span class="d">of 300 tracked · <span class="up">+19</span></span></div>
|
||
<div class="panel stat"><span class="label">AI answers mentioning you</span><span class="v num">23</span><span class="d">of 96 tracked prompts</span></div>
|
||
</div>
|
||
<div class="grid" style="grid-template-columns:minmax(0,1.5fr) minmax(0,1fr)">
|
||
<div class="tbl-wrap"><table><thead><tr><th>Site</th><th>City</th><th>Health</th><th class="r">Clicks</th><th class="r">Top 10</th><th class="r">AI mentions</th></tr></thead><tbody>
|
||
${SITES.map(s=>{const h=s.d==='smileline-dental.in'?health:s.h;const st=h>=75?'good':h>=60?'warn':'bad';return `<tr class="click" tabindex="0" data-site="${s.d}"><td>${s.d}</td><td class="muted">${s.c}</td><td><span class="health"><span class="hbar"><i style="width:${h}%;background:var(--${st})"></i></span><span class="num">${h}</span></span></td><td class="r num ${s.t.startsWith('+')?'up':'down'}">${s.t}</td><td class="r num">${s.k}</td><td class="r num">${s.ai}</td></tr>`}).join('')}
|
||
</tbody></table></div>
|
||
<div class="panel"><h3>Next 5 actions this week</h3><p class="muted" style="font-size:12.5px">Ranked by expected traffic gain. Each one opens with the fix already written.</p>
|
||
<div class="actions">
|
||
${[['Add LocalBusiness schema','smileline-dental.in · homepage','High','p-bad'],['Shorten 14 title tags','smileline-dental.in','High','p-bad'],['Fix 22 broken internal links','kraftleather.co','High','p-bad'],['Answer “best leather bags kanpur” in an FAQ','kraftleather.co · AI gap','Med','p-warn'],['Compress hero image (LCP 4.1 s)','metroautoparts.in','Med','p-warn']].map((a,i)=>`<div class="act"><span class="k">${i+1}</span><div>${a[0]}<small>${a[1]}</small></div><span class="pill ${a[3]}">${a[2]}</span></div>`).join('')}
|
||
</div>
|
||
<button class="btn primary" id="openAudit">Open fixes for smileline-dental.in</button>
|
||
</div>
|
||
</div>`};
|
||
function ring(v){const c=2*Math.PI*34,st=v>=75?'good':v>=60?'warn':'bad';return `<svg class="ring" viewBox="0 0 84 84" role="img" aria-label="Health score ${v} of 100"><circle cx="42" cy="42" r="34" fill="none" stroke="var(--line)" stroke-width="8"/><circle cx="42" cy="42" r="34" fill="none" stroke="var(--${st})" stroke-width="8" stroke-linecap="round" stroke-dasharray="${c*v/100} ${c}" transform="rotate(-90 42 42)"/><text x="42" y="48" text-anchor="middle" font-family="Bricolage Grotesque, sans-serif" font-size="22" font-weight="700" fill="var(--ink)">${v}</text></svg>`}
|
||
R.audit=()=>{
|
||
const it=ISSUES.find(x=>x.id===sel);const done=fixed.has(it.id);
|
||
return `
|
||
<div class="screen-head"><div><h2>smileline-dental.in · Audit</h2><p>Crawled 412 pages · 128 checks · last crawl 14 Oct 2026, 03:12 IST</p></div>
|
||
<div class="score">${ring(health)}<div><span class="label">Health score</span><p class="muted" style="font-size:12.5px">${fixed.size} of ${ISSUES.length} top issues fixed</p></div></div></div>
|
||
<div class="split">
|
||
<div class="grid" style="align-content:start">
|
||
<span class="label">Top issues by impact</span>
|
||
${ISSUES.map(x=>`<button class="issue ${x.id===sel?'on':''} ${fixed.has(x.id)?'fixed':''}" data-issue="${x.id}"><span class="sev" style="background:var(--${fixed.has(x.id)?'good':x.sev})"></span><span><b style="font-weight:600">${x.title}</b><br><small class="muted">${x.pages}</small></span>${fixed.has(x.id)?'<span class="pill p-good">Fixed</span>':`<span class="pill ${x.sev==='bad'?'p-bad':'p-warn'}">${x.impact}</span>`}</button>`).join('')}
|
||
<p class="muted" style="font-size:12.5px">+ 46 lower-impact issues (redirect chains, missing H1 on 3 pages, mixed content)</p>
|
||
</div>
|
||
<div class="panel">
|
||
<h3>${it.title}</h3>
|
||
<p>${it.why}</p>
|
||
<span class="label">Now on the site</span><div class="code">${it.before.replace(/<(?!\/?del>)/g,'<')}</div>
|
||
<span class="label">AI-generated fix <span class="pill p-acc" style="text-transform:none;letter-spacing:0">editable</span></span><div class="code new" contenteditable="true" spellcheck="false" aria-label="Generated fix, editable">${it.after.replace(/</g,'<')}</div>
|
||
<div class="prog" id="prog" ${done?'':'hidden'}>
|
||
<div class="pstep ${done?'ok':''}" id="ps1"><span class="b"></span>Pushing to WordPress (via CrawlerX plugin)</div>
|
||
<div class="pstep ${done?'ok':''}" id="ps2"><span class="b"></span>Re-crawling affected pages</div>
|
||
<div class="pstep ${done?'ok':''}" id="ps3"><span class="b"></span>Verified live · Google re-index requested via Search Console</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn mark" id="approve" ${done?'disabled':''}>${done?'Fix is live':'Approve and push to WordPress'}</button>
|
||
<button class="btn" id="copyFix">Copy code</button>
|
||
</div>
|
||
</div>
|
||
</div>`};
|
||
R.kw=()=>`
|
||
<div class="screen-head"><div><h2>Keyword research</h2><p>Seed: <b>dentist</b> · location: Pune, Maharashtra · Google India</p></div>
|
||
<div class="tabs" role="tablist">${[['en','English'],['hi','हिन्दी'],['ta','தமிழ்']].map(([k,l])=>`<button class="tab ${kwLang===k?'on':''}" data-lang="${k}" role="tab" aria-selected="${kwLang===k}">${l}</button>`).join('')}</div></div>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Keyword</th><th>Location</th><th class="r">Monthly searches</th><th>Intent</th><th>Difficulty</th><th></th></tr></thead><tbody>
|
||
${KW[kwLang].map((r,i)=>`<tr><td>${r[0]}</td><td class="muted">${r[1]}</td><td class="r num">${r[2]}</td><td><span class="pill p-acc">${r[3]}</span></td><td><span class="kd"><i style="background:var(--${r[5]})"></i><span class="num">${r[4]}</span>/100</span></td><td class="r"><button class="btn" data-track="${i}" style="padding:4px 10px;font-size:12.5px">Track</button></td></tr>`).join('')}
|
||
</tbody></table></div>
|
||
<div class="grid g3">
|
||
<div class="panel"><span class="label">Quick win</span><p><b>Regional-language demand is untapped.</b> Hindi “दांत दर्द का इलाज” gets 22,200 searches with difficulty 26. No Pune clinic has a Hindi page for it.</p></div>
|
||
<div class="panel"><span class="label">Content brief</span><p>One click turns a keyword into a brief: headings, questions to answer, word count and internal links.</p><button class="btn" id="brief">Generate brief</button></div>
|
||
<div class="panel"><span class="label">Data note</span><p class="muted">Volumes are monthly averages from Google Ads data via our data provider, cached and refreshed every 30 days.</p></div>
|
||
</div>`;
|
||
function geo(){const g=[];const center=3;for(let r=0;r<7;r++)for(let c=0;c<7;c++){const d=Math.hypot(r-center,c-center*1.1+.6);let v=Math.max(1,Math.round(1+d*2.3+((r*7+c)%3)));if(v>20)v='20+';g.push(v)}return g}
|
||
R.rank=()=>{const g=geo();const col=v=>v==='20+'||v>10?'bad':v>3?'warn':'good';
|
||
const pts=[18,17,15,15,13,12,11,9,8,7,6,4];const w=520,h=120,x=i=>20+i*(w-40)/(pts.length-1),y=v=>10+(v-1)/19*(h-30);
|
||
return `
|
||
<div class="screen-head"><div><h2>Ranks & local · smileline-dental.in</h2><p>Keyword: <b>dentist near me</b> · tracked weekly · Google mobile</p></div><span class="pill p-good">Position 4 · up 14 in 12 weeks</span></div>
|
||
<div class="grid g2">
|
||
<div class="panel"><h3>Local geo-grid · 7 × 7 points, 0.5 km apart</h3>
|
||
<div class="geo" role="img" aria-label="Rank by map location; ranks 1 to 3 near the clinic, falling to 20+ at the edges">${g.map(v=>`<span style="background:var(--${col(v)}-soft);color:var(--${col(v)})">${v}</span>`).join('')}</div>
|
||
<div class="legend"><span><i style="background:var(--good)"></i>Rank 1–3</span><span><i style="background:var(--warn)"></i>4–10</span><span><i style="background:var(--bad)"></i>11+</span></div>
|
||
<p class="muted" style="font-size:12.5px">Share of local visibility: <b class="num">41%</b>. The clinic owns the map within about 1 km of FC Road and loses to two competitors in Kothrud.</p>
|
||
</div>
|
||
<div class="panel"><h3>Position over 12 weeks</h3>
|
||
<svg class="spark" viewBox="0 0 ${w} ${h}" role="img" aria-label="Position improved from 18 to 4 over 12 weeks" preserveAspectRatio="none">
|
||
${[1,10,20].map(v=>`<line x1="20" x2="${w-20}" y1="${y(v)}" y2="${y(v)}" stroke="var(--line)"/><text x="0" y="${y(v)+4}" font-size="10" fill="var(--muted)" font-family="IBM Plex Mono, monospace">${v}</text>`).join('')}
|
||
<polyline fill="none" stroke="var(--accent)" stroke-width="2.5" points="${pts.map((v,i)=>x(i)+','+y(v)).join(' ')}"/>
|
||
<circle cx="${x(pts.length-1)}" cy="${y(4)}" r="5" fill="var(--accent)"/>
|
||
</svg>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Keyword</th><th class="r">Pos.</th><th class="r">Change</th><th>SERP feature</th></tr></thead><tbody>
|
||
<tr><td>dentist near me</td><td class="r num">4</td><td class="r num up">+14</td><td>Map pack</td></tr>
|
||
<tr><td>root canal treatment cost pune</td><td class="r num">2</td><td class="r num up">+5</td><td>AI Overview cited</td></tr>
|
||
<tr><td>teeth whitening pune</td><td class="r num">7</td><td class="r num up">+3</td><td>People also ask</td></tr>
|
||
<tr><td>dental implants price</td><td class="r num">23</td><td class="r num down">−2</td><td>—</td></tr>
|
||
</tbody></table></div>
|
||
</div>
|
||
</div>`};
|
||
R.ai=()=>{
|
||
const P=[['best dentist in pune for root canal',1,1,0,1],['painless dental clinic shivajinagar',1,0,1,1],['how much does teeth whitening cost in pune',0,1,0,1],['dentist open on sunday pune',0,0,0,0],['affordable braces pune for adults',0,0,1,0]];
|
||
const mk=v=>v?'<span class="yes">Mentioned</span>':'<span class="no">—</span>';
|
||
const S=[['SmileLine Dental (you)',42,1],['Clove Dental',58,0],['Sabka Dentist',35,0],['Dr. Mehta\'s Clinic',21,0]];
|
||
return `
|
||
<div class="screen-head"><div><h2>AI visibility · smileline-dental.in</h2><p>12 tracked prompts · checked weekly in ChatGPT, Gemini, Perplexity and Google AI Overviews</p></div><span class="pill p-acc">Score 42 / 100 · up from 17</span></div>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Prompt customers ask</th><th>ChatGPT</th><th>Gemini</th><th>Perplexity</th><th>AI Overview</th></tr></thead><tbody>
|
||
${P.map(p=>`<tr><td>${p[0]}</td><td>${mk(p[1])}</td><td>${mk(p[2])}</td><td>${mk(p[3])}</td><td>${mk(p[4])}</td></tr>`).join('')}
|
||
</tbody></table></div>
|
||
<div class="grid g2">
|
||
<div class="panel"><h3>Share of AI mentions vs competitors</h3><div class="share">
|
||
${S.map(s=>`<div class="srow"><span>${s[0]}</span><span class="sbar"><i class="${s[2]?'me':''}" style="width:${s[1]}%"></i></span><span class="num r">${s[1]}%</span></div>`).join('')}
|
||
</div><p class="muted" style="font-size:12px">Share of the 48 prompt-engine checks this week that named each clinic.</p></div>
|
||
<div class="panel"><h3>Why the AI skipped you on “dentist open on sunday pune”</h3>
|
||
<div class="actions">
|
||
<div class="act"><span class="k">1</span><div>Sunday hours are only in an image<small>Add opening hours as text and in schema</small></div><span class="pill p-bad">High</span></div>
|
||
<div class="act"><span class="k">2</span><div>No mention on Practo or Justdial listing<small>AI engines cite these directories for clinics</small></div><span class="pill p-warn">Med</span></div>
|
||
<div class="act"><span class="k">3</span><div>robots.txt blocks GPTBot<small>Allow AI crawlers to read the site</small></div><span class="pill p-warn">Med</span></div>
|
||
</div>
|
||
<button class="btn primary" id="aiFix">Send these to the fix queue</button>
|
||
</div>
|
||
</div>`};
|
||
R.report=()=>`
|
||
<div class="screen-head"><div><h2>Client report · October 2026</h2><p>Sent under your agency's brand. Clients never see CrawlerX.</p></div><button class="btn primary" id="send">Schedule for every Monday</button></div>
|
||
<div class="grid" style="grid-template-columns:minmax(0,1.6fr) minmax(0,1fr)">
|
||
<div class="report">
|
||
<div class="rhead"><span class="agency-logo">Pixelkraft Digital</span><span class="muted">Monthly SEO report · SmileLine Dental Clinic, Pune</span></div>
|
||
<div class="rbody">
|
||
<p><b>Summary in plain words.</b> Google clicks grew 12% this month. You now appear 4th for “dentist near me” across most of Shivajinagar, and ChatGPT recommends SmileLine for root canal questions in Pune.</p>
|
||
<div class="grid g3">
|
||
<div class="panel stat"><span class="label">Website visits from Google</span><span class="v num">3,912</span><span class="d up">+12%</span></div>
|
||
<div class="panel stat"><span class="label">Calls from Google Maps</span><span class="v num">184</span><span class="d up">+21%</span></div>
|
||
<div class="panel stat"><span class="label">Health score</span><span class="v num">${health}</span><span class="d up">from 64</span></div>
|
||
</div>
|
||
<div><span class="label">What we fixed</span><ul style="margin:6px 0 0;padding-left:18px;display:grid;gap:3px"><li>Added clinic details as structured data so Google and AI read them correctly</li><li>Rewrote 14 page titles to show the service and “Pune” first</li><li>Described 38 images for image search</li></ul></div>
|
||
<div><span class="label">Next month</span><p>Hindi page for “दांत दर्द का इलाज”, Sunday hours in text, Practo listing update.</p></div>
|
||
</div>
|
||
</div>
|
||
<div class="phone" aria-label="WhatsApp preview">
|
||
<span class="label">WhatsApp preview</span>
|
||
<div class="bubble"><b>Pixelkraft Digital</b><br>Hi Dr. Kulkarni, your October SEO report is ready.<br>• Google visits +12%<br>• Maps calls +21%<br>• Now #4 for “dentist near me”<br>Full PDF attached.<div class="t">Mon 09:00</div></div>
|
||
<div class="bubble">📄 SmileLine_SEO_Oct2026.pdf · 6 pages<div class="t">Mon 09:00</div></div>
|
||
</div>
|
||
</div>`;
|
||
let cur$='inr';
|
||
R.price=()=>{
|
||
const p={inr:['₹0','₹999','₹2,499','₹7,499'],usd:['$0','$15','$29','$79']}[cur$];
|
||
return `
|
||
<div class="screen-head"><div><h2>Plans</h2><p>All modules on every paid plan. Plans differ only by how many sites and how much data. Prices exclude 18% GST.</p></div>
|
||
<div class="toggle" role="group" aria-label="Currency"><button class="${cur$==='inr'?'on':''}" data-cur="inr">₹ INR</button><button class="${cur$==='usd'?'on':''}" data-cur="usd">$ USD</button></div></div>
|
||
<div class="plans">
|
||
<div class="plan"><h3>Free</h3><span class="price">${p[0]}</span><ul><li>1 site, 100-page audit</li><li>10 tracked keywords</li><li>30 keyword lookups/month</li></ul></div>
|
||
<div class="plan"><h3>Starter</h3><span class="price">${p[1]}<small class="muted" style="font-size:13px;font-weight:400">/mo</small></span><ul><li>3 sites, 1,000 pages each</li><li>100 tracked keywords</li><li>150 lookups, 40 AI checks</li><li>AI fixes + push</li></ul></div>
|
||
<div class="plan feat"><span class="pill p-acc" style="justify-self:start">Most agencies pick this</span><h3>Pro</h3><span class="price">${p[2]}<small class="muted" style="font-size:13px;font-weight:400">/mo</small></span><ul><li><b>10 sites</b>, 3,000 pages each</li><li>300 tracked keywords + geo-grid</li><li>450 lookups, 120 AI checks</li><li>White-label PDF + WhatsApp reports</li></ul></div>
|
||
<div class="plan"><h3>Agency</h3><span class="price">${p[3]}<small class="muted" style="font-size:13px;font-weight:400">/mo</small></span><ul><li>30 sites, 5,000 pages each</li><li>1,000 tracked keywords</li><li>Client portal, 5 seats</li><li>API + MCP access</li></ul></div>
|
||
</div>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Compare at the 10-site level</th><th>CrawlerX Pro</th><th>Semrush Pro</th><th>Ubersuggest Individual</th><th>SE Ranking Essential</th></tr></thead><tbody>
|
||
<tr><td>Price per month</td><td><b>${cur$==='inr'?'₹2,499':'$29'}</b></td><td>$139.95</td><td>$29</td><td>$59</td></tr>
|
||
<tr><td>Sites / projects included</td><td>10</td><td>5 projects</td><td>limits are per domain</td><td>5 projects</td></tr>
|
||
<tr><td>Writes and pushes the fix</td><td class="ck">Yes</td><td class="x">No</td><td class="x">No</td><td class="x">No</td></tr>
|
||
<tr><td>AI search visibility</td><td class="ck">Included</td><td>Semrush One, from $199</td><td class="x">No</td><td>Add-on</td></tr>
|
||
<tr><td>Local geo-grid</td><td class="ck">Included</td><td>Local add-on</td><td class="x">No</td><td>Add-on</td></tr>
|
||
<tr><td>White-label reports</td><td class="ck">Included</td><td>Business plan only</td><td class="x">No</td><td>Higher plans</td></tr>
|
||
<tr><td>INR billing, UPI Autopay, GST invoice</td><td class="ck">Yes</td><td>INR billing</td><td class="x">No</td><td class="x">No</td></tr>
|
||
<tr><td>Hindi / Tamil interface and keywords</td><td class="ck">Yes</td><td>Keywords only</td><td>Keywords only</td><td>Keywords only</td></tr>
|
||
</tbody></table></div>
|
||
<p class="muted" style="font-size:12px">Competitor prices are published list prices as of 2026 from public pricing pages and reviews; limits vary by plan and change often.</p>`};
|
||
|
||
/* ---------- wiring ---------- */
|
||
function bind(id){
|
||
if(id==='onboard'){$('#startCrawl').onclick=runCrawl}
|
||
if(id==='dash'){document.querySelectorAll('[data-site]').forEach(r=>{const f=()=>{if(r.dataset.site==='smileline-dental.in')go(2);else toast('In the full product this opens '+r.dataset.site+'. The demo follows smileline-dental.in.')};r.onclick=f;r.onkeydown=e=>{if(e.key==='Enter')f()}});$('#openAudit').onclick=()=>go(2)}
|
||
if(id==='audit'){
|
||
document.querySelectorAll('[data-issue]').forEach(b=>b.onclick=()=>{sel=b.dataset.issue;render()});
|
||
$('#approve').onclick=approve;
|
||
$('#copyFix').onclick=()=>{const t=document.querySelector('.code.new').innerText;const ok=()=>toast('Fix copied');try{navigator.clipboard.writeText(t).then(ok,()=>toast('Select the code and copy it manually'))}catch(e){toast('Select the code and copy it manually')}};
|
||
}
|
||
if(id==='kw'){document.querySelectorAll('[data-lang]').forEach(b=>b.onclick=()=>{kwLang=b.dataset.lang;render()});document.querySelectorAll('[data-track]').forEach(b=>b.onclick=()=>{b.textContent='Tracking';b.disabled=true;toast('Added to rank tracking · 299 of 300 used')});$('#brief').onclick=()=>toast('Brief drafted: 7 headings, 6 questions, ~1,400 words')}
|
||
if(id==='ai'){$('#aiFix').onclick=()=>toast('3 fixes added to the queue for smileline-dental.in')}
|
||
if(id==='report'){$('#send').onclick=()=>toast('Scheduled: PDF + WhatsApp every Monday 09:00 IST')}
|
||
if(id==='price'){document.querySelectorAll('[data-cur]').forEach(b=>b.onclick=()=>{cur$=b.dataset.cur;render()})}
|
||
}
|
||
function runCrawl(){
|
||
const log=$('#log'),btn=$('#startCrawl');btn.disabled=true;log.innerHTML='';
|
||
const lines=['Fetching robots.txt and sitemaps for 10 sites…','Rendering JavaScript pages with headless Chrome…',...SITES.slice(0,6).map(s=>`✓ ${s.d} — ${200+Math.round(s.h*4)} pages`),'… 4 more sites','Pulling Search Console clicks (28 days)…','Done. 3,906 pages, 612 issues, 47 fixes ready.'];
|
||
lines.forEach((l,i)=>setTimeout(()=>{const s=document.createElement('span');s.textContent=l;log.appendChild(s);if(i===lines.length-1){btn.textContent='Open portfolio';btn.disabled=false;btn.onclick=()=>go(1)}},i*380));
|
||
}
|
||
function approve(){
|
||
const id=sel,it=ISSUES.find(x=>x.id===id);$('#approve').disabled=true;$('#prog').hidden=false;
|
||
const st=['#ps1','#ps2','#ps3'];
|
||
st.forEach((s,i)=>{setTimeout(()=>{$(s).className='pstep go'},i*900);setTimeout(()=>{$(s).className='pstep ok';if(i===2){fixed.add(id);health=Math.min(100,health+it.gain);toast(`Fix is live · health ${health-it.gain} → ${health}`);const next=ISSUES.find(x=>!fixed.has(x.id));if(next)sel=next.id;setTimeout(render,700)}},i*900+800)});
|
||
}
|
||
function render(){$('#canvas').innerHTML=R[SCREENS[cur].id]();bind(SCREENS[cur].id)}
|
||
function go(i){
|
||
if(i<0||i>=SCREENS.length)return;cur=i;
|
||
$('#tourStep').textContent=`Step ${i+1} of ${SCREENS.length} · ${SCREENS[i].name}`;
|
||
$('#tourText').innerHTML=SCREENS[i].tour;
|
||
document.querySelectorAll('.dot').forEach((d,k)=>d.className='dot'+(k===i?' on':k<i?' done':''));
|
||
document.querySelectorAll('.nav button').forEach((b,k)=>b.classList.toggle('on',k===i));
|
||
$('#prev').disabled=i===0;$('#next').textContent=i===SCREENS.length-1?'End of tour':'Next step';$('#next').disabled=i===SCREENS.length-1;
|
||
render();
|
||
try{localStorage.setItem('cx-step',i)}catch(e){}
|
||
}
|
||
let start=0;try{start=+localStorage.getItem('cx-step')||0}catch(e){}
|
||
go(Math.min(start,SCREENS.length-1));
|
||
</script>
|
||
</body>
|
||
</html>
|