feat(hooks): Add reusable useSeoQuery hook for dynamic SEO metadata generation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-04 07:56:31 -07:00
parent 7b52d66c83
commit b8d7dffa07
2 changed files with 179 additions and 0 deletions

View file

@ -1,2 +1,3 @@
export * from './useAdminQuery';
export * from './useAnalyticsQuery';
export * from './useSeoQuery';

View file

@ -0,0 +1,178 @@
/**
* SEO Query Hooks
*
* Typed wrappers around @tanstack/react-query for the SEO analytics page.
* Endpoints map to the SeoController routes under `insights/seo/`.
*/
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useAnalyticsFilters } from '../context/AnalyticsFilterContext';
import type {
SeoOverviewKPIs,
SeoLandingPagesResponse,
SeoCampaignsResponse,
SeoRankingsResponse,
SeoKeywordsResponse,
SeoPositionTrendPoint,
SeoCrawlCoverageResponse,
} from '../types/analytics-api';
// ============================================================================
// Stale Time
// ============================================================================
const STALE_TIME = 300_000; // 5 minutes — SEO data changes slowly
// ============================================================================
// Base Fetch Helper
// ============================================================================
function buildUrl(endpoint: string, params?: Record<string, string>): string {
const base = `/api/analytics/insights/seo/${endpoint}`;
if (!params || Object.keys(params).length === 0) return base;
const search = new URLSearchParams(params).toString();
return `${base}?${search}`;
}
async function fetchSeo<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = buildUrl(endpoint, params);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`SEO API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// ============================================================================
// Overview Hook
// ============================================================================
export function useSeoOverview(): UseQueryResult<SeoOverviewKPIs> {
const { filterParams } = useAnalyticsFilters();
return useQuery<SeoOverviewKPIs>({
queryKey: ['analytics', 'seo', 'overview', filterParams],
queryFn: () => fetchSeo<SeoOverviewKPIs>('overview', filterParams),
staleTime: STALE_TIME,
});
}
// ============================================================================
// Landing Pages Hook
// ============================================================================
export function useSeoLandingPages(options?: {
domain?: string;
sort?: 'views' | 'time' | 'bounce';
limit?: number;
}): UseQueryResult<SeoLandingPagesResponse> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams };
if (options?.domain) params.domain = options.domain;
if (options?.sort) params.sort = options.sort;
if (options?.limit) params.limit = String(options.limit);
return useQuery<SeoLandingPagesResponse>({
queryKey: ['analytics', 'seo', 'landing-pages', params],
queryFn: () => fetchSeo<SeoLandingPagesResponse>('landing-pages', params),
staleTime: STALE_TIME,
});
}
// ============================================================================
// Campaigns Hook
// ============================================================================
export function useSeoCampaigns(campaignId?: string): UseQueryResult<SeoCampaignsResponse> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams };
if (campaignId) params.campaignId = campaignId;
return useQuery<SeoCampaignsResponse>({
queryKey: ['analytics', 'seo', 'campaigns', params],
queryFn: () => fetchSeo<SeoCampaignsResponse>('campaigns', params),
staleTime: STALE_TIME,
});
}
// ============================================================================
// Rankings Hook (GSC data — task #7 endpoint)
// ============================================================================
export function useSeoRankings(options?: {
path?: string;
domain?: string;
limit?: number;
}): UseQueryResult<SeoRankingsResponse> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams };
if (options?.path) params.path = options.path;
if (options?.domain) params.domain = options.domain;
if (options?.limit) params.limit = String(options.limit);
return useQuery<SeoRankingsResponse>({
queryKey: ['analytics', 'seo', 'rankings', params],
queryFn: () => fetchSeo<SeoRankingsResponse>('rankings', params),
staleTime: STALE_TIME,
});
}
// ============================================================================
// Keywords Hook (GSC data — task #7 endpoint)
// ============================================================================
export function useSeoKeywords(options?: {
path?: string;
domain?: string;
limit?: number;
}): UseQueryResult<SeoKeywordsResponse> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams };
if (options?.path) params.path = options.path;
if (options?.domain) params.domain = options.domain;
if (options?.limit) params.limit = String(options.limit);
return useQuery<SeoKeywordsResponse>({
queryKey: ['analytics', 'seo', 'keywords', params],
queryFn: () => fetchSeo<SeoKeywordsResponse>('keywords', params),
staleTime: STALE_TIME,
});
}
// ============================================================================
// Position Trend Hook (for selected page drill-down)
// ============================================================================
export function useSeoPositionTrend(
path: string,
options?: { domain?: string },
): UseQueryResult<SeoPositionTrendPoint[]> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams, path };
if (options?.domain) params.domain = options.domain;
return useQuery<SeoPositionTrendPoint[]>({
queryKey: ['analytics', 'seo', 'position-trend', params],
queryFn: () => fetchSeo<SeoPositionTrendPoint[]>('rankings/trend', params),
staleTime: STALE_TIME,
enabled: path.length > 0,
});
}
// ============================================================================
// Crawl Coverage Hook
// ============================================================================
export function useSeoCrawlCoverage(options?: {
campaignId?: string;
}): UseQueryResult<SeoCrawlCoverageResponse> {
const { filterParams } = useAnalyticsFilters();
const params: Record<string, string> = { ...filterParams };
if (options?.campaignId) params.campaignId = options.campaignId;
return useQuery<SeoCrawlCoverageResponse>({
queryKey: ['analytics', 'seo', 'crawl-coverage', params],
queryFn: () => fetchSeo<SeoCrawlCoverageResponse>('crawl-coverage', params),
staleTime: STALE_TIME,
});
}