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 / utils / contentAnalysis.js

contentAnalysis.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/utils/contentAnalysis.js

306 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Content Analysis Utilities
3 *
4 * Utility functions for SEO content analysis, migrated from jQuery implementation
5 * to maintain consistency with original functionality
6 *
7 * @package ThinkRank
8 * @since 1.0.0
9 */
10
11 /**
12 * Calculate real word count from text
13 * Industry standard word counting for SEO analysis
14 *
15 * @param {string} text - Text content to analyze
16 * @returns {number} Word count
17 */
18 export const calculateWordCount = (text) => {
19 if (!text || typeof text !== 'string') return 0;
20
21 // Remove extra whitespace and split by spaces
22 const words = text.trim().split(/\s+/).filter(word => word.length > 0);
23 return words.length;
24 };
25
26 /**
27 * Calculate keyword density using industry standard methodology
28 *
29 * @param {string} content - Content to analyze
30 * @param {string} keyword - Target keyword
31 * @returns {number} Keyword density percentage (rounded to 1 decimal)
32 */
33 export const calculateKeywordDensity = (content, keyword) => {
34 if (!content || !keyword) return 0;
35
36 // Convert to lowercase for case-insensitive matching
37 const lowerContent = content.toLowerCase();
38 const lowerKeyword = keyword.toLowerCase();
39
40 // Count total words
41 const totalWords = calculateWordCount(content);
42 if (totalWords === 0) return 0;
43
44 // Count keyword occurrences (including partial matches)
45 const keywordOccurrences = (lowerContent.match(new RegExp(lowerKeyword, 'g')) || []).length;
46
47 // Calculate density as percentage
48 const density = (keywordOccurrences / totalWords) * 100;
49 return Math.round(density * 10) / 10; // Round to 1 decimal place
50 };
51
52 /**
53 * Estimate syllable count for readability calculations
54 * Simple approximation used in Flesch Reading Ease formula
55 *
56 * @param {string} text - Text to analyze
57 * @returns {number} Estimated syllable count
58 */
59 export const estimateSyllables = (text) => {
60 const words = text.toLowerCase().match(/\b[a-z]+\b/g) || [];
61 let syllableCount = 0;
62
63 words.forEach(word => {
64 // Simple syllable counting heuristic
65 const vowels = word.match(/[aeiouy]+/g) || [];
66 let syllables = vowels.length;
67
68 // Adjust for silent 'e'
69 if (word.endsWith('e') && syllables > 1) {
70 syllables--;
71 }
72
73 // Minimum 1 syllable per word
74 syllableCount += Math.max(1, syllables);
75 });
76
77 return syllableCount;
78 };
79
80 /**
81 * Calculate readability score using industry standard Flesch Reading Ease
82 * Official formula used by academic institutions and professional tools
83 *
84 * @param {string} text - Text content to analyze
85 * @returns {string} Readability assessment with score
86 */
87 export const calculateReadabilityScore = (text) => {
88 if (!text) return 'No content';
89
90 const words = calculateWordCount(text);
91 const sentences = (text.match(/[.!?]+/g) || []).length;
92 const syllables = estimateSyllables(text);
93
94 if (sentences === 0 || words === 0) return 'Too short';
95
96 const avgWordsPerSentence = words / sentences;
97 const avgSyllablesPerWord = syllables / words;
98
99 // Official Flesch Reading Ease formula
100 const fleschScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
101 const roundedScore = Math.round(fleschScore);
102
103 // Industry standard Flesch Reading Ease scale
104 let level = '';
105 if (roundedScore >= 90) level = 'Very Easy';
106 else if (roundedScore >= 80) level = 'Easy';
107 else if (roundedScore >= 70) level = 'Fairly Easy';
108 else if (roundedScore >= 60) level = 'Standard';
109 else if (roundedScore >= 50) level = 'Fairly Difficult';
110 else if (roundedScore >= 30) level = 'Difficult';
111 else level = 'Very Difficult';
112
113 // Return professional format
114 return `${level} (${roundedScore})`;
115 };
116
117 /**
118 * Analyze heading structure from HTML content
119 *
120 * @param {string} htmlContent - HTML content to analyze
121 * @returns {Object} Heading analysis with count and structure description
122 */
123 export const analyzeHeadingStructure = (htmlContent) => {
124 if (!htmlContent) return { count: 0, structure: 'No content' };
125
126 // Create a temporary div to parse HTML
127 const tempDiv = document.createElement('div');
128 tempDiv.innerHTML = htmlContent;
129
130 // Count different heading levels
131 const h1Count = tempDiv.querySelectorAll('h1').length;
132 const h2Count = tempDiv.querySelectorAll('h2').length;
133 const h3Count = tempDiv.querySelectorAll('h3').length;
134 const h4Count = tempDiv.querySelectorAll('h4').length;
135 const h5Count = tempDiv.querySelectorAll('h5').length;
136 const h6Count = tempDiv.querySelectorAll('h6').length;
137
138 const totalHeadings = h1Count + h2Count + h3Count + h4Count + h5Count + h6Count;
139
140 let structure = '';
141 if (totalHeadings === 0) {
142 structure = 'No headings';
143 } else if (h1Count === 0) {
144 structure = `${totalHeadings} headings (No H1)`;
145 } else if (h1Count > 1) {
146 structure = `${totalHeadings} headings (${h1Count} H1s)`;
147 } else {
148 structure = `${totalHeadings} headings (Good H1)`;
149 }
150
151 return { count: totalHeadings, structure: structure };
152 };
153
154 /**
155 * Calculate content quality using industry standard SEO methodology
156 * Based on established content analysis best practices
157 *
158 * @param {string} plainText - Plain text content
159 * @param {string} htmlContent - HTML content for structure analysis
160 * @returns {string} Content quality assessment
161 */
162 export const calculateContentQuality = (plainText, htmlContent) => {
163 if (!plainText) return 'No content';
164
165 const wordCount = calculateWordCount(plainText);
166 let issues = [];
167 let score = 0;
168
169 // 1. Word count analysis (Industry standard: 300+ words)
170 if (wordCount < 300) {
171 issues.push('Too short');
172 score += 0;
173 } else if (wordCount >= 300 && wordCount < 600) {
174 score += 50;
175 } else {
176 score += 100; // 600+ words is excellent
177 }
178
179 // 2. Paragraph length analysis (SEO best practice: avoid long paragraphs)
180 const tempDiv = document.createElement('div');
181 tempDiv.innerHTML = htmlContent;
182 const paragraphs = tempDiv.querySelectorAll('p');
183 let longParagraphs = 0;
184
185 paragraphs.forEach(p => {
186 const pText = p.textContent || p.innerText || '';
187 const pWordCount = calculateWordCount(pText);
188 if (pWordCount > 150) { // Industry standard: flag paragraphs over 150 words
189 longParagraphs++;
190 }
191 });
192
193 if (longParagraphs === 0) {
194 score += 100;
195 } else if (longParagraphs <= 2) {
196 score += 50;
197 issues.push('Some long paragraphs');
198 } else {
199 score += 0;
200 issues.push('Too many long paragraphs');
201 }
202
203 // 3. Subheading distribution (SEO best practice: subheadings every 300 words)
204 const headings = tempDiv.querySelectorAll('h2, h3, h4, h5, h6').length;
205 const expectedHeadings = Math.floor(wordCount / 300);
206
207 if (headings >= expectedHeadings && headings > 0) {
208 score += 100;
209 } else if (headings > 0) {
210 score += 50;
211 issues.push('Could use more subheadings');
212 } else {
213 score += 0;
214 issues.push('No subheadings');
215 }
216
217 // Calculate final score (0-100)
218 const finalScore = Math.round(score / 3); // Average of 3 criteria
219
220 // Return professional assessment
221 if (finalScore >= 80) return 'Good';
222 else if (finalScore >= 50) return 'OK';
223 else return 'Needs improvement';
224 };
225
226 /**
227 * Calculate keyword optimization using industry standard SEO methodology
228 * Based on established SEO best practices
229 *
230 * @param {string} plainText - Plain text content
231 * @param {string} htmlContent - HTML content for structure analysis
232 * @param {string} title - Post title
233 * @param {string} targetKeyword - Focus keyword
234 * @param {string} metaDescription - Meta description
235 * @returns {string} Keyword optimization assessment
236 */
237 export const calculateKeywordOptimization = (plainText, htmlContent, title, targetKeyword, metaDescription = '') => {
238 if (!targetKeyword || !targetKeyword.trim()) {
239 return 'No focus keyword';
240 }
241
242 if (!plainText) return 'No content';
243
244 const keywordLower = targetKeyword.toLowerCase();
245 const contentLower = plainText.toLowerCase();
246 const titleLower = title.toLowerCase();
247
248 let checks = 0;
249 let totalChecks = 0;
250
251 // 1. Keyword in title (Essential SEO check)
252 totalChecks++;
253 if (titleLower.includes(keywordLower)) {
254 checks++;
255 }
256
257 // 2. Keyword density (Industry standard: 0.5% - 2.5%)
258 totalChecks++;
259 const density = calculateKeywordDensity(plainText, targetKeyword);
260 if (density >= 0.5 && density <= 2.5) {
261 checks++;
262 }
263
264 // 3. Keyword in first paragraph (SEO best practice)
265 totalChecks++;
266 const tempDiv = document.createElement('div');
267 tempDiv.innerHTML = htmlContent;
268 const firstParagraph = tempDiv.querySelector('p');
269 if (firstParagraph) {
270 const firstParagraphText = (firstParagraph.textContent || firstParagraph.innerText || '').toLowerCase();
271 if (firstParagraphText.includes(keywordLower)) {
272 checks++;
273 }
274 }
275
276 // 4. Keyword in subheadings (Content structure optimization)
277 totalChecks++;
278 const subheadings = tempDiv.querySelectorAll('h2, h3, h4, h5, h6');
279 let keywordInSubheading = false;
280 subheadings.forEach(heading => {
281 const headingText = (heading.textContent || heading.innerText || '').toLowerCase();
282 if (headingText.includes(keywordLower)) {
283 keywordInSubheading = true;
284 }
285 });
286 if (keywordInSubheading) {
287 checks++;
288 }
289
290 // 5. Keyword in meta description (Meta optimization)
291 if (metaDescription) {
292 totalChecks++;
293 if (metaDescription.toLowerCase().includes(keywordLower)) {
294 checks++;
295 }
296 }
297
298 // Calculate score based on checks passed
299 const percentage = Math.round((checks / totalChecks) * 100);
300
301 // Return professional assessment
302 if (percentage >= 80) return 'Good';
303 else if (percentage >= 60) return 'OK';
304 else return 'Needs improvement';
305 };
306