This document outlines the direct table queries used in the Web SEO dashboard frontend, replacing RPC functions with Supabase query builder calls.
All hooks now use direct table queries instead of RPC functions, making the code more maintainable and easier to debug.
Fetches overview metrics for a specific website by querying multiple tables directly.
Queries:
web_keyword_ranks- for keywords countweb_page_metrics- for traffic sumweb_audit_summary- for audit scoreweb_backlink_summary- for referring domains
Implementation:
// Get latest dates for each table
const [kwDateResult, pagesDateResult, blDateResult, auditDateResult] = await Promise.all([
supabase.from('web_keyword_ranks').select('date').eq('website_id', websiteId).order('date', { ascending: false }).limit(1),
supabase.from('web_page_metrics').select('date').eq('website_id', websiteId).order('date', { ascending: false }).limit(1),
supabase.from('web_backlink_summary').select('date').eq('website_id', websiteId).order('date', { ascending: false }).limit(1),
supabase.from('web_audit_summary').select('date').eq('website_id', websiteId).order('date', { ascending: false }).limit(1)
]);
// Get data for each metric using the latest dates
const [keywordsResult, trafficResult, auditResult, backlinksResult] = await Promise.all([
// Keywords count
kwDate ? supabase.from('web_keyword_ranks').select('id', { count: 'exact' }).eq('website_id', websiteId).eq('date', kwDate) : { data: [], count: 0 },
// Traffic sum
pagesDate ? supabase.from('web_page_metrics').select('est_traffic').eq('website_id', websiteId).eq('date', pagesDate) : { data: [] },
// Audit score
auditDate ? supabase.from('web_audit_summary').select('score').eq('website_id', websiteId).eq('date', auditDate).single() : { data: null },
// Referring domains
blDate ? supabase.from('web_backlink_summary').select('ref_domains').eq('website_id', websiteId).eq('date', blDate).single() : { data: null }
]);Fetches top landing pages for a specific website.
Query:
// Get latest date
const { data: latestDateData } = await supabase
.from('web_page_metrics')
.select('date')
.eq('website_id', websiteId)
.order('date', { ascending: false })
.limit(1);
// Get top pages with join to web_pages table
const { data, error } = await supabase
.from('web_page_metrics')
.select(`
page_id,
est_traffic,
organic_keywords,
web_pages!inner(url)
`)
.eq('website_id', websiteId)
.eq('date', latestDate)
.order('est_traffic', { ascending: false })
.limit(limit);Fetches top keywords with position changes for a specific website.
Queries:
// Get latest and previous dates
const { data: latestDateData } = await supabase
.from('web_keyword_ranks')
.select('date')
.eq('website_id', websiteId)
.order('date', { ascending: false })
.limit(1);
const { data: prevDateData } = await supabase
.from('web_keyword_ranks')
.select('date')
.eq('website_id', websiteId)
.lt('date', latestDate)
.order('date', { ascending: false })
.limit(1);
// Get current keywords with join to web_keywords table
const { data: currentData } = await supabase
.from('web_keyword_ranks')
.select(`
keyword_id,
position,
volume,
est_traffic,
date,
web_keywords!inner(keyword)
`)
.eq('website_id', websiteId)
.eq('date', latestDate)
.order('est_traffic', { ascending: false })
.limit(limit);
// Get previous positions for comparison
const { data: prevData } = await supabase
.from('web_keyword_ranks')
.select('keyword_id, position')
.eq('website_id', websiteId)
.eq('date', prevDate);Fetches competitors for a specific website.
Query:
// Get latest date
const { data: latestDateData } = await supabase
.from('web_competitor_domain_rollup')
.select('date')
.eq('website_id', websiteId)
.order('date', { ascending: false })
.limit(1);
// Get competitors data
const { data, error } = await supabase
.from('web_competitor_domain_rollup')
.select('competitor_domain, keywords_sum, est_traffic_sum, visibility_max')
.eq('website_id', websiteId)
.eq('date', latestDate)
.order('est_traffic_sum', { ascending: false })
.limit(limit);Fetches audit summary for a specific website.
Query:
const { data, error } = await supabase
.from('web_audit_summary')
.select('score, pages_crawled, high_issues, medium_issues, low_issues, date')
.eq('website_id', websiteId)
.order('date', { ascending: false })
.limit(1)
.single();Fetches audit issues for a specific website.
Query:
// Get latest date
const { data: latestDateData } = await supabase
.from('web_audit_issues')
.select('date')
.eq('website_id', websiteId)
.order('date', { ascending: false })
.limit(1);
// Get audit issues
const { data, error } = await supabase
.from('web_audit_issues')
.select('id, issue_type, priority, pages_affected, date')
.eq('website_id', websiteId)
.eq('date', latestDate)
.order('priority', { ascending: true })
.order('pages_affected', { ascending: false });Fetches websites for a specific client.
Query:
const { data, error } = await supabase
.from('websites')
.select('id, domain_clean')
.eq('client_id', clientId)
.order('created_at', { ascending: true });- No RPC Functions Required - Eliminates the need to create and maintain database functions
- Better Error Handling - Direct access to Supabase error messages
- Easier Debugging - Can see exactly what queries are being executed
- Type Safety - Better TypeScript integration with Supabase client
- Flexibility - Easy to modify queries without database changes
- Performance Monitoring - Built-in query performance tracking
The queries rely on the following table relationships:
web_keyword_ranks→web_keywords(viakeyword_id)web_page_metrics→web_pages(viapage_id)- All tables →
websites(viawebsite_id) - All tables →
clients(viaclient_id)
The implementation expects these tables to exist with the following key fields:
website_id,keyword_id,date,position,volume,est_traffic
website_id,page_id,date,est_traffic,organic_keywords
website_id,date,ref_domains
website_id,date,score,pages_crawled,high_issues,medium_issues,low_issues
website_id,date,issue_type,priority,pages_affected
website_id,date,competitor_domain,keywords_sum,est_traffic_sum,visibility_max
id,keyword
id,url
id,client_id,domain_clean,created_at
All queries include proper error handling with:
- Logging of errors with context
- Graceful fallbacks for missing data
- Type-safe error propagation
- Performance monitoring integration