/* Page: GSC Search Performance */ function GscView({ client, range }) { const rangeParam = range === 'Last 7 days' ? '7d' : range === 'Last 90 days' ? '90d' : '28d'; const { data, loading, error } = useApi(`/api/clients/${client.id}/gsc?range=${rangeParam}`, [client.id, rangeParam]); if (loading) return
Loading search performance...
; if (error) return
Failed to load GSC data: {error}
; const D = data || {}; return ( <>
Google Search Console · sc-domain:karatenation.ph

Search performance

Clicks, impressions, CTR, and average position. Query × page matrix maps where intent meets which page on the site.
Clicks · {rangeParam}{(D.summary?.clicks / 1000).toFixed(1)}k +22.1%GSC
Impressions · {rangeParam}{(D.summary?.impressions / 1000).toFixed(0)}k +14.6%GSC
CTR{D.summary?.ctr?.toFixed(1)}% +0.6ppGSC
Avg position{D.summary?.position?.toFixed(1)} −2.7GSC · all queries

Clicks vs impressions

Daily · {rangeParam}
Web Image News
d.clicks)} previous={[]}/>

Top queries

28d
QueriesPagesCountriesDevices
{(D.queries || []).map((q, i) => ( ))}
QueryClicksImpsCTRPosΔ Pos
{q.query} {(q.clicks || 0).toLocaleString()} {(q.impressions || 0).toLocaleString()}
{(q.ctr || 0).toFixed(1)}%
{(q.position || 0).toFixed(1)}

Striking-distance queries

Pos 8–20 · 1k+ imps

22 queries within reach of page-1. Optimise the mapped page for intent + entity coverage to convert impressions into clicks.

