/* 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.
Open in GSC
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.6pp GSC
Avg position {D.summary?.position?.toFixed(1)} −2.7 GSC · all queries
Clicks vs impressions
Daily · {rangeParam}
d.clicks)} previous={[]}/>
Top queries 28d Queries Pages Countries Devices
Query Clicks Imps CTR Pos Δ Pos
{(D.queries || []).map((q, i) => (
{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.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.8pp GA4 · 30d
Avg session duration {D.summary?.avgSessionDurationFormatted || '—'} +0:08 GA4 · 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
Page Sessions Engaged Conv Conv rate
{(D.pages || []).slice(0, 6).map((p, i) => (
{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} +3 Ahrefs · vs prev 30d
Referring domains {D.overview?.referringDomains} +14 Ahrefs · live
Backlinks {D.overview?.backlinks?.toLocaleString()} +112 Ahrefs · live
Dofollow ratio 82% +0.4pp Ahrefs
Lost · 30d 8 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
Domain DR Links Dofollow Domain traffic First seen Outreach
{(D.domains || []).map((d, i) => (
{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'}
Manage
>
) : (
Connect
)}
))}
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
Invite client user
>
);
}
function Field({ label, value }) {
return (
{label}
{value}
);
}
window.SettingsView = SettingsView;