PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / admin / components / seo / ScoreBreakdown.js

ScoreBreakdown.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/admin/components/seo/ScoreBreakdown.js

350 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * SEO Score Breakdown Component
3 *
4 * Displays detailed breakdown of the 10-factor SEO scoring system
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { __ } from '@wordpress/i18n';
11 import { useState, useEffect } from '@wordpress/element';
12 import { Card, CardBody, CardHeader, ProgressBar, Button, Flex, FlexItem } from '@wordpress/components';
13 import { chevronDown, chevronUp } from '@wordpress/icons';
14
15 /**
16 * Score Breakdown Component
17 *
18 * @param {Object} props Component props
19 * @param {Object} props.scoreData Complete scoring data from API
20 * @param {Function} props.onRefresh Callback for refreshing scores (optional)
21 * @param {boolean} props.showOverview Whether to show the overview card (default: true)
22 * @returns {JSX.Element} Score breakdown component
23 */
24 const ScoreBreakdown = ({ scoreData, onRefresh = null, showOverview = true }) => {
25 const [expandedSections, setExpandedSections] = useState({});
26 const [isRefreshing, setIsRefreshing] = useState(false);
27
28 // 2025 SEO Algorithm Factors (Google Q1 2025 Update)
29 // Based on First Page Sage research and latest Google algorithm changes
30 const scoreFactors = {
31 satisfying_content: {
32 label: __('Satisfying Content', 'thinkrank'),
33 description: __('#1 ranking factor - Consistent publication of satisfying content (23%)', 'thinkrank'),
34 maxScore: 23,
35 icon: '🎯'
36 },
37 title_optimization: {
38 label: __('Title Optimization', 'thinkrank'),
39 description: __('Keyword in meta title with looser matching requirements (14%)', 'thinkrank'),
40 maxScore: 14,
41 icon: '📝'
42 },
43 niche_expertise: {
44 label: __('Niche Expertise', 'thinkrank'),
45 description: __('Hub & spoke content clusters demonstrating expertise (13%)', 'thinkrank'),
46 maxScore: 13,
47 icon: '🧠'
48 },
49 searcher_engagement: {
50 label: __('Searcher Engagement', 'thinkrank'),
51 description: __('Dwell time, bounce rate, pages per session metrics (12%)', 'thinkrank'),
52 maxScore: 12,
53 icon: '👥'
54 },
55 backlink_authority: {
56 label: __('Backlink Authority', 'thinkrank'),
57 description: __('Quality backlinks - declining but still important (13%)', 'thinkrank'),
58 maxScore: 13,
59 icon: '🔗'
60 },
61 content_freshness: {
62 label: __('Content Freshness', 'thinkrank'),
63 description: __('Quarterly content updates and maintenance (6%)', 'thinkrank'),
64 maxScore: 6,
65 icon: '🔄'
66 },
67 mobile_experience: {
68 label: __('Mobile Experience Score', 'thinkrank'),
69 description: __('NEW 2025: Mobile Experience Score (MES) replacing mobile-first (5%)', 'thinkrank'),
70 maxScore: 5,
71 icon: '📱'
72 },
73 trustworthiness: {
74 label: __('Trustworthiness', 'thinkrank'),
75 description: __('E-A-T signals and brand authority indicators (5%)', 'thinkrank'),
76 maxScore: 5,
77 icon: '🛡️'
78 },
79 link_diversity: {
80 label: __('Link Distribution Diversity', 'thinkrank'),
81 description: __('Diverse link profile and natural link patterns (4%)', 'thinkrank'),
82 maxScore: 4,
83 icon: '🌐'
84 },
85 core_web_vitals: {
86 label: __('Core Web Vitals 2.0', 'thinkrank'),
87 description: __('Updated 2025 performance metrics and user experience (3%)', 'thinkrank'),
88 maxScore: 3,
89 icon: ''
90 },
91 site_security: {
92 label: __('Site Security', 'thinkrank'),
93 description: __('HTTPS, security headers, and trust signals (1%)', 'thinkrank'),
94 maxScore: 1,
95 icon: '🔒'
96 },
97 internal_linking: {
98 label: __('Internal Linking', 'thinkrank'),
99 description: __('Strategic internal link structure and content clusters (1%)', 'thinkrank'),
100 maxScore: 1,
101 icon: '🔗'
102 }
103 };
104
105 /**
106 * Toggle expanded state for a section
107 */
108 const toggleSection = (factorKey) => {
109 setExpandedSections(prev => ({
110 ...prev,
111 [factorKey]: !prev[factorKey]
112 }));
113 };
114
115 /**
116 * Get score color class based on percentage
117 */
118 const getScoreColor = (score, maxScore) => {
119 const percentage = (score / maxScore) * 100;
120 if (percentage >= 90) return 'score-excellent';
121 if (percentage >= 70) return 'score-good';
122 if (percentage >= 50) return 'score-fair';
123 return 'score-poor';
124 };
125
126 /**
127 * Handle refresh scores
128 */
129 const handleRefresh = async () => {
130 if (!onRefresh) return;
131
132 setIsRefreshing(true);
133 try {
134 await onRefresh();
135 } finally {
136 setIsRefreshing(false);
137 }
138 };
139
140 /**
141 * Convert technical details to human-readable format
142 */
143 const formatFactorDetails = (factorKey, details) => {
144 if (!details || typeof details !== 'object') return null;
145
146 const formatters = {
147 satisfying_content: (d) => [
148 `Word count: ${d.word_count || 0} words (saved content)`,
149 `Content satisfaction: ${d.intent_satisfaction || 0}%`,
150 `Content uniqueness: ${d.uniqueness_score || 0}%`,
151 `Content depth: ${d.content_depth || 'Basic'}`
152 ],
153 title_optimization: (d) => [
154 `Title length: ${d.title_length || 0} characters`,
155 `Keyword in title: ${d.keyword_in_title ? 'Yes' : 'No'}`,
156 `Title optimization: ${d.title_score || 0}%`
157 ],
158 niche_expertise: (d) => [
159 `Topic coverage: ${d.topic_coverage || 'Basic'}`,
160 `Content depth: ${d.content_depth || 'Standard'}`,
161 `Expertise signals: ${d.expertise_score || 0}%`
162 ],
163 searcher_engagement: (d) => [
164 `Content engagement: ${d.engagement_score || 0}%`,
165 `Reading time: ${d.estimated_reading_time || 'Unknown'}`,
166 `User experience: ${d.ux_score || 'Good'}`
167 ],
168 backlink_authority: (d) => [
169 `Authority score: ${d.authority_score || 0}%`,
170 `Link quality: ${d.link_quality || 'Unknown'}`,
171 `Domain authority: ${d.domain_authority || 'Not assessed'}`
172 ],
173 content_freshness: (d) => [
174 `Freshness status: ${d.freshness_status || 'Current'}`,
175 `Last updated: ${d.last_updated ? new Date(d.last_updated).toLocaleDateString() : 'Unknown'}`
176 ],
177 mobile_experience: (d) => [
178 `Mobile score: ${d.mes_score || 'Good'}`,
179 `Mobile friendly: ${d.mobile_friendly ? 'Yes' : 'No'}`,
180 `Page speed: ${d.page_speed || 'Not tested'}`
181 ],
182 technical_factors: (d) => [
183 `Meta description: ${d.meta_description_length || 0} characters`,
184 `Schema markup: ${d.schema_present ? 'Present' : 'Missing'}`,
185 `Technical score: ${d.technical_score || 0}%`
186 ]
187 };
188
189 const formatter = formatters[factorKey];
190 return formatter ? formatter(details) : null;
191 };
192
193 if (!scoreData || !scoreData.score_breakdown) {
194 return (
195 <Card>
196 <CardBody>
197 <p>{__('No scoring data available. Run an SEO analysis to see detailed breakdown.', 'thinkrank')}</p>
198 </CardBody>
199 </Card>
200 );
201 }
202
203 const { overall_score, score_breakdown, grade, calculated_at } = scoreData;
204
205 return (
206 <div className="thinkrank-score-breakdown">
207 {/* Overall Score Header - Only show if showOverview is true */}
208 {showOverview && (
209 <Card className="score-overview-card">
210 <CardHeader>
211 <Flex justify="space-between" align="center">
212 <FlexItem>
213 <h3>{__('SEO Score Breakdown', 'thinkrank')}</h3>
214 </FlexItem>
215 {onRefresh && (
216 <FlexItem>
217 <Button
218 variant="secondary"
219 onClick={handleRefresh}
220 isBusy={isRefreshing}
221 disabled={isRefreshing}
222 >
223 {__('Refresh', 'thinkrank')}
224 </Button>
225 </FlexItem>
226 )}
227 </Flex>
228 </CardHeader>
229 <CardBody>
230 <div className="overall-score-display">
231 <div className={`score-circle ${getScoreColor(overall_score, 100)}`}>
232 <span className="score-number">{overall_score}</span>
233 <span className="score-grade">{grade}</span>
234 </div>
235 <div className="score-info">
236 <p className="score-description">
237 {overall_score >= 80 && __('Excellent SEO optimization!', 'thinkrank')}
238 {overall_score >= 60 && overall_score < 80 && __('Good SEO with room for improvement', 'thinkrank')}
239 {overall_score >= 40 && overall_score < 60 && __('Fair SEO - needs attention', 'thinkrank')}
240 {overall_score < 40 && __('Poor SEO - requires significant work', 'thinkrank')}
241 </p>
242 {calculated_at && (
243 <p className="score-timestamp">
244 {__('Last updated:', 'thinkrank')} {new Date(calculated_at).toLocaleString()}
245 </p>
246 )}
247 </div>
248 </div>
249 </CardBody>
250 </Card>
251 )}
252
253 {/* Detailed Factor Breakdown */}
254 <Card className="score-factors-card">
255 <CardHeader>
256 <h4>{__('Scoring Factors', 'thinkrank')}</h4>
257 </CardHeader>
258 <CardBody>
259 <div className="score-factors-list">
260 {Object.entries(scoreFactors).map(([factorKey, factorConfig]) => {
261 const factorData = score_breakdown[factorKey];
262 if (!factorData) return null;
263
264 const isExpanded = expandedSections[factorKey];
265 const percentage = (factorData.score / factorData.max_score) * 100;
266
267 return (
268 <div key={factorKey} className="score-factor-item">
269 <div
270 className="factor-header"
271 onClick={() => toggleSection(factorKey)}
272 role="button"
273 tabIndex={0}
274 >
275 <Flex justify="space-between" align="center">
276 <FlexItem>
277 <div className="factor-info">
278 <span className="factor-icon">{factorConfig.icon}</span>
279 <div className="factor-text">
280 <span className="factor-label">{factorConfig.label}</span>
281 <span className="factor-description">{factorConfig.description}</span>
282 </div>
283 </div>
284 </FlexItem>
285 <FlexItem className="factor-score-container">
286 <div className="factor-score">
287 <span className="score-value">
288 {factorData.score}/{factorData.max_score}
289 </span>
290 <div className="score-bar">
291 <div
292 className={`score-bar-fill ${getScoreColor(factorData.score, factorData.max_score)}`}
293 style={{ width: `${percentage}%` }}
294 />
295 </div>
296 </div>
297 </FlexItem>
298 <FlexItem>
299 <Button
300 variant="tertiary"
301 icon={isExpanded ? chevronUp : chevronDown}
302 size="small"
303 />
304 </FlexItem>
305 </Flex>
306 </div>
307
308 {isExpanded && (
309 <div className="factor-details">
310 {factorData.suggestions && factorData.suggestions.length > 0 && (
311 <div className="factor-suggestions">
312 <h5>{__('Suggestions:', 'thinkrank')}</h5>
313 <ul>
314 {factorData.suggestions.map((suggestion, index) => (
315 <li key={index}>{suggestion}</li>
316 ))}
317 </ul>
318 </div>
319 )}
320
321 {factorData.details && (
322 <div className="factor-technical-details">
323 <h5>{__('Details:', 'thinkrank')}</h5>
324 <div className="details-list">
325 {formatFactorDetails(factorKey, factorData.details)?.map((detail, index) => (
326 <div key={index} className="detail-item">
327 {detail}
328 </div>
329 )) || (
330 <div className="detail-item">
331 {__('Technical details available', 'thinkrank')}
332 </div>
333 )}
334 </div>
335 </div>
336 )}
337 </div>
338 )}
339 </div>
340 );
341 })}
342 </div>
343 </CardBody>
344 </Card>
345 </div>
346 );
347 };
348
349 export default ScoreBreakdown;
350