{[ { q: 'self defense classes near me', pos: 8.7, imps: 31800, page: '/programs/self-defense' }, { q: 'karate vs taekwondo for kids', pos: 12.4, imps: 18200, page: '/blog/karate-vs-taekwondo' }, { q: 'after school activities makati',pos: 11.2, imps: 9400, page: '/programs/after-school' }, { q: 'martial arts birthday party ph',pos: 14.6, imps: 6200, page: '/events/birthday-party' }, { q: 'karate near bgc', pos: 9.4, imps: 4800, page: '/locations/bgc' }, ].map((r, i) => (
{r.q}
{r.page}
{(r.imps/1000).toFixed(1)}k {r.pos.toFixed(1)}
))}
); } window.GscView = GscView; /* Page: GA4 Traffic & engagement */ function Ga4View({ client, range }) { const rangeParam = range === 'Last 7 days' ? '7d' : range === 'Last 90 days' ? '90d' : '30d'; const { data, loading, error } = useApi(`/api/clients/${client.id}/ga4?range=${rangeParam}`, [client.id, rangeParam]); if (loading) return
Loading traffic data...
; if (error) return
Failed to load GA4 data: {error}
; const D = data || {}; return ( <>
GA4 · property 287 654 032

Traffic & engagement

Sessions, users, channel mix, and conversions. Filtered to organic by default; toggle to see the full picture.
Sessions{((D.summary?.sessions || 0)/1000).toFixed(1)}k +14.2%GA4 · 30d
Users{((D.summary?.users || 0)/1000).toFixed(1)}k +12.6%GA4 · 30d
Engaged sessions{D.summary?.engagedRate != null ? D.summary.engagedRate.toFixed(1) : '—'}% +1.8ppGA4 · 30d
Avg session duration{D.summary?.avgSessionDurationFormatted || '—'}+0:08GA4 · 30d
Conversions{D.summary?.conversions != null ? D.summary.conversions.toLocaleString() : '—'} +24.1%GA4 · key events

Sessions by channel

Daily · 30d

Channel mix

a + c.value, 0)}/>
{(D.channels || []).map((c, i) => (
{c.name} {c.share.toFixed(1)}% = 0 ? 'up' : 'down'}`} style={{minWidth: 50, justifyContent:'flex-end'}}> {c.delta >= 0 ? '+' : ''}{c.delta.toFixed(1)}%
))}

Top landing pages

Organic only · 30d
{(D.pages || []).slice(0, 6).map((p, i) => ( ))}
PageSessionsEngagedConvConv rate
{p.url} ORGANIC
{p.sessions.toLocaleString()}
{Math.round((p.engagedSessions / Math.max(p.sessions,1)) * 100)}%
{p.conversions} {p.sessions > 0 ? ((p.conversions / p.sessions) * 100).toFixed(1) : '0.0'}%

Conversions by event

GA4 · 30d
({ label: cv.event.toUpperCase().replace(/_/g, ' '), value: cv.conversions, color: ['var(--gold)','var(--accent)','var(--gold-light)','var(--text-muted)','var(--text-dim)'][i], }))} max={Math.max(1, ...(D.conversions || []).slice(0,5).map(cv => cv.conversions))} format={v => v.toString()}/>
Note · Q2

Trial-form conversions drive the bulk of attributable revenue. Phone-click value is undercounted in GA4 — true number tracked in CallRail.

); } window.Ga4View = Ga4View; /* Page: Backlinks */ function BacklinksView({ client }) { const { data, loading, error } = useApi(`/api/clients/${client.id}/backlinks`, [client.id]); if (loading) return
Loading backlinks...
; if (error) return
Failed to load backlinks: {error}
; const D = data || {}; return ( <>
Ahrefs + Semrush · merged dataset

Backlinks

Referring domain growth, DR distribution, gained vs lost. Outreach-ready filters at the bottom of the table.
Domain rating{D.overview?.domainRating} +3Ahrefs · vs prev 30d
Referring domains{D.overview?.referringDomains} +14Ahrefs · live
Backlinks{D.overview?.backlinks?.toLocaleString()} +112Ahrefs · live
Dofollow ratio82%+0.4ppAhrefs
Lost · 30d8 4 from DR70+Ahrefs

Referring domains · growth

90d
h.refdomains)} previous={[]} height={220}/>

DR distribution

`${v} domains`}/>
14 referring domains at DR 70+ — strong national press coverage from PH publications. Mid-band (DR 30–50) is where most outreach should focus next cycle.

Top referring domains

Sorted by DR
All Gained 30d Lost 30d
{(D.domains || []).map((d, i) => ( ))}
DomainDRLinksDofollowDomain trafficFirst seenOutreach
{d.domain}
{d.domainRating}
{d.links} {d.dofollow} {d.firstSeen} {i % 3 === 0 ? 'Earned' : i % 3 === 1 ? 'Pitched' : 'Cold'}
); } window.BacklinksView = BacklinksView; /* Page: Settings / integrations */ function SettingsView({ client }) { const { data: health } = useApi('/api/sync/health', []); const recentSyncs = health?.recentSyncs || []; function getStatus(code) { const hit = recentSyncs.find(s => s.client_id === client?.id && s.integration === code.toLowerCase()); if (!hit) return 'available'; return hit.status === 'ok' ? 'connected' : 'error'; } function getLastSync(code) { const hit = recentSyncs.find(s => s.client_id === client?.id && s.integration === code.toLowerCase()); if (!hit) return null; const age = Math.floor(Date.now() / 1000) - hit.synced_at; return age < 3600 ? `${Math.round(age/60)}m ago` : age < 86400 ? `${Math.round(age/3600)}h ago` : `${Math.round(age/86400)}d ago`; } const sources = [ { name: 'Google Analytics 4', code: 'GA4', desc: 'Sessions, users, conversions, attribution.' }, { name: 'Google Search Console',code: 'GSC', desc: 'Queries, pages, clicks, impressions, position.' }, { name: 'Ahrefs', code: 'AHREFS', desc: 'Backlinks, referring domains, domain rating.' }, { name: 'Semrush', code: 'SEMRUSH', desc: 'Keyword positions, visibility, SERP features.' }, { name: 'Google Ads', code: 'ADS', desc: 'Paid campaigns, keywords, conversions.', future: true }, { name: 'Meta Ads', code: 'META', desc: 'Paid social campaigns, audiences, ROAS.', future: true }, { name: 'Looker Studio', code: 'LOOKER', desc: 'Embed live tiles into client portals.' }, { name: 'CallRail', code: 'CALLRAIL',desc: 'Call tracking attributed to keyword + page.' }, ].map(s => ({ ...s, status: getStatus(s.code), prop: recentSyncs.find(r => r.client_id === client?.id && r.integration === s.code.toLowerCase())?.integration || null, sync: getLastSync(s.code), })); return ( <>
Workspace · {client?.name || 'Client'}

Integrations

Connect data sources once per client. Tokens are encrypted and refreshed automatically. PPC and Paid Social sources will activate when those channels launch.
{sources.map((s, i) => (
{s.code}
{s.name} {s.future && PPC · soon}
{s.desc}
{s.sync && (
{s.code} · SYNCED {s.sync}
)}
{s.status === 'connected' || s.status === 'error' ? ( <> {s.status === 'connected' ? 'Connected' : 'Error'} ) : ( )}
))}

Workspace defaults

Client portal access

The client view is white-label by default. Toggle the agency wordmark on or off in workspace settings, or assign a custom subdomain.

SR
Sofia Reyes
OWNER · KARATENATION.PH
Active
MJ
Miguel Javier
VIEWER
Pending
); } function Field({ label, value }) { return (
{label} {value}
); } window.SettingsView = SettingsView;