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:
79
lib/api/gsc.ts
Normal file
79
lib/api/gsc.ts
Normal 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}`)
|
||||
}
|
||||
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user