/** * Content Analysis Utilities * * Utility functions for SEO content analysis, migrated from jQuery implementation * to maintain consistency with original functionality * * @package ThinkRank * @since 1.0.0 */ /** * Calculate real word count from text * Industry standard word counting for SEO analysis * * @param {string} text - Text content to analyze * @returns {number} Word count */ export const calculateWordCount = (text) => { if (!text || typeof text !== 'string') return 0; // Remove extra whitespace and split by spaces const words = text.trim().split(/\s+/).filter(word => word.length > 0); return words.length; }; /** * Calculate keyword density using industry standard methodology * * @param {string} content - Content to analyze * @param {string} keyword - Target keyword * @returns {number} Keyword density percentage (rounded to 1 decimal) */ export const calculateKeywordDensity = (content, keyword) => { if (!content || !keyword) return 0; // Convert to lowercase for case-insensitive matching const lowerContent = content.toLowerCase(); const lowerKeyword = keyword.toLowerCase(); // Count total words const totalWords = calculateWordCount(content); if (totalWords === 0) return 0; // Count keyword occurrences (including partial matches) const keywordOccurrences = (lowerContent.match(new RegExp(lowerKeyword, 'g')) || []).length; // Calculate density as percentage const density = (keywordOccurrences / totalWords) * 100; return Math.round(density * 10) / 10; // Round to 1 decimal place }; /** * Estimate syllable count for readability calculations * Simple approximation used in Flesch Reading Ease formula * * @param {string} text - Text to analyze * @returns {number} Estimated syllable count */ export const estimateSyllables = (text) => { const words = text.toLowerCase().match(/\b[a-z]+\b/g) || []; let syllableCount = 0; words.forEach(word => { // Simple syllable counting heuristic const vowels = word.match(/[aeiouy]+/g) || []; let syllables = vowels.length; // Adjust for silent 'e' if (word.endsWith('e') && syllables > 1) { syllables--; } // Minimum 1 syllable per word syllableCount += Math.max(1, syllables); }); return syllableCount; }; /** * Calculate readability score using industry standard Flesch Reading Ease * Official formula used by academic institutions and professional tools * * @param {string} text - Text content to analyze * @returns {string} Readability assessment with score */ export const calculateReadabilityScore = (text) => { if (!text) return 'No content'; const words = calculateWordCount(text); const sentences = (text.match(/[.!?]+/g) || []).length; const syllables = estimateSyllables(text); if (sentences === 0 || words === 0) return 'Too short'; const avgWordsPerSentence = words / sentences; const avgSyllablesPerWord = syllables / words; // Official Flesch Reading Ease formula const fleschScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); const roundedScore = Math.round(fleschScore); // Industry standard Flesch Reading Ease scale let level = ''; if (roundedScore >= 90) level = 'Very Easy'; else if (roundedScore >= 80) level = 'Easy'; else if (roundedScore >= 70) level = 'Fairly Easy'; else if (roundedScore >= 60) level = 'Standard'; else if (roundedScore >= 50) level = 'Fairly Difficult'; else if (roundedScore >= 30) level = 'Difficult'; else level = 'Very Difficult'; // Return professional format return `${level} (${roundedScore})`; }; /** * Analyze heading structure from HTML content * * @param {string} htmlContent - HTML content to analyze * @returns {Object} Heading analysis with count and structure description */ export const analyzeHeadingStructure = (htmlContent) => { if (!htmlContent) return { count: 0, structure: 'No content' }; // Create a temporary div to parse HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // Count different heading levels const h1Count = tempDiv.querySelectorAll('h1').length; const h2Count = tempDiv.querySelectorAll('h2').length; const h3Count = tempDiv.querySelectorAll('h3').length; const h4Count = tempDiv.querySelectorAll('h4').length; const h5Count = tempDiv.querySelectorAll('h5').length; const h6Count = tempDiv.querySelectorAll('h6').length; const totalHeadings = h1Count + h2Count + h3Count + h4Count + h5Count + h6Count; let structure = ''; if (totalHeadings === 0) { structure = 'No headings'; } else if (h1Count === 0) { structure = `${totalHeadings} headings (No H1)`; } else if (h1Count > 1) { structure = `${totalHeadings} headings (${h1Count} H1s)`; } else { structure = `${totalHeadings} headings (Good H1)`; } return { count: totalHeadings, structure: structure }; }; /** * Calculate content quality using industry standard SEO methodology * Based on established content analysis best practices * * @param {string} plainText - Plain text content * @param {string} htmlContent - HTML content for structure analysis * @returns {string} Content quality assessment */ export const calculateContentQuality = (plainText, htmlContent) => { if (!plainText) return 'No content'; const wordCount = calculateWordCount(plainText); let issues = []; let score = 0; // 1. Word count analysis (Industry standard: 300+ words) if (wordCount < 300) { issues.push('Too short'); score += 0; } else if (wordCount >= 300 && wordCount < 600) { score += 50; } else { score += 100; // 600+ words is excellent } // 2. Paragraph length analysis (SEO best practice: avoid long paragraphs) const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const paragraphs = tempDiv.querySelectorAll('p'); let longParagraphs = 0; paragraphs.forEach(p => { const pText = p.textContent || p.innerText || ''; const pWordCount = calculateWordCount(pText); if (pWordCount > 150) { // Industry standard: flag paragraphs over 150 words longParagraphs++; } }); if (longParagraphs === 0) { score += 100; } else if (longParagraphs <= 2) { score += 50; issues.push('Some long paragraphs'); } else { score += 0; issues.push('Too many long paragraphs'); } // 3. Subheading distribution (SEO best practice: subheadings every 300 words) const headings = tempDiv.querySelectorAll('h2, h3, h4, h5, h6').length; const expectedHeadings = Math.floor(wordCount / 300); if (headings >= expectedHeadings && headings > 0) { score += 100; } else if (headings > 0) { score += 50; issues.push('Could use more subheadings'); } else { score += 0; issues.push('No subheadings'); } // Calculate final score (0-100) const finalScore = Math.round(score / 3); // Average of 3 criteria // Return professional assessment if (finalScore >= 80) return 'Good'; else if (finalScore >= 50) return 'OK'; else return 'Needs improvement'; }; /** * Calculate keyword optimization using industry standard SEO methodology * Based on established SEO best practices * * @param {string} plainText - Plain text content * @param {string} htmlContent - HTML content for structure analysis * @param {string} title - Post title * @param {string} targetKeyword - Focus keyword * @param {string} metaDescription - Meta description * @returns {string} Keyword optimization assessment */ export const calculateKeywordOptimization = (plainText, htmlContent, title, targetKeyword, metaDescription = '') => { if (!targetKeyword || !targetKeyword.trim()) { return 'No focus keyword'; } if (!plainText) return 'No content'; const keywordLower = targetKeyword.toLowerCase(); const contentLower = plainText.toLowerCase(); const titleLower = title.toLowerCase(); let checks = 0; let totalChecks = 0; // 1. Keyword in title (Essential SEO check) totalChecks++; if (titleLower.includes(keywordLower)) { checks++; } // 2. Keyword density (Industry standard: 0.5% - 2.5%) totalChecks++; const density = calculateKeywordDensity(plainText, targetKeyword); if (density >= 0.5 && density <= 2.5) { checks++; } // 3. Keyword in first paragraph (SEO best practice) totalChecks++; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const firstParagraph = tempDiv.querySelector('p'); if (firstParagraph) { const firstParagraphText = (firstParagraph.textContent || firstParagraph.innerText || '').toLowerCase(); if (firstParagraphText.includes(keywordLower)) { checks++; } } // 4. Keyword in subheadings (Content structure optimization) totalChecks++; const subheadings = tempDiv.querySelectorAll('h2, h3, h4, h5, h6'); let keywordInSubheading = false; subheadings.forEach(heading => { const headingText = (heading.textContent || heading.innerText || '').toLowerCase(); if (headingText.includes(keywordLower)) { keywordInSubheading = true; } }); if (keywordInSubheading) { checks++; } // 5. Keyword in meta description (Meta optimization) if (metaDescription) { totalChecks++; if (metaDescription.toLowerCase().includes(keywordLower)) { checks++; } } // Calculate score based on checks passed const percentage = Math.round((checks / totalChecks) * 100); // Return professional assessment if (percentage >= 80) return 'Good'; else if (percentage >= 60) return 'OK'; else return 'Needs improvement'; };