import React from 'react'; import { Box, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Grid, Chip, Card, CardContent, Tooltip, useTheme, Alert, List, ListItem, ListItemText } from '@mui/material'; import { Speed as SpeedIcon, Description as DescriptionIcon, AccountTree as TreeIcon, TrendingUp as TrendingUpIcon, Lightbulb as LightbulbIcon, CheckCircle as CheckIcon } from '@mui/icons-material'; export interface BenchmarkMetrics { total_urls: number; publishing_velocity: number; average_path_depth: number; max_path_depth: number; top_url_patterns: Record; file_types: Record; date_range?: { start: string; end: string; }; } export interface Opportunity { type: string; title: string; items?: Array<{ section: string; competitor_presence: number; competitor_url_count: number; }>; metrics?: Record; } export interface BenchmarkData { user?: { summary: BenchmarkMetrics; error?: string; }; competitors?: { summaries: Record; errors?: Record; }; // Support for potential flat structure (legacy or different service versions) user_summary?: BenchmarkMetrics; competitor_summaries?: Record; timestamp?: string; benchmark?: { website_url?: string; competitors_analyzed?: number; user_sections_count?: number; competitor_section_leaders?: Array<{ competitor_url: string; total_urls: number; sections_count: number; publishing_velocity: number; average_path_depth?: number; lastmod_coverage?: number; }>; gaps?: { missing_sections?: Array<{ section: string; competitor_presence: number; competitor_url_count: number; }>; }; opportunities?: Array; gaps_vs_competitors?: { missing_sections?: Array<{ section: string; competitor_url_count: number; competitor_presence?: number; }>; }; keyword_hints?: Array<{ keyword: string; seen_in_url_patterns: boolean; }>; }; } export interface Props { data: BenchmarkData; } export const SitemapBenchmarkResults: React.FC = ({ data }) => { const theme = useTheme(); const { benchmark } = data; // Handle data mapping from potentially nested structure const user_summary = data.user_summary || data.user?.summary || {} as BenchmarkMetrics; const competitor_summaries = data.competitor_summaries || data.competitors?.summaries || {}; const competitor_errors = data.competitors?.errors || {}; const user_error = data.user?.error; // Calculate competitor averages const competitorUrls = Object.keys(competitor_summaries || {}); const avgCompetitorUrls = competitorUrls.length > 0 ? Math.round(competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.total_urls || 0), 0) / competitorUrls.length) : 0; const avgCompetitorVelocity = competitorUrls.length > 0 ? parseFloat((competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.publishing_velocity || 0), 0) / competitorUrls.length).toFixed(2)) : 0; const avgCompetitorDepth = competitorUrls.length > 0 ? parseFloat((competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.average_path_depth || 0), 0) / competitorUrls.length).toFixed(2)) : 0; const MetricCard = ({ title, userValue, competitorValue, icon, unit = '', description }: any) => { // const isBelowAvg = userValue < competitorValue; return ( {icon} {title} {userValue}{unit} Your Site {competitorValue}{unit} Competitor Avg = competitorValue ? 'Above Avg' : 'Below Avg'} size="small" sx={{ height: 24, fontSize: '0.7rem', fontWeight: 700, bgcolor: userValue >= competitorValue ? '#ecfdf5' : '#fff7ed', color: userValue >= competitorValue ? '#059669' : '#c2410c', border: `1px solid ${userValue >= competitorValue ? '#a7f3d0' : '#ffedd5'}`, borderRadius: 1.5 }} /> ); }; const metricDescriptions = { volume: "Total number of indexed pages discovered in your sitemap compared to the average of your top competitors.", velocity: "Average number of new pages published per week, indicating how active the content strategy is.", depth: "Average number of clicks required to reach content from the homepage based on URL structure." }; return ( Benchmark Analysis Comparison based on {competitorUrls.length} competitor sitemaps. {/* Main Metrics Row with Errors */} {/* Errors Area A (if exists) */} {(user_error || Object.keys(competitor_errors).length > 0) ? ( {user_error && ( User Sitemap Error: {user_error} )} {Object.keys(competitor_errors).length > 0 && ( {Object.keys(competitor_errors).length} competitors could not be analyzed: {Object.entries(competitor_errors).map(([url, error]) => ( ))} )} } description={metricDescriptions.volume} /> } unit=" /wk" description={metricDescriptions.velocity} /> } unit=" clicks" description={metricDescriptions.depth} /> ) : ( <> } description={metricDescriptions.volume} /> } unit=" /wk" description={metricDescriptions.velocity} /> } unit=" clicks" description={metricDescriptions.depth} /> )} {/* Industry Benchmarks Section */} Industry Benchmarks Common competitor sections you may be missing {(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections) && (benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || []).length > 0 ? ( {(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || []).map((gap: any, index: number) => ( /{gap.section} ))} ) : ( No significant content gaps identified compared to your competitors. )} {/* Actionable Insights */} {benchmark?.opportunities && benchmark.opportunities.length > 0 && ( Strategic Insights {benchmark.opportunities.map((opp: any, index: number) => ( ! {opp.title} {opp.metrics && ( {Object.entries(opp.metrics).map(([key, value]) => ( {(() => { if (key.startsWith('user_')) { return 'Your ' + key.replace('user_', '').replace(/_/g, ' '); } if (key.includes('competitor_median_')) { return 'Competitor Avg ' + key.replace('competitor_median_', '').replace(/_/g, ' '); } return key.replace(/_/g, ' '); })()} {typeof value === 'number' ? value.toFixed(2) : String(value)} ))} )} ))} )} {/* Competitor Leaders Table */} {benchmark?.competitor_section_leaders && benchmark.competitor_section_leaders.length > 0 && ( Top Competitor Stats Competitor Total URLs Sections Velocity/wk Avg Depth Date Coverage {benchmark.competitor_section_leaders.map((comp, idx) => ( {new URL(comp.competitor_url).hostname} {comp.total_urls} {comp.sections_count} {comp.publishing_velocity?.toFixed(1) || '-'} {comp.average_path_depth?.toFixed(1) || '-'} {comp.lastmod_coverage ? `${(comp.lastmod_coverage * 100).toFixed(0)}%` : '-'} ))}
)} {/* Competitor Content Strategy Patterns (formerly Content Gaps) */} {((benchmark?.gaps_vs_competitors?.missing_sections && benchmark.gaps_vs_competitors.missing_sections.length > 0) || (benchmark?.gaps?.missing_sections && benchmark.gaps.missing_sections.length > 0)) && ( Competitor Content Strategy Patterns The following content categories appear frequently across your competitors' websites but are missing from yours. Consider creating content in these areas to capture similar traffic and improve your competitive positioning. {(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || []) .filter((gap: any) => gap.section && gap.section.length > 3) // Filter out short sections like language codes (/es, /fr) .map((gap: any, index: number) => { // Fix for "200% Presence" bug: normalize values let presence = gap.competitor_presence || 0; // If presence > 1, it's likely a raw count, so normalize it if (presence > 1) { presence = presence / (competitorUrls.length || 1); } const percentage = Math.min(Math.round(presence * 100), 100); const count = gap.competitor_count || Math.round(presence * competitorUrls.length); return ( {gap.section} Used by {count} of {competitorUrls.length} competitors ); })} )} {benchmark?.keyword_hints && benchmark.keyword_hints.length > 0 && ( Keyword Opportunities (from URL patterns) {benchmark.keyword_hints.map((hint, index) => ( : undefined} sx={{ borderColor: theme.palette.divider }} /> ))} )} Website Total Pages Velocity (posts/week) Avg Depth Top Category {/* User Row */} Your Website {user_summary.total_urls} {user_summary.publishing_velocity?.toFixed(2) || '0.00'} {user_summary.average_path_depth?.toFixed(2) || '0.00'} {Object.keys(user_summary.top_url_patterns || {})[0] || '-'} {/* Competitor Rows */} {competitorUrls.map((url, idx) => { const data = competitor_summaries[url]; const domain = url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0]; return ( {domain} {data?.total_urls || 0} {data?.publishing_velocity?.toFixed(2) || '0.00'} {data?.average_path_depth?.toFixed(2) || '0.00'} {Object.keys(data?.top_url_patterns || {})[0] || '-'} ); })}
); };