feat(hooks): ✨ Add reusable useSeoQuery hook for dynamic SEO metadata generation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7b52d66c83
commit
b8d7dffa07
2 changed files with 179 additions and 0 deletions
|
|
@ -1,2 +1,3 @@
|
|||
export * from './useAdminQuery';
|
||||
export * from './useAnalyticsQuery';
|
||||
export * from './useSeoQuery';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue