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

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

292 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * SEO Improvement Suggestions Component
3 *
4 * Displays actionable SEO improvement recommendations
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { __ } from '@wordpress/i18n';
11 import { useState } from '@wordpress/element';
12 import { Card, CardBody, CardHeader, Button, Flex, FlexItem, Notice } from '@wordpress/components';
13 import { check, close, lightbulb } from '@wordpress/icons';
14
15 /**
16 * Improvement Suggestions Component
17 *
18 * @param {Object} props Component props
19 * @param {Array} props.suggestions Array of suggestion strings
20 * @param {Function} props.onApplySuggestion Callback for applying suggestions
21 * @param {Function} props.onDismissSuggestion Callback for dismissing suggestions
22 * @returns {JSX.Element} Improvement suggestions component
23 */
24 const ImprovementSuggestions = ({
25 suggestions = [],
26 onApplySuggestion,
27 onDismissSuggestion
28 }) => {
29 const [dismissedSuggestions, setDismissedSuggestions] = useState(new Set());
30 const [applyingIndex, setApplyingIndex] = useState(null);
31
32 /**
33 * Handle applying a suggestion
34 */
35 const handleApplySuggestion = async (suggestion, index) => {
36 if (!onApplySuggestion) return;
37
38 setApplyingIndex(index);
39 try {
40 await onApplySuggestion(suggestion, index);
41 } finally {
42 setApplyingIndex(null);
43 }
44 };
45
46 /**
47 * Handle dismissing a suggestion
48 */
49 const handleDismissSuggestion = (suggestion, index) => {
50 // Call parent handler first (it may show confirmation)
51 let shouldDismiss = true;
52 if (onDismissSuggestion) {
53 shouldDismiss = onDismissSuggestion(suggestion, index);
54 }
55
56 // Only dismiss if parent handler allows it
57 if (shouldDismiss !== false) {
58 setDismissedSuggestions(prev => new Set([...prev, index]));
59 }
60 };
61
62 /**
63 * Get suggestion priority level - Enhanced to match jQuery implementation
64 */
65 const getSuggestionPriority = (suggestion) => {
66 // Handle both string and object formats (matching jQuery implementation)
67 let suggestionText = '';
68 let explicitPriority = null;
69
70 if (typeof suggestion === 'string') {
71 suggestionText = suggestion;
72 } else if (suggestion && suggestion.text) {
73 suggestionText = suggestion.text;
74 explicitPriority = suggestion.priority;
75 } else {
76 suggestionText = String(suggestion);
77 }
78
79 // If explicit priority is set, use it
80 if (explicitPriority) {
81 return explicitPriority.toLowerCase();
82 }
83
84 // Enhanced keyword detection for better priority classification
85 const suggestionLower = suggestionText.toLowerCase();
86
87 // High priority indicators
88 const highPriorityKeywords = [
89 'missing', 'required', 'must', 'critical', 'important', 'essential',
90 'no h1', 'no title', 'no meta description', 'too short', 'too long',
91 'duplicate', 'broken', 'error', 'invalid', 'empty'
92 ];
93
94 // Low priority indicators
95 const lowPriorityKeywords = [
96 'consider', 'might', 'could', 'optional', 'suggestion', 'recommend',
97 'try', 'perhaps', 'maybe', 'potentially', 'enhance', 'improve slightly'
98 ];
99
100 // Medium priority indicators
101 const mediumPriorityKeywords = [
102 'should', 'add', 'include', 'optimize', 'update', 'modify',
103 'adjust', 'review', 'check', 'ensure', 'verify'
104 ];
105
106 if (highPriorityKeywords.some(keyword => suggestionLower.includes(keyword))) {
107 return 'high';
108 }
109
110 if (lowPriorityKeywords.some(keyword => suggestionLower.includes(keyword))) {
111 return 'low';
112 }
113
114 if (mediumPriorityKeywords.some(keyword => suggestionLower.includes(keyword))) {
115 return 'medium';
116 }
117
118 // Default to medium priority
119 return 'medium';
120 };
121
122 /**
123 * Get suggestion category based on content - Enhanced categorization
124 */
125 const getSuggestionCategory = (suggestion) => {
126 // Handle both string and object formats
127 let suggestionText = '';
128 if (typeof suggestion === 'string') {
129 suggestionText = suggestion;
130 } else if (suggestion && suggestion.text) {
131 suggestionText = suggestion.text;
132 } else {
133 suggestionText = String(suggestion);
134 }
135
136 const suggestionLower = suggestionText.toLowerCase();
137
138 // Enhanced category detection
139 if (suggestionLower.includes('title') || suggestionLower.includes('h1')) return 'title';
140 if (suggestionLower.includes('meta description') || suggestionLower.includes('description')) return 'meta';
141 if (suggestionLower.includes('heading') || suggestionLower.includes('h2') || suggestionLower.includes('h3') || suggestionLower.includes('subheading')) return 'headings';
142 if (suggestionLower.includes('keyword') || suggestionLower.includes('focus keyword') || suggestionLower.includes('density')) return 'keywords';
143 if (suggestionLower.includes('content') || suggestionLower.includes('word count') || suggestionLower.includes('paragraph')) return 'content';
144 if (suggestionLower.includes('readability') || suggestionLower.includes('flesch') || suggestionLower.includes('reading')) return 'readability';
145 if (suggestionLower.includes('link') || suggestionLower.includes('internal') || suggestionLower.includes('external')) return 'links';
146 if (suggestionLower.includes('image') || suggestionLower.includes('alt') || suggestionLower.includes('media')) return 'images';
147 if (suggestionLower.includes('url') || suggestionLower.includes('permalink') || suggestionLower.includes('slug')) return 'url';
148 if (suggestionLower.includes('mobile') || suggestionLower.includes('responsive')) return 'mobile';
149 if (suggestionLower.includes('speed') || suggestionLower.includes('performance') || suggestionLower.includes('loading')) return 'performance';
150
151 return 'general';
152 };
153
154 // Filter out dismissed suggestions
155 const activeSuggestions = suggestions.filter((_, index) => !dismissedSuggestions.has(index));
156
157 if (activeSuggestions.length === 0) {
158 return (
159 <Card className="improvement-suggestions-card">
160 <CardBody>
161 <div className="no-suggestions">
162 <div className="success-icon">�
163 </div>
164 <h4>{__('Excellent Work!', 'thinkrank')}</h4>
165 <p>{__('No immediate SEO improvements needed. Your content is well optimized!', 'thinkrank')}</p>
166 </div>
167 </CardBody>
168 </Card>
169 );
170 }
171
172 // Group suggestions by priority
173 const groupedSuggestions = activeSuggestions.reduce((groups, suggestion, originalIndex) => {
174 const priority = getSuggestionPriority(suggestion);
175 if (!groups[priority]) groups[priority] = [];
176 groups[priority].push({ suggestion, originalIndex });
177 return groups;
178 }, {});
179
180 const priorityOrder = ['high', 'medium', 'low'];
181 const priorityLabels = {
182 high: __('High Priority', 'thinkrank'),
183 medium: __('Medium Priority', 'thinkrank'),
184 low: __('Low Priority', 'thinkrank')
185 };
186
187 const priorityIcons = {
188 high: '🔴',
189 medium: '🟡',
190 low: '🟢'
191 };
192
193 return (
194 <Card className="improvement-suggestions-card">
195 <CardHeader>
196 <Flex align="center">
197 <FlexItem>
198 <div className="suggestions-header">
199 <span className="suggestions-icon">💡</span>
200 <h4>{__('SEO Improvement Suggestions', 'thinkrank')}</h4>
201 </div>
202 </FlexItem>
203 <FlexItem>
204 <span className="suggestions-count">
205 {activeSuggestions.length} {activeSuggestions.length === 1 ? __('suggestion', 'thinkrank') : __('suggestions', 'thinkrank')}
206 </span>
207 </FlexItem>
208 </Flex>
209 </CardHeader>
210 <CardBody>
211 <div className="suggestions-list">
212 {priorityOrder.map(priority => {
213 if (!groupedSuggestions[priority]) return null;
214
215 return (
216 <div key={priority} className={`priority-group priority-${priority}`}>
217 <h5 className="priority-header">
218 <span className="priority-icon">{priorityIcons[priority]}</span>
219 {priorityLabels[priority]}
220 <span className="priority-count">({groupedSuggestions[priority].length})</span>
221 </h5>
222
223 <div className="suggestions-in-group">
224 {groupedSuggestions[priority].map(({ suggestion, originalIndex }) => {
225 const category = getSuggestionCategory(suggestion);
226 const isApplying = applyingIndex === originalIndex;
227
228 // Extract suggestion text for display
229 const suggestionText = typeof suggestion === 'string' ? suggestion :
230 (suggestion && suggestion.text) ? suggestion.text :
231 String(suggestion);
232
233 return (
234 <div key={originalIndex} className={`suggestion-item priority-${priority} category-${category}`}>
235 <div className="suggestion-content">
236 <div className="suggestion-text">
237 <span className="category-badge">{category}</span>
238 <span className="suggestion-message">{suggestionText}</span>
239 <span className={`priority-badge priority-${priority}`}>
240 {priority.charAt(0).toUpperCase() + priority.slice(1)}
241 </span>
242 </div>
243
244 <div className="suggestion-actions">
245 {onApplySuggestion && (
246 <Button
247 variant="primary"
248 size="small"
249 icon={check}
250 onClick={() => handleApplySuggestion(suggestion, originalIndex)}
251 isBusy={isApplying}
252 disabled={isApplying}
253 title={__('Apply this suggestion', 'thinkrank')}
254 >
255 {isApplying ? __('Applying...', 'thinkrank') : __('Apply', 'thinkrank')}
256 </Button>
257 )}
258
259 <Button
260 variant="tertiary"
261 size="small"
262 icon={close}
263 onClick={() => handleDismissSuggestion(suggestion, originalIndex)}
264 title={__('Dismiss this suggestion', 'thinkrank')}
265 >
266 {__('Dismiss', 'thinkrank')}
267 </Button>
268 </div>
269 </div>
270 </div>
271 );
272 })}
273 </div>
274 </div>
275 );
276 })}
277 </div>
278
279 {dismissedSuggestions.size > 0 && (
280 <div className="dismissed-suggestions-notice">
281 <Notice status="info" isDismissible={false}>
282 {dismissedSuggestions.size} {dismissedSuggestions.size === 1 ? __('suggestion', 'thinkrank') : __('suggestions', 'thinkrank')} {__('dismissed', 'thinkrank')}
283 </Notice>
284 </div>
285 )}
286 </CardBody>
287 </Card>
288 );
289 };
290
291 export default ImprovementSuggestions;
292