feat: add Google Search Console integration UI

Search Console page with overview cards, top queries/pages tables,
and query↔page drill-down. Integrations tab in Settings for
connect/disconnect flow. New Search tab in site navigation.
This commit is contained in:
Usman Baig
2026-03-14 15:36:37 +01:00
parent 9b7781115f
commit 34c705549b
6 changed files with 987 additions and 5 deletions

79
lib/api/gsc.ts Normal file
View File

@@ -0,0 +1,79 @@
import apiRequest from './client'
// ─── Types ──────────────────────────────────────────────────────────
export interface GSCStatus {
connected: boolean
google_email?: string
gsc_property?: string
status?: 'active' | 'syncing' | 'error'
error_message?: string | null
last_synced_at?: string | null
created_at?: string
}
export interface GSCOverview {
total_clicks: number
total_impressions: number
avg_ctr: number
avg_position: number
prev_clicks: number
prev_impressions: number
prev_avg_ctr: number
prev_avg_position: number
}
export interface GSCDataRow {
query: string
page: string
impressions: number
clicks: number
ctr: number
position: number
}
export interface GSCQueryResponse {
queries: GSCDataRow[]
total: number
}
export interface GSCPageResponse {
pages: GSCDataRow[]
total: number
}
// ─── API Functions ──────────────────────────────────────────────────
export async function getGSCAuthURL(siteId: string): Promise<{ auth_url: string }> {
return apiRequest<{ auth_url: string }>(`/sites/${siteId}/integrations/gsc/auth-url`)
}
export async function getGSCStatus(siteId: string): Promise<GSCStatus> {
return apiRequest<GSCStatus>(`/sites/${siteId}/integrations/gsc/status`)
}
export async function disconnectGSC(siteId: string): Promise<void> {
await apiRequest(`/sites/${siteId}/integrations/gsc`, {
method: 'DELETE',
})
}
export async function getGSCOverview(siteId: string, startDate: string, endDate: string): Promise<GSCOverview> {
return apiRequest<GSCOverview>(`/sites/${siteId}/gsc/overview?start_date=${startDate}&end_date=${endDate}`)
}
export async function getGSCTopQueries(siteId: string, startDate: string, endDate: string, limit = 50, offset = 0): Promise<GSCQueryResponse> {
return apiRequest<GSCQueryResponse>(`/sites/${siteId}/gsc/top-queries?start_date=${startDate}&end_date=${endDate}&limit=${limit}&offset=${offset}`)
}
export async function getGSCTopPages(siteId: string, startDate: string, endDate: string, limit = 50, offset = 0): Promise<GSCPageResponse> {
return apiRequest<GSCPageResponse>(`/sites/${siteId}/gsc/top-pages?start_date=${startDate}&end_date=${endDate}&limit=${limit}&offset=${offset}`)
}
export async function getGSCQueryPages(siteId: string, query: string, startDate: string, endDate: string): Promise<GSCPageResponse> {
return apiRequest<GSCPageResponse>(`/sites/${siteId}/gsc/query-pages?query=${encodeURIComponent(query)}&start_date=${startDate}&end_date=${endDate}`)
}
export async function getGSCPageQueries(siteId: string, page: string, startDate: string, endDate: string): Promise<GSCQueryResponse> {
return apiRequest<GSCQueryResponse>(`/sites/${siteId}/gsc/page-queries?page=${encodeURIComponent(page)}&start_date=${startDate}&end_date=${endDate}`)
}

View File

@@ -33,6 +33,8 @@ import { listFunnels, type Funnel } from '@/lib/api/funnels'
import { getUptimeStatus, type UptimeStatusResponse } from '@/lib/api/uptime'
import { listGoals, type Goal } from '@/lib/api/goals'
import { listReportSchedules, type ReportSchedule } from '@/lib/api/report-schedules'
import { getGSCStatus, getGSCOverview, getGSCTopQueries, getGSCTopPages } from '@/lib/api/gsc'
import type { GSCStatus, GSCOverview, GSCQueryResponse, GSCPageResponse } from '@/lib/api/gsc'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import type {
Stats,
@@ -78,6 +80,10 @@ const fetchers = {
uptimeStatus: (siteId: string) => getUptimeStatus(siteId),
goals: (siteId: string) => listGoals(siteId),
reportSchedules: (siteId: string) => listReportSchedules(siteId),
gscStatus: (siteId: string) => getGSCStatus(siteId),
gscOverview: (siteId: string, start: string, end: string) => getGSCOverview(siteId, start, end),
gscTopQueries: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopQueries(siteId, start, end, limit, offset),
gscTopPages: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopPages(siteId, start, end, limit, offset),
subscription: () => getSubscription(),
}
@@ -397,6 +403,46 @@ export function useReportSchedules(siteId: string) {
)
}
// * Hook for GSC connection status
export function useGSCStatus(siteId: string) {
return useSWR<GSCStatus>(
siteId ? ['gscStatus', siteId] : null,
() => fetchers.gscStatus(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 30 * 1000,
}
)
}
// * Hook for GSC overview metrics (clicks, impressions, CTR, position)
export function useGSCOverview(siteId: string, start: string, end: string) {
return useSWR<GSCOverview>(
siteId && start && end ? ['gscOverview', siteId, start, end] : null,
() => fetchers.gscOverview(siteId, start, end),
dashboardSWRConfig
)
}
// * Hook for GSC top queries
export function useGSCTopQueries(siteId: string, start: string, end: string, limit = 50, offset = 0) {
return useSWR<GSCQueryResponse>(
siteId && start && end ? ['gscTopQueries', siteId, start, end, limit, offset] : null,
() => fetchers.gscTopQueries(siteId, start, end, limit, offset),
dashboardSWRConfig
)
}
// * Hook for GSC top pages
export function useGSCTopPages(siteId: string, start: string, end: string, limit = 50, offset = 0) {
return useSWR<GSCPageResponse>(
siteId && start && end ? ['gscTopPages', siteId, start, end, limit, offset] : null,
() => fetchers.gscTopPages(siteId, start, end, limit, offset),
dashboardSWRConfig
)
}
// * Hook for subscription details (changes rarely)
export function useSubscription() {
return useSWR<SubscriptionDetails>(