/** * SEO Score Breakdown Component * * Displays detailed breakdown of the 10-factor SEO scoring system * * @package ThinkRank * @since 1.0.0 */ import { __ } from '@wordpress/i18n'; import { useState, useEffect } from '@wordpress/element'; import { Card, CardBody, CardHeader, ProgressBar, Button, Flex, FlexItem } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; /** * Score Breakdown Component * * @param {Object} props Component props * @param {Object} props.scoreData Complete scoring data from API * @param {Function} props.onRefresh Callback for refreshing scores (optional) * @param {boolean} props.showOverview Whether to show the overview card (default: true) * @returns {JSX.Element} Score breakdown component */ const ScoreBreakdown = ({ scoreData, onRefresh = null, showOverview = true }) => { const [expandedSections, setExpandedSections] = useState({}); const [isRefreshing, setIsRefreshing] = useState(false); // 2025 SEO Algorithm Factors (Google Q1 2025 Update) // Based on First Page Sage research and latest Google algorithm changes const scoreFactors = { satisfying_content: { label: __('Satisfying Content', 'thinkrank'), description: __('#1 ranking factor - Consistent publication of satisfying content (23%)', 'thinkrank'), maxScore: 23, icon: '🎯' }, title_optimization: { label: __('Title Optimization', 'thinkrank'), description: __('Keyword in meta title with looser matching requirements (14%)', 'thinkrank'), maxScore: 14, icon: '📝' }, niche_expertise: { label: __('Niche Expertise', 'thinkrank'), description: __('Hub & spoke content clusters demonstrating expertise (13%)', 'thinkrank'), maxScore: 13, icon: '🧠' }, searcher_engagement: { label: __('Searcher Engagement', 'thinkrank'), description: __('Dwell time, bounce rate, pages per session metrics (12%)', 'thinkrank'), maxScore: 12, icon: '👥' }, backlink_authority: { label: __('Backlink Authority', 'thinkrank'), description: __('Quality backlinks - declining but still important (13%)', 'thinkrank'), maxScore: 13, icon: '🔗' }, content_freshness: { label: __('Content Freshness', 'thinkrank'), description: __('Quarterly content updates and maintenance (6%)', 'thinkrank'), maxScore: 6, icon: '🔄' }, mobile_experience: { label: __('Mobile Experience Score', 'thinkrank'), description: __('NEW 2025: Mobile Experience Score (MES) replacing mobile-first (5%)', 'thinkrank'), maxScore: 5, icon: '📱' }, trustworthiness: { label: __('Trustworthiness', 'thinkrank'), description: __('E-A-T signals and brand authority indicators (5%)', 'thinkrank'), maxScore: 5, icon: '🛡️' }, link_diversity: { label: __('Link Distribution Diversity', 'thinkrank'), description: __('Diverse link profile and natural link patterns (4%)', 'thinkrank'), maxScore: 4, icon: '🌐' }, core_web_vitals: { label: __('Core Web Vitals 2.0', 'thinkrank'), description: __('Updated 2025 performance metrics and user experience (3%)', 'thinkrank'), maxScore: 3, icon: '⚡' }, site_security: { label: __('Site Security', 'thinkrank'), description: __('HTTPS, security headers, and trust signals (1%)', 'thinkrank'), maxScore: 1, icon: '🔒' }, internal_linking: { label: __('Internal Linking', 'thinkrank'), description: __('Strategic internal link structure and content clusters (1%)', 'thinkrank'), maxScore: 1, icon: '🔗' } }; /** * Toggle expanded state for a section */ const toggleSection = (factorKey) => { setExpandedSections(prev => ({ ...prev, [factorKey]: !prev[factorKey] })); }; /** * Get score color class based on percentage */ const getScoreColor = (score, maxScore) => { const percentage = (score / maxScore) * 100; if (percentage >= 90) return 'score-excellent'; if (percentage >= 70) return 'score-good'; if (percentage >= 50) return 'score-fair'; return 'score-poor'; }; /** * Handle refresh scores */ const handleRefresh = async () => { if (!onRefresh) return; setIsRefreshing(true); try { await onRefresh(); } finally { setIsRefreshing(false); } }; /** * Convert technical details to human-readable format */ const formatFactorDetails = (factorKey, details) => { if (!details || typeof details !== 'object') return null; const formatters = { satisfying_content: (d) => [ `Word count: ${d.word_count || 0} words (saved content)`, `Content satisfaction: ${d.intent_satisfaction || 0}%`, `Content uniqueness: ${d.uniqueness_score || 0}%`, `Content depth: ${d.content_depth || 'Basic'}` ], title_optimization: (d) => [ `Title length: ${d.title_length || 0} characters`, `Keyword in title: ${d.keyword_in_title ? 'Yes' : 'No'}`, `Title optimization: ${d.title_score || 0}%` ], niche_expertise: (d) => [ `Topic coverage: ${d.topic_coverage || 'Basic'}`, `Content depth: ${d.content_depth || 'Standard'}`, `Expertise signals: ${d.expertise_score || 0}%` ], searcher_engagement: (d) => [ `Content engagement: ${d.engagement_score || 0}%`, `Reading time: ${d.estimated_reading_time || 'Unknown'}`, `User experience: ${d.ux_score || 'Good'}` ], backlink_authority: (d) => [ `Authority score: ${d.authority_score || 0}%`, `Link quality: ${d.link_quality || 'Unknown'}`, `Domain authority: ${d.domain_authority || 'Not assessed'}` ], content_freshness: (d) => [ `Freshness status: ${d.freshness_status || 'Current'}`, `Last updated: ${d.last_updated ? new Date(d.last_updated).toLocaleDateString() : 'Unknown'}` ], mobile_experience: (d) => [ `Mobile score: ${d.mes_score || 'Good'}`, `Mobile friendly: ${d.mobile_friendly ? 'Yes' : 'No'}`, `Page speed: ${d.page_speed || 'Not tested'}` ], technical_factors: (d) => [ `Meta description: ${d.meta_description_length || 0} characters`, `Schema markup: ${d.schema_present ? 'Present' : 'Missing'}`, `Technical score: ${d.technical_score || 0}%` ] }; const formatter = formatters[factorKey]; return formatter ? formatter(details) : null; }; if (!scoreData || !scoreData.score_breakdown) { return (

{__('No scoring data available. Run an SEO analysis to see detailed breakdown.', 'thinkrank')}

); } const { overall_score, score_breakdown, grade, calculated_at } = scoreData; return (
{/* Overall Score Header - Only show if showOverview is true */} {showOverview && (

{__('SEO Score Breakdown', 'thinkrank')}

{onRefresh && ( )}
{overall_score} {grade}

{overall_score >= 80 && __('Excellent SEO optimization!', 'thinkrank')} {overall_score >= 60 && overall_score < 80 && __('Good SEO with room for improvement', 'thinkrank')} {overall_score >= 40 && overall_score < 60 && __('Fair SEO - needs attention', 'thinkrank')} {overall_score < 40 && __('Poor SEO - requires significant work', 'thinkrank')}

{calculated_at && (

{__('Last updated:', 'thinkrank')} {new Date(calculated_at).toLocaleString()}

)}
)} {/* Detailed Factor Breakdown */}

{__('Scoring Factors', 'thinkrank')}

{Object.entries(scoreFactors).map(([factorKey, factorConfig]) => { const factorData = score_breakdown[factorKey]; if (!factorData) return null; const isExpanded = expandedSections[factorKey]; const percentage = (factorData.score / factorData.max_score) * 100; return (
toggleSection(factorKey)} role="button" tabIndex={0} >
{factorConfig.icon}
{factorConfig.label} {factorConfig.description}
{factorData.score}/{factorData.max_score}
{isExpanded && (
{factorData.suggestions && factorData.suggestions.length > 0 && (
{__('Suggestions:', 'thinkrank')}
    {factorData.suggestions.map((suggestion, index) => (
  • {suggestion}
  • ))}
)} {factorData.details && (
{__('Details:', 'thinkrank')}
{formatFactorDetails(factorKey, factorData.details)?.map((detail, index) => (
{detail}
)) || (
{__('Technical details available', 'thinkrank')}
)}
)}
)}
); })}
); }; export default ScoreBreakdown;