CA
Cited by AI
by Tay Design Co.
Client
Loading... switch ↓
Light mode
Overview — Loading...
Loading...
Loading dashboard data...
Content
Loading…
Loading content…
`; const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `developer-report-${Date.now()}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function renderReportSparkline(history) { if (!history || history.length < 2) { return `
Run the monitor for at least 2 weeks to see your score trend.
`; } const scores = history.map(h => parseFloat(h.geo_score) || 0); const maxScore = Math.max(...scores, 10); const W = 560, H = 90, PL = 30, PR = 12, PT = 8, PB = 24; const plotW = W - PL - PR, plotH = H - PT - PB; const n = scores.length; const xStep = n > 1 ? plotW / (n - 1) : plotW; const pts = scores.map((s, i) => [PL + (n > 1 ? i * xStep : plotW / 2), PT + plotH - (s / maxScore) * plotH]); const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); const areaPath = `${linePath} L${pts[pts.length-1][0].toFixed(1)},${(PT+plotH).toFixed(1)} L${pts[0][0].toFixed(1)},${(PT+plotH).toFixed(1)} Z`; const yTicks = [0, 5, 10].map(v => { const y = PT + plotH - (v / maxScore) * plotH; return `${v} `; }).join(''); const xLabels = history.map((h, i) => { if (n <= 6 || i % Math.ceil(n / 6) === 0 || i === n - 1) { const d = new Date(h.week_of); const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); return `${label}`; } return ''; }).join(''); const dots = pts.map(([x, y], i) => `${scores[i]}` ).join(''); return ` ${yTicks}${xLabels} ${dots} `; } // ── Onboarding Checklist ────────────────────────────────── async function loadOnboardingChecklist() { // Onboarding checklist is for agency use only if (currentUser?.role === 'client') return; try { const res = await fetch(`${API}/api/reports/${currentClientId}/onboarding`); if (!res.ok) return; const data = await res.json(); renderOnboardingChecklist(data); } catch { /* silent */ } } function renderOnboardingChecklist(data) { const el = document.getElementById('onboarding-checklist'); if (!el) return; const steps = [ { key: 'brand_dna', label: 'Complete Brand DNA' }, { key: 'prompts', label: 'Add tracking prompts' }, { key: 'pillars', label: 'Define content pillars' }, { key: 'monitoring', label: 'Run AI monitor' }, { key: 'content', label: 'Publish first article' }, ]; const done = steps.filter(s => data[s.key]).length; if (done === steps.length) { el.style.display = 'none'; return; } el.innerHTML = `
Getting Started
${done} of ${steps.length} steps complete
${steps.map(s => `
${data[s.key] ? `` : ''}
${s.label}
`).join('')}
`; el.style.display = ''; } // ── Client Tour ─────────────────────────────────────────── const tourStepsAgency = [ { target: '[data-page="overview"]', title: 'Overview', body: 'Your GEO score and AI citation trend at a glance.' }, { target: '[data-page="monitor"]', title: 'AI Monitor', body: 'See which prompts mention your brand across ChatGPT, Gemini, Claude, and more.' }, { target: '[data-page="articles"]', title: 'Content', body: 'Published content optimized for AI citation — crafted by your agency.' }, { target: '[data-page="reports"]', title: 'Reports', body: 'Track your progress over time with weekly trend data.' }, ]; const tourStepsClient = [ { target: '[data-page="overview"]', title: 'Overview', body: 'Your GEO score and how AI tools like ChatGPT and Claude reference your business.' }, { target: '[data-page="monitor"]', title: 'AI Monitor', body: 'See which AI prompts mention your brand and track visibility over time.' }, { target: '[data-page="articles"]', title: 'Content', body: 'Content created for you — download approved pieces to use on your channels.' }, { target: '[data-page="implementation"]', title: 'Action Plan', body: 'Your personalised checklist of steps to improve your AI visibility score.' }, ]; let tourSteps = tourStepsAgency; let tourIdx = 0; function startTour() { const key = `tour_done_${currentUser?.id}`; if (localStorage.getItem(key)) return; tourSteps = currentUser?.role === 'client' ? tourStepsClient : tourStepsAgency; tourIdx = 0; showTourStep(); } function showTourStep() { const existing = document.getElementById('tour-tooltip'); if (existing) existing.remove(); if (tourIdx >= tourSteps.length) { const key = `tour_done_${currentUser?.id}`; localStorage.setItem(key, '1'); fetch('/api/auth/clear-first-login', { method: 'POST' }).catch(() => {}); return; } const step = tourSteps[tourIdx]; const target = document.querySelector(step.target); const isLast = tourIdx === tourSteps.length - 1; const tip = document.createElement('div'); tip.id = 'tour-tooltip'; tip.className = 'tour-tip'; tip.innerHTML = `
${step.title}${tourIdx+1} / ${tourSteps.length}
${step.body}
`; document.body.appendChild(tip); if (target) { target.classList.add('tour-highlight'); const rect = target.getBoundingClientRect(); tip.style.top = (rect.top + rect.height / 2 - 40) + 'px'; tip.style.left = (rect.right + 12) + 'px'; } else { tip.style.top = '50%'; tip.style.left = '50%'; tip.style.transform = 'translate(-50%,-50%)'; } } function nextTourStep() { const step = tourSteps[tourIdx]; const target = document.querySelector(step.target); if (target) target.classList.remove('tour-highlight'); tourIdx++; showTourStep(); } function skipTour() { const step = tourSteps[tourIdx]; const target = document.querySelector(step.target); if (target) target.classList.remove('tour-highlight'); const existing = document.getElementById('tour-tooltip'); if (existing) existing.remove(); const key = `tour_done_${currentUser?.id}`; localStorage.setItem(key, '1'); fetch('/api/auth/clear-first-login', { method: 'POST' }).catch(() => {}); } // ── Auth ───────────────────────────────────────────────── async function initAuth() { try { const res = await fetch('/api/auth/me'); if (!res.ok) { window.location.href = '/login'; return false; } currentUser = await res.json(); return true; } catch { window.location.href = '/login'; return false; } } async function logout() { await fetch('/api/auth/logout', { method: 'POST' }); localStorage.removeItem('cba_session'); window.location.href = '/login'; } // ── Client: My Report ──────────────────────────────────── const GAP_EXPLANATIONS = { "Business name missing from page title": { why: "AI engines treat the page title as the primary identifier for your business. If your name isn't there, AI can't reliably attribute content to you.", fix: "Add your business name to your homepage <title> tag." }, "Location not found in page title or main heading": { why: "When someone asks an AI 'best [service] near me', AI engines look for location signals in your title and headings to match you to that query.", fix: "Include your city/region in your H1 or page title." }, "No schema.org structured data found": { why: "Schema markup tells AI engines exactly who you are, what you do, where you're located. It's the single highest-impact fix for AI visibility.", fix: "Add LocalBusiness schema to your homepage using Google's Structured Data Markup Helper." }, "Meta description is missing or too short": { why: "AI engines use your meta description to understand what your business offers at a glance.", fix: "Write a 140–160 character meta description naming your business, what you do, and who you serve." }, "Phone number or address not found on homepage": { why: "AI systems verify local businesses using NAP data (Name, Address, Phone). Missing contact details make it harder for AI to recommend you.", fix: "Add your full address and phone number visibly on your homepage." }, "No social proof visible": { why: "Reviews, ratings, and endorsements signal credibility. Sites with social proof are cited more frequently.", fix: "Add a review count or star rating to your homepage." }, "Core industry keywords not prominent on homepage": { why: "AI engines need to understand what you do before they can recommend you.", fix: "Make sure your primary service is named clearly in your H1 or opening paragraph." }, "Site is not on HTTPS": { why: "AI engines strongly favor HTTPS sites as trustworthy sources.", fix: "Contact your web host — most offer free SSL certificates. This is a must-fix." }, }; async function loadMyReport() { document.getElementById('page-title').textContent = 'My Report'; const el = document.getElementById('dashboard-content'); // GEO Report tier — show status page, not a quick scan if (currentUser?.tier === 'geo_report') { el.innerHTML = `
${renderGeoReportStatus()}
`; return; } el.innerHTML = '
Loading your report…
'; try { const res = await fetch(`${API}/api/client/my-report`); const { scan } = await res.json(); if (!scan) { el.innerHTML = `
📋
Your report is being prepared
We're running your Quick Scan now. Check back in a few minutes — you'll also get it by email.
`; return; } if (scan.scan_status === 'processing' || scan.scan_status === 'pending') { el.innerHTML = `
Scan in progress…
We're querying 5 AI engines and auditing your site. This usually takes 1–3 minutes. You'll get an email when it's done.
`; return; } const r = scan.report_data || {}; const score = r.overall_score ?? 0; const citationResults = r.citation_results || []; const topGaps = r.site_audit?.findings?.missing || []; const present = r.site_audit?.findings?.present || []; const prompts = r.prompts_tested || []; const kg = r.knowledge_graph || {}; const industry = scan.industry || 'your industry'; const location = scan.location || ''; const locStr = location ? ` in ${location}` : ''; const scoreColor = score < 4 ? 'var(--slate)' : score < 7 ? 'var(--blue)' : '#4ea368'; const scoreLabel = score < 4 ? 'Needs Work' : score < 7 ? 'Making Progress' : 'Strong Visibility'; // Engine tally const ENGINE_NAMES = ['ChatGPT', 'Claude', 'Gemini', 'Perplexity', 'Grok']; const tally = {}; ENGINE_NAMES.forEach(e => { tally[e] = { cited: 0, checked: 0 }; }); citationResults.forEach(r2 => { (r2.analysis?.client_citation_details || []).forEach(d => { if (tally[d.engine]) { tally[d.engine].checked++; if (d.cited) tally[d.engine].cited++; } }); }); const citationRate = citationResults.length ? Math.round((citationResults.filter(r2 => (r2.analysis?.engines_citing_client || 0) > 0).length / citationResults.length) * 100) : 0; const engineRows = ENGINE_NAMES.map(name => { const t = tally[name]; const dot = t.checked === 0 ? '#888' : t.cited > 0 ? '#4ea368' : '#666'; const status = t.checked === 0 ? `Not configured` : t.cited > 0 ? `Cited ${t.cited}/${t.checked}` : `Not cited (${t.checked} tested)`; return `
${name}
${status}
`; }).join(''); const gapItems = topGaps.slice(0, 6).map((gap, i) => { const key = Object.keys(GAP_EXPLANATIONS).find(k => gap.includes(k.substring(0, 20))); const exp = GAP_EXPLANATIONS[key] || null; return `
${i + 1}. ${escHtml(gap.split(' — ')[0])}
${escHtml(gap.split(' — ')[1] || gap)}
${exp ? `
Why it matters: ${exp.why}
Quick fix: ${exp.fix}
` : ''}
`; }).join(''); const presentItems = present.map(p => `
✓ ${escHtml(p)}
` ).join(''); const geoRecs = [ { title: `"What is ${industry}${locStr}?" — The Answer Page`, desc: `A clear definitional page explaining what ${industry} is becomes a go-to citation source when people ask AI for an explanation.` }, { title: 'FAQ Page Built for AI Questions', desc: `Create 8–12 questions your clients actually ask, written the way someone would ask a voice assistant.` }, { title: `"${industry} Near Me${locStr}" — Local Signal Page`, desc: `A page that clearly states your location, hours, and what you offer locally gives AI the signals it needs to cite you for local queries.` }, { title: 'About Your Approach — The Credibility Page', desc: `A page about your background, credentials, and specific method gives AI the context to recommend you with confidence.` }, ]; const recItems = geoRecs.map((rec, i) => `
${i + 1}. ${rec.title}
${rec.desc}
` ).join(''); const kgHtml = kg.status === 'checked' ? kg.present ? `✓ ${escHtml(scan.business_name)} appears in Google's Knowledge Graph — a strong AI trust signal.` : `✗ Not found in Google's Knowledge Graph. Claim and complete your Google Business Profile to start building this.` : `Knowledge Graph check not available.`; el.innerHTML = `
${score} / 10
${scoreLabel}
${citationRate}%
Citation Rate
${citationResults.length}
Prompts Tested
5
Engines
AI Engine Breakdown
We asked each engine ${citationResults.length} prompts your potential clients might type.
${engineRows}
${present.length ? `
What's Already Working
${presentItems}
` : ''}
Google Knowledge Graph
${kgHtml}
${topGaps.length ? `
Gaps Costing You Citations
Each gap below is a signal AI engines look for when deciding who to recommend.
${gapItems}
` : ''}
GEO Content Recommendations
These are the 4 highest-impact pages you can add to dramatically improve your AI citation rate.
${recItems}
Ready to close these gaps?
The Full Platform monitors your AI visibility weekly, tracks your progress, and builds the content strategy to get you cited consistently.
✓ Your $97 applies toward any subscription plan.
See Platform Plans →
`; } catch (err) { el.innerHTML = `
Error loading report: ${err.message}
`; } } // ── Client: GEO Report Status ──────────────────────────── function renderGeoReportStatus() { return `
📊
Your GEO Visibility Report is in progress
Tay is personally auditing your AI visibility and building your custom report. You'll receive it within 3–5 business days — along with a video walkthrough explaining every finding and next step.
What's included in your report
${[ 'Full AI visibility audit across ChatGPT, Claude, Gemini, Perplexity, and Grok', 'Detailed gap analysis with prioritized fixes', 'GEO content strategy: 4–6 pages built to get you cited', 'Knowledge Graph & schema recommendations', 'Personal video walkthrough from Tay explaining every finding', ].map((item, i) => `
${i + 1}
${item}
`).join('')}
Want ongoing monitoring?
The Full Platform tracks your AI citations weekly and helps you execute on the content strategy from your report.
✓ Your $497 applies toward any subscription plan.
See Platform Plans →
`; } // ── Client: Team (CMO) ──────────────────────────────────── async function loadTeam() { document.getElementById('page-title').textContent = 'Team'; const el = document.getElementById('dashboard-content'); el.innerHTML = '
Loading team…
'; try { const res = await fetch(`${API}/api/client/teammates`); const { teammates } = await res.json(); const me = currentUser; const memberRows = (teammates || []).map(tm => { const isMe = tm.email === me.email; const initials = (tm.name || tm.email).substring(0, 1).toUpperCase(); return `
${initials}
${escHtml(tm.name || '—')}
${escHtml(tm.email)}
${isMe ? '
You
' : ''} ${!isMe && tm.invite_pending ? '
Invite pending
' : ''}
`; }).join(''); const slotsUsed = teammates?.length || 0; const slotsLeft = 5 - slotsUsed; el.innerHTML = `
Team Members
Your CMO plan includes up to 5 team members. Invite colleagues to give them access to this dashboard.
${memberRows || '
No team members yet — invite someone above.
'}
${slotsUsed} of 5 seats used
`; } catch (err) { el.innerHTML = `
Error: ${err.message}
`; } } async function sendTeamInvite() { const emailEl = document.getElementById('team-invite-email'); const email = emailEl?.value?.trim(); if (!email) return; try { const res = await fetch(`${API}/api/auth/invite-teammate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); const data = await res.json(); if (data.ok) { showToast('Invite sent to ' + email, 'ok'); emailEl.value = ''; loadTeam(); } else { showToast(data.error || 'Failed to send invite', 'err'); } } catch (err) { showToast(err.message, 'err'); } } async function loadOrders() { document.getElementById('page-title').textContent = 'Orders'; const el = document.getElementById('dashboard-content'); el.innerHTML = '
Loading orders…
'; try { const res = await fetch(`${API}/api/clients/orders`); if (!res.ok) throw new Error((await res.json()).error || 'Failed to load orders'); const orders = await res.json(); if (!orders.length) { el.innerHTML = '
Orders
No orders yet.
'; return; } const productLabel = { quick_scan: 'Quick Scan', geo_report: 'GEO Report', geo_package: 'GEO Package' }; const scanLabel = { processing: 'Running', completed: 'Done', failed: 'Failed', pending: 'Queued' }; const scanColor = { processing: '#6092c1', completed: '#4ea368', failed: '#c16060', pending: '#8099b0' }; const rows = orders.map(o => { const prod = productLabel[o.product_type] || o.product_type || '—'; const paid = o.payment_status === 'paid'; const scanSt = o.scan_status || 'pending'; const accountSt = o.account_active ? 'Active' : o.invite_pending ? 'Invite sent' : o.user_id ? 'No password' : 'No account'; const accountColor = o.account_active ? '#4ea368' : o.invite_pending ? '#6092c1' : '#8099b0'; const d = new Date(o.created_at); const date = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); return ` ${escHtml(o.business_name)} ${escHtml(o.email)} ${prod} ${paid ? 'Paid' : 'Unpaid'} ${scanLabel[scanSt] || scanSt} ${accountSt} ${date} `; }).join(''); el.innerHTML = `
Orders (${orders.length})
${rows}
Business Email Product Payment Scan Account Date
`; } catch (err) { el.innerHTML = `
Error: ${escHtml(err.message)}
`; } } // ── Blog Images ─────────────────────────────────────────── async function loadBlogImages() { document.getElementById('page-title').textContent = 'Blog Images'; const el = document.getElementById('dashboard-content'); el.innerHTML = '
Loading blog posts…
'; try { const res = await fetch(`${API}/api/blog-admin/posts`); if (!res.ok) throw new Error('Failed to load blog posts'); const posts = await res.json(); renderBlogImages(posts); } catch (err) { el.innerHTML = `
Error: ${escHtml(err.message)}
`; } } function renderBlogImages(posts) { const el = document.getElementById('dashboard-content'); const withImg = posts.filter(p => p.has_image).length; const cards = posts.map(p => { const sourceLabel = p.source === 'queue' ? 'Scheduled' : 'Live'; const thumb = p.has_image ? `${escHtml(p.image_alt || p.title)}` : `
No image yet
`; const altDisplay = p.image_alt ? `
"${escHtml(p.image_alt)}"
` : `
No alt text — upload an image to generate
`; return `
${thumb}
${sourceLabel} ${p.pillar ? `${escHtml(p.pillar)}` : ''}
${escHtml(p.title)}
${altDisplay} ${p.has_image ? `
` : ` `}
`; }).join(''); el.innerHTML = `
Blog Images
${withImg}/${posts.length} posts have images

Upload an image for each post. GEO-optimized alt text is auto-generated from the post title, topic, and context — then stored directly in the post file. Images appear in the blog and as social sharing previews.

${cards}
`; } async function uploadBlogImage(slug, input) { const file = input.files[0]; if (!file) return; const card = document.getElementById(`bimg-${slug}`); if (card) card.style.opacity = '0.5'; try { const fd = new FormData(); fd.append('image', file); const res = await fetch(`${API}/api/blog-admin/image/${encodeURIComponent(slug)}`, { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Upload failed'); showToast('Image uploaded — alt text generated', 'ok'); loadBlogImages(); } catch (err) { showToast('Upload failed: ' + err.message, 'err'); if (card) card.style.opacity = ''; } } async function editBlogAlt(slug, btn) { const current = btn.dataset.currentAlt || ''; const newAlt = prompt('Edit GEO alt text:', current); if (!newAlt || newAlt.trim() === current) return; try { const res = await fetch(`${API}/api/blog-admin/alt/${encodeURIComponent(slug)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alt_text: newAlt.trim() }) }); if (!res.ok) throw new Error((await res.json()).error || 'Save failed'); showToast('Alt text saved', 'ok'); loadBlogImages(); } catch (err) { showToast('Failed: ' + err.message, 'err'); } } async function manageBilling() { const btn = document.getElementById('manage-billing-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Loading…'; } try { const res = await fetch(`${API}/api/stripe/billing-portal`, { method: 'POST' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Could not open billing portal'); window.location.href = data.url; } catch (err) { showToast(err.message, 'err'); if (btn) { btn.disabled = false; btn.innerHTML = 'Manage Billing'; } } } function applyRoleUI() { const isClient = currentUser?.role === 'client'; const tier = currentUser?.tier || ''; document.querySelectorAll('.agency-only').forEach(el => { el.style.display = isClient ? 'none' : ''; }); if (isClient) { currentClientId = currentUser.client_id; const monBtn = document.getElementById('btn-monitor'); if (monBtn) monBtn.style.display = 'none'; // Show My Report tab for quick_scan / geo_report clients const myReportTab = document.getElementById('nav-my-report'); if (myReportTab && (tier === 'quick_scan' || tier === 'geo_report')) { myReportTab.style.display = ''; } // Show Team tab only for CMO tier const teamTab = document.getElementById('nav-team'); if (teamTab && tier === 'cmo') { teamTab.style.display = ''; } // Show Manage Billing button for platform/cmo clients with a Stripe subscription const billingBtn = document.getElementById('manage-billing-btn'); if (billingBtn && (tier === 'platform' || tier === 'cmo')) { billingBtn.style.display = ''; } // For quick_scan / geo_report: hide monitor/overview, default to my-report if (tier === 'quick_scan' || tier === 'geo_report') { const monTab = document.querySelector('.ni[data-page="monitor"]'); if (monTab) monTab.style.display = 'none'; const ovTab = document.querySelector('.ni[data-page="overview"]'); if (ovTab) ovTab.style.display = 'none'; const articleTab = document.querySelector('.ni[data-page="articles"]'); if (articleTab) articleTab.style.display = 'none'; const brandTab = document.querySelector('.ni[data-page="brand-dna"]'); if (brandTab) brandTab.style.display = 'none'; const implTab = document.querySelector('.ni[data-page="implementation"]'); if (implTab) implTab.style.display = 'none'; const rptTab = document.querySelector('.ni[data-page="reports"]'); if (rptTab) rptTab.style.display = 'none'; } // Add nav section label const myReportTabEl = document.getElementById('nav-my-report'); if (myReportTabEl && myReportTabEl.style.display !== 'none' && !document.getElementById('client-nav-section')) { const labelEl = document.createElement('div'); labelEl.className = 'ns'; labelEl.id = 'client-nav-section'; labelEl.textContent = 'My Account'; myReportTabEl.parentNode.insertBefore(labelEl, myReportTabEl); } } const userNameEl = document.getElementById('sidebar-user-name'); if (userNameEl) userNameEl.textContent = currentUser.name || currentUser.email; const userRoleEl = document.getElementById('sidebar-user-role'); if (userRoleEl) { const tierLabels = { quick_scan: 'Quick Scan', geo_report: 'GEO Report', platform: 'Platform', cmo: 'CMO' }; userRoleEl.textContent = isClient ? (tierLabels[tier] || 'Client') : 'Agency'; } } // ── Boot ───────────────────────────────────────────────── (async () => { const authed = await initAuth(); if (!authed) return; applyRoleUI(); const tier = currentUser?.tier || ''; const isLimitedClient = currentUser?.role === 'client' && (tier === 'quick_scan' || tier === 'geo_report'); if (!isLimitedClient) { await loadClients(); const active = allClients.find(c => c.id === currentClientId) || allClients[0]; if (active) { currentClientId = active.id; document.getElementById('client-name').textContent = active.business_name; } } if (isLimitedClient) { navigate('my-report'); } else { await loadDashboard(); if (currentUser?.role === 'client' && currentUser?.first_login) { setTimeout(() => startTour(), 800); } } })();