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 / SEOScoreSummary.js

SEOScoreSummary.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/SEOScoreSummary.js

231 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * SEO Score Summary Component
3 *
4 * Displays the main SEO score summary card with Content Quality,
5 * Keyword Optimization, and Readability scores - restored from jQuery implementation
6 *
7 * @package ThinkRank
8 * @since 1.0.0
9 */
10
11 import { __ } from '@wordpress/i18n';
12 import { useState, useEffect } from '@wordpress/element';
13 import { Card, CardBody } from '@wordpress/components';
14 import {
15 calculateWordCount,
16 calculateKeywordDensity,
17 calculateReadabilityScore,
18 calculateContentQuality,
19 calculateKeywordOptimization,
20 analyzeHeadingStructure
21 } from './utils/contentAnalysis';
22
23 /**
24 * SEO Score Summary Component
25 *
26 * @param {Object} props Component props
27 * @param {Object} props.scoreData Complete scoring data from API
28 * @param {string} props.targetKeyword Focus keyword for analysis
29 * @param {string} props.postTitle Post title
30 * @param {string} props.metaDescription Meta description
31 * @returns {JSX.Element} SEO score summary component
32 */
33 const SEOScoreSummary = ({
34 scoreData,
35 targetKeyword = '',
36 postTitle = '',
37 metaDescription = ''
38 }) => {
39 const [contentAnalysis, setContentAnalysis] = useState({
40 wordCount: 0,
41 contentStructure: 'No content',
42 keywordDensity: '0%',
43 contentQuality: 'No content',
44 keywordOptimization: 'No focus keyword',
45 readabilityScore: 'No content'
46 });
47
48 /**
49 * Extract content from WordPress editor
50 */
51 const getPostContent = () => {
52 let content = '';
53 let htmlContent = '';
54
55 // Try to get content from Block Editor first
56 if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) {
57 try {
58 const blockContent = wp.data.select('core/editor').getEditedPostContent();
59 if (blockContent) {
60 htmlContent = blockContent;
61 }
62 } catch (e) {
63 // Block editor not available, try classic editor
64 }
65 }
66
67 // Fallback to TinyMCE (Classic Editor)
68 if (!htmlContent && typeof tinymce !== 'undefined') {
69 const editor = tinymce.get('content');
70 if (editor && !editor.isHidden()) {
71 htmlContent = editor.getContent();
72 }
73 }
74
75 // Fallback to textarea
76 if (!htmlContent) {
77 const contentTextarea = document.getElementById('content');
78 if (contentTextarea) {
79 htmlContent = contentTextarea.value || '';
80 }
81 }
82
83 // Convert HTML to plain text for analysis using DOMParser (safer than innerHTML)
84 if (htmlContent) {
85 try {
86 const parser = new DOMParser();
87 const doc = parser.parseFromString(htmlContent, 'text/html');
88 content = doc.body.textContent || doc.body.innerText || '';
89 } catch (error) {
90 // Fallback: strip HTML tags with regex if DOMParser fails
91 content = htmlContent.replace(/<[^>]*>/g, '');
92 }
93 }
94
95 return { content: content.trim(), htmlContent };
96 };
97
98 /**
99 * Update content analysis metrics
100 */
101 const updateContentAnalysis = () => {
102 const { content, htmlContent } = getPostContent();
103
104 if (!content) {
105 setContentAnalysis({
106 wordCount: 0,
107 contentStructure: 'No content',
108 keywordDensity: 'No keyword set',
109 contentQuality: 'No content',
110 keywordOptimization: 'No content',
111 readabilityScore: 'No content'
112 });
113 return;
114 }
115
116 // Calculate metrics using the same logic as jQuery implementation
117 const wordCount = calculateWordCount(content);
118 const headingAnalysis = analyzeHeadingStructure(htmlContent);
119 const readabilityScore = calculateReadabilityScore(content);
120 const contentQuality = calculateContentQuality(content, htmlContent);
121
122 let keywordDensity = 'No keyword set';
123 let keywordOptimization = 'No focus keyword';
124
125 if (targetKeyword && targetKeyword.trim()) {
126 const density = calculateKeywordDensity(content, targetKeyword);
127 keywordDensity = `${density}% (${targetKeyword})`;
128 keywordOptimization = calculateKeywordOptimization(
129 content,
130 htmlContent,
131 postTitle,
132 targetKeyword,
133 metaDescription
134 );
135 }
136
137 setContentAnalysis({
138 wordCount,
139 contentStructure: headingAnalysis.structure,
140 keywordDensity,
141 contentQuality,
142 keywordOptimization,
143 readabilityScore
144 });
145 };
146
147 // Update analysis when dependencies change
148 useEffect(() => {
149 updateContentAnalysis();
150 }, [targetKeyword, postTitle, metaDescription]);
151
152 // Update analysis periodically to catch content changes
153 useEffect(() => {
154 const interval = setInterval(updateContentAnalysis, 2000);
155 return () => clearInterval(interval);
156 }, [targetKeyword, postTitle, metaDescription]);
157
158 if (!scoreData) {
159 return null;
160 }
161
162 const { overall_score = 0, grade = 'F' } = scoreData;
163
164 /**
165 * Get score color class based on score
166 */
167 const getScoreColorClass = (score) => {
168 if (score >= 80) return 'score-high';
169 if (score >= 60) return 'score-medium';
170 return 'score-low';
171 };
172
173 return (
174 <Card className="seo-score-summary-card">
175 <CardBody>
176 <div className="analysis-section score-section">
177 {/* Main Score Circle */}
178 <div className={`score-circle ${getScoreColorClass(overall_score)}`}>
179 <span className="score-number" id="analysis-score">{overall_score}</span>
180 <span className="score-grade" id="seo-grade">{grade}</span>
181 <span className="score-label">{__('SEO Score', 'thinkrank')}</span>
182 </div>
183
184 {/* Three Key Metrics Breakdown */}
185 <div className="score-breakdown">
186 <div className="score-item">
187 <span className="score-label">{__('Content Quality', 'thinkrank')}</span>
188 <span className="score-value" id="content-quality">{contentAnalysis.contentQuality}</span>
189 </div>
190 <div className="score-item">
191 <span className="score-label">{__('Keyword Optimization', 'thinkrank')}</span>
192 <span className="score-value" id="keyword-optimization">{contentAnalysis.keywordOptimization}</span>
193 </div>
194 <div className="score-item">
195 <span className="score-label">{__('Readability', 'thinkrank')}</span>
196 <span className="score-value" id="readability-score">{contentAnalysis.readabilityScore}</span>
197 </div>
198 </div>
199 </div>
200
201 {/* Content Analysis Section */}
202 <div className="analysis-section content-analysis">
203 <h4>{__('Content Analysis', 'thinkrank')}</h4>
204 <div className="analysis-grid">
205 <div className="analysis-item">
206 <span className="analysis-label">{__('Word Count', 'thinkrank')}</span>
207 <span className="analysis-value" id="word-count">
208 {contentAnalysis.wordCount.toLocaleString()}
209 </span>
210 </div>
211 <div className="analysis-item">
212 <span className="analysis-label">{__('Structure', 'thinkrank')}</span>
213 <span className="analysis-value" id="content-structure">
214 {contentAnalysis.contentStructure}
215 </span>
216 </div>
217 <div className="analysis-item">
218 <span className="analysis-label">{__('Keyword Density', 'thinkrank')}</span>
219 <span className="analysis-value" id="keyword-density">
220 {contentAnalysis.keywordDensity}
221 </span>
222 </div>
223 </div>
224 </div>
225 </CardBody>
226 </Card>
227 );
228 };
229
230 export default SEOScoreSummary;
231