UNPKG

vibe-code-build

Version:

Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat

1,215 lines (1,075 loc) • 46.2 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import ora from 'ora'; export class SEOChecker { constructor(projectPath = process.cwd(), options = {}) { this.projectPath = projectPath; this.options = options; this.results = { technical: {}, content: {}, social: {}, performance: {}, overall: {} }; } async checkAll() { const spinner = this.options.silent ? null : ora('Running comprehensive SEO analysis...').start(); try { if (spinner) spinner.text = 'Checking technical SEO...'; this.results.technical = await this.checkTechnicalSEO(); if (spinner) spinner.text = 'Analyzing content SEO...'; this.results.content = await this.checkContentSEO(); if (spinner) spinner.text = 'Validating social media SEO...'; this.results.social = await this.checkSocialSEO(); if (spinner) spinner.text = 'Measuring SEO performance factors...'; this.results.performance = await this.checkPerformanceSEO(); if (spinner) spinner.text = 'Calculating SEO score...'; this.results.overall = this.calculateOverallScore(); if (spinner) spinner.succeed('SEO analysis completed'); return this.results; } catch (error) { if (spinner) spinner.fail('SEO analysis failed'); throw error; } } async checkTechnicalSEO() { const issues = []; const checks = { robots: { found: false, valid: false }, sitemap: { found: false, valid: false }, canonical: { found: false, issues: [] }, structuredData: { found: false, valid: false }, https: { enabled: false }, mobileConfig: { found: false } }; // Check robots.txt try { const robotsPath = path.join(this.projectPath, 'public', 'robots.txt'); const robotsContent = await fs.readFile(robotsPath, 'utf8'); checks.robots.found = true; if (robotsContent.includes('User-agent:')) { checks.robots.valid = true; } else { issues.push({ type: 'invalid-robots', severity: 'medium', message: 'robots.txt exists but appears to be invalid', explanation: 'A valid robots.txt helps search engines understand which parts of your site to crawl', recommendation: 'Add proper User-agent and crawling directives', example: `User-agent: *\nAllow: /\nSitemap: https://yoursite.com/sitemap.xml` }); } if (!robotsContent.includes('Sitemap:')) { issues.push({ type: 'missing-sitemap-ref', severity: 'low', message: 'robots.txt missing sitemap reference', explanation: 'Including your sitemap URL in robots.txt helps search engines find it faster', recommendation: 'Add Sitemap: directive to robots.txt', example: 'Sitemap: https://yoursite.com/sitemap.xml' }); } } catch { issues.push({ type: 'missing-robots', severity: 'high', message: 'No robots.txt file found', explanation: 'robots.txt controls how search engines crawl your site. Without it, you have no control over crawling behavior', recommendation: 'Create a robots.txt file in your public directory', impact: 'Search engines may crawl pages you don\'t want indexed', example: `User-agent: *\nAllow: /\n\n# Block admin pages\nDisallow: /admin/\nDisallow: /api/\n\nSitemap: https://yoursite.com/sitemap.xml` }); } // Check sitemap.xml try { const sitemapPath = path.join(this.projectPath, 'public', 'sitemap.xml'); const sitemapContent = await fs.readFile(sitemapPath, 'utf8'); checks.sitemap.found = true; if (sitemapContent.includes('<urlset') && sitemapContent.includes('<url>')) { checks.sitemap.valid = true; // Check for important sitemap elements if (!sitemapContent.includes('<lastmod>')) { issues.push({ type: 'sitemap-no-lastmod', severity: 'low', message: 'Sitemap missing lastmod dates', explanation: 'Last modification dates help search engines prioritize crawling updated content', recommendation: 'Add <lastmod> tags to your sitemap entries', example: '<lastmod>2024-01-20T09:00:00+00:00</lastmod>' }); } if (!sitemapContent.includes('<priority>')) { issues.push({ type: 'sitemap-no-priority', severity: 'low', message: 'Sitemap missing priority values', explanation: 'Priority helps search engines understand the relative importance of your pages', recommendation: 'Add <priority> tags (0.0-1.0) to important pages', example: '<priority>0.8</priority>' }); } } else { checks.sitemap.valid = false; issues.push({ type: 'invalid-sitemap', severity: 'high', message: 'sitemap.xml exists but appears to be invalid', explanation: 'An invalid sitemap prevents search engines from discovering all your pages', recommendation: 'Ensure sitemap follows XML sitemap protocol', example: `<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://yoursite.com/</loc> <lastmod>2024-01-20</lastmod> <priority>1.0</priority> </url> </urlset>` }); } } catch { issues.push({ type: 'missing-sitemap', severity: 'high', message: 'No sitemap.xml file found', explanation: 'Sitemaps help search engines discover and index all your important pages efficiently', recommendation: 'Generate a sitemap.xml file listing all public pages', impact: 'Search engines may miss important pages, leading to incomplete indexing', tools: ['next-sitemap', 'sitemap.js', 'gatsby-plugin-sitemap'] }); } // Check for structured data const htmlFiles = await this.findFiles(['.html', '.jsx', '.tsx']); let structuredDataFound = false; for (const file of htmlFiles.slice(0, 10)) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes('application/ld+json') || content.includes('itemscope') || content.includes('itemtype')) { structuredDataFound = true; checks.structuredData.found = true; break; } } catch {} } if (!structuredDataFound) { issues.push({ type: 'missing-structured-data', severity: 'medium', message: 'No structured data (Schema.org) found', explanation: 'Structured data helps search engines understand your content and can enable rich snippets in search results', recommendation: 'Add JSON-LD structured data to your pages', impact: 'Missing out on rich snippets, knowledge graph inclusion, and better SERP visibility', example: `<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "WebSite", "name": "Your Site Name", "url": "https://yoursite.com", "potentialAction": { "@type": "SearchAction", "target": "https://yoursite.com/search?q={search_term_string}", "query-input": "required name=search_term_string" } } </script>` }); } // Check for SSL/HTTPS configuration try { const packageJson = JSON.parse(await fs.readFile(path.join(this.projectPath, 'package.json'), 'utf8')); if (packageJson.homepage && packageJson.homepage.startsWith('https://')) { checks.https.enabled = true; } else if (packageJson.homepage && packageJson.homepage.startsWith('http://')) { issues.push({ type: 'no-https', severity: 'high', message: 'Site not using HTTPS', explanation: 'HTTPS is a ranking factor and required for many modern web features', recommendation: 'Enable SSL/TLS certificate for your domain', impact: 'Lower search rankings, security warnings in browsers, reduced user trust' }); } } catch {} // Check for mobile configuration for (const file of htmlFiles.slice(0, 10)) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes('viewport') && content.includes('width=device-width')) { checks.mobileConfig.found = true; break; } } catch {} } if (!checks.mobileConfig.found) { issues.push({ type: 'missing-viewport', severity: 'high', message: 'No mobile viewport meta tag found', explanation: 'Viewport meta tag is essential for mobile responsiveness, a key ranking factor', recommendation: 'Add viewport meta tag to all HTML pages', impact: 'Poor mobile experience, lower mobile search rankings', example: '<meta name="viewport" content="width=device-width, initial-scale=1.0">' }); } return { status: issues.filter(i => i.severity === 'high').length > 2 ? 'failed' : issues.length > 5 ? 'warning' : 'passed', message: `Found ${issues.length} technical SEO issues`, score: this.calculateTechnicalScore(checks, issues), checks, issues, summary: { critical: issues.filter(i => i.severity === 'critical').length, high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length } }; } async checkContentSEO() { const issues = []; const htmlFiles = await this.findFiles(['.html', '.jsx', '.tsx', '.vue', '.svelte']); const contentStats = { pagesAnalyzed: 0, titlesFound: 0, descriptionsFound: 0, h1Found: 0, imagesWithAlt: 0, totalImages: 0, headingHierarchy: { correct: 0, incorrect: 0 } }; for (const file of htmlFiles.slice(0, 30)) { try { const content = await fs.readFile(file, 'utf8'); const relativePath = path.relative(this.projectPath, file); contentStats.pagesAnalyzed++; // Check title tag const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i) || content.match(/title\s*[:=]\s*['"`]([^'"`]+)['"`]/i); if (titleMatch) { contentStats.titlesFound++; const title = titleMatch[1]; if (title.length < 30) { issues.push({ type: 'short-title', severity: 'medium', file: relativePath, message: `Title too short (${title.length} chars): "${title}"`, explanation: 'Short titles don\'t utilize the full ~60 character limit in SERPs', recommendation: 'Expand title to 50-60 characters with relevant keywords', example: 'Original: "Home" → Better: "YourBrand - Professional Web Development Services"' }); } else if (title.length > 60) { issues.push({ type: 'long-title', severity: 'medium', file: relativePath, message: `Title too long (${title.length} chars): "${title}"`, explanation: 'Titles over 60 characters get truncated in search results', recommendation: 'Shorten title to under 60 characters while keeping main keywords', impact: 'Important keywords might be cut off in search results' }); } if (title.toLowerCase() === 'untitled' || title.toLowerCase() === 'react app') { issues.push({ type: 'generic-title', severity: 'high', file: relativePath, message: `Generic title found: "${title}"`, explanation: 'Generic titles provide no value for SEO or user understanding', recommendation: 'Use descriptive, keyword-rich titles unique to each page', impact: 'Poor click-through rates, missed ranking opportunities' }); } } else { issues.push({ type: 'missing-title', severity: 'critical', file: relativePath, message: 'No title tag found', explanation: 'Title tags are the most important on-page SEO element', recommendation: 'Add a unique, descriptive title tag to every page', impact: 'Page may not appear properly in search results', example: '<title>Your Page Title - Brand Name</title>' }); } // Check meta description const descMatch = content.match(/meta\s+name=["']description["']\s+content=["']([^"']+)["']/i) || content.match(/description\s*[:=]\s*['"`]([^'"`]+)['"`]/i); if (descMatch) { contentStats.descriptionsFound++; const description = descMatch[1]; if (description.length < 120) { issues.push({ type: 'short-description', severity: 'low', file: relativePath, message: `Description too short (${description.length} chars)`, explanation: 'Short descriptions don\'t fully utilize the ~160 character limit', recommendation: 'Expand to 150-160 characters with compelling copy and keywords', example: 'Include a call-to-action and unique value proposition' }); } else if (description.length > 160) { issues.push({ type: 'long-description', severity: 'low', file: relativePath, message: `Description too long (${description.length} chars)`, explanation: 'Descriptions over 160 characters get truncated in search results', recommendation: 'Shorten to under 160 characters while maintaining clarity' }); } } else { issues.push({ type: 'missing-description', severity: 'high', file: relativePath, message: 'No meta description found', explanation: 'Meta descriptions influence click-through rates from search results', recommendation: 'Add compelling meta descriptions to improve CTR', impact: 'Search engines will auto-generate snippets, often poorly', example: '<meta name="description" content="Your compelling 150-160 character description with keywords and call-to-action.">' }); } // Check heading structure const h1Matches = content.match(/<h1[^>]*>/gi) || []; const h2Matches = content.match(/<h2[^>]*>/gi) || []; const h3Matches = content.match(/<h3[^>]*>/gi) || []; if (h1Matches.length > 0) { contentStats.h1Found++; if (h1Matches.length > 1) { issues.push({ type: 'multiple-h1', severity: 'medium', file: relativePath, message: `Multiple H1 tags found (${h1Matches.length})`, explanation: 'Each page should have exactly one H1 tag for clear content hierarchy', recommendation: 'Use only one H1 per page, use H2-H6 for subsections', impact: 'Confuses search engines about the main topic of the page' }); } } else if (relativePath.includes('index') || relativePath.includes('home')) { issues.push({ type: 'missing-h1', severity: 'high', file: relativePath, message: 'No H1 tag found', explanation: 'H1 tags signal the main topic of your page to search engines', recommendation: 'Add a descriptive H1 tag that includes target keywords', example: '<h1>Your Main Page Heading with Primary Keywords</h1>' }); } // Check for proper heading hierarchy if (h3Matches.length > 0 && h2Matches.length === 0) { contentStats.headingHierarchy.incorrect++; issues.push({ type: 'broken-heading-hierarchy', severity: 'low', file: relativePath, message: 'H3 found without H2 - broken heading hierarchy', explanation: 'Proper heading hierarchy (H1→H2→H3) helps search engines understand content structure', recommendation: 'Use headings in sequential order without skipping levels' }); } else if (h2Matches.length > 0) { contentStats.headingHierarchy.correct++; } // Check images for alt text const imgMatches = content.match(/<img[^>]+>/gi) || []; contentStats.totalImages += imgMatches.length; for (const img of imgMatches) { if (img.includes('alt=')) { contentStats.imagesWithAlt++; // Check for empty alt text if (img.match(/alt=["'][\s]*["']/)) { issues.push({ type: 'empty-alt-text', severity: 'medium', file: relativePath, message: 'Image with empty alt text found', explanation: 'Empty alt text provides no value for accessibility or SEO', recommendation: 'Add descriptive alt text or use alt="" only for decorative images' }); } } else { issues.push({ type: 'missing-alt-text', severity: 'medium', file: relativePath, message: 'Image missing alt text', explanation: 'Alt text improves accessibility and helps search engines understand images', recommendation: 'Add descriptive alt text to all informational images', example: '<img src="product.jpg" alt="Blue ceramic coffee mug with company logo">' }); } } // Check for keyword stuffing indicators const textContent = content.replace(/<[^>]+>/g, ' ').toLowerCase(); const words = textContent.split(/\s+/); const wordFrequency = {}; words.forEach(word => { if (word.length > 3) { wordFrequency[word] = (wordFrequency[word] || 0) + 1; } }); const suspiciousWords = Object.entries(wordFrequency) .filter(([word, count]) => count > 20 && count / words.length > 0.03) .sort((a, b) => b[1] - a[1]); if (suspiciousWords.length > 0) { issues.push({ type: 'keyword-stuffing', severity: 'medium', file: relativePath, message: `Possible keyword stuffing: "${suspiciousWords[0][0]}" appears ${suspiciousWords[0][1]} times`, explanation: 'Overusing keywords can result in search engine penalties', recommendation: 'Use keywords naturally, focus on readability and user value', impact: 'Risk of algorithmic penalties, poor user experience' }); } } catch (error) { // Continue with next file } } const score = this.calculateContentScore(contentStats, issues); return { status: issues.filter(i => i.severity === 'critical' || i.severity === 'high').length > 5 ? 'failed' : issues.length > 10 ? 'warning' : 'passed', message: `Analyzed ${contentStats.pagesAnalyzed} pages, found ${issues.length} content SEO issues`, score, stats: contentStats, issues: issues.slice(0, 20), summary: { critical: issues.filter(i => i.severity === 'critical').length, high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length }, recommendations: this.getContentRecommendations(contentStats, issues) }; } async checkSocialSEO() { const issues = []; const htmlFiles = await this.findFiles(['.html', '.jsx', '.tsx']); const socialStats = { ogTags: { found: 0, complete: 0 }, twitterCards: { found: 0, complete: 0 }, pagesChecked: 0 }; for (const file of htmlFiles.slice(0, 20)) { try { const content = await fs.readFile(file, 'utf8'); const relativePath = path.relative(this.projectPath, file); socialStats.pagesChecked++; // Check Open Graph tags const ogTitle = content.includes('og:title'); const ogDescription = content.includes('og:description'); const ogImage = content.includes('og:image'); const ogUrl = content.includes('og:url'); const ogType = content.includes('og:type'); if (ogTitle || ogDescription || ogImage) { socialStats.ogTags.found++; if (ogTitle && ogDescription && ogImage && ogUrl) { socialStats.ogTags.complete++; } else { const missing = []; if (!ogTitle) missing.push('og:title'); if (!ogDescription) missing.push('og:description'); if (!ogImage) missing.push('og:image'); if (!ogUrl) missing.push('og:url'); issues.push({ type: 'incomplete-og-tags', severity: 'medium', file: relativePath, message: `Missing Open Graph tags: ${missing.join(', ')}`, explanation: 'Complete OG tags ensure your content looks great when shared on social media', recommendation: 'Add all essential OG tags for better social sharing', example: `<meta property="og:title" content="Your Page Title"> <meta property="og:description" content="Page description"> <meta property="og:image" content="https://yoursite.com/image.jpg"> <meta property="og:url" content="https://yoursite.com/page">` }); } if (ogImage && !content.includes('og:image:width')) { issues.push({ type: 'missing-og-image-dimensions', severity: 'low', file: relativePath, message: 'OG image dimensions not specified', explanation: 'Specifying image dimensions helps social platforms render previews faster', recommendation: 'Add og:image:width and og:image:height tags', example: `<meta property="og:image:width" content="1200"> <meta property="og:image:height" content="630">` }); } } else if (relativePath.includes('index') || relativePath.includes('home')) { issues.push({ type: 'missing-og-tags', severity: 'high', file: relativePath, message: 'No Open Graph tags found', explanation: 'OG tags control how your pages appear when shared on Facebook, LinkedIn, and other platforms', recommendation: 'Add Open Graph meta tags to all important pages', impact: 'Poor social media presence, missed viral potential' }); } // Check Twitter Card tags const twitterCard = content.includes('twitter:card'); const twitterTitle = content.includes('twitter:title'); const twitterDescription = content.includes('twitter:description'); const twitterImage = content.includes('twitter:image'); if (twitterCard || twitterTitle) { socialStats.twitterCards.found++; if (twitterCard && twitterTitle && twitterDescription && twitterImage) { socialStats.twitterCards.complete++; } else { const missing = []; if (!twitterCard) missing.push('twitter:card'); if (!twitterTitle) missing.push('twitter:title'); if (!twitterDescription) missing.push('twitter:description'); if (!twitterImage) missing.push('twitter:image'); issues.push({ type: 'incomplete-twitter-cards', severity: 'low', file: relativePath, message: `Missing Twitter Card tags: ${missing.join(', ')}`, explanation: 'Twitter Cards enhance how your content appears on Twitter/X', recommendation: 'Add complete Twitter Card metadata', example: `<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="Your Title"> <meta name="twitter:description" content="Your Description"> <meta name="twitter:image" content="https://yoursite.com/twitter-image.jpg">` }); } } // Check for social sharing optimization if (!content.includes('article:author') && (content.includes('article') || content.includes('blog'))) { issues.push({ type: 'missing-article-metadata', severity: 'low', file: relativePath, message: 'Article metadata missing for blog/article content', explanation: 'Article metadata improves how blog posts appear in social feeds', recommendation: 'Add article:author, article:published_time for blog posts', example: `<meta property="article:author" content="Author Name"> <meta property="article:published_time" content="2024-01-20T08:00:00+00:00">` }); } } catch {} } const score = this.calculateSocialScore(socialStats, issues); return { status: socialStats.ogTags.found === 0 ? 'failed' : socialStats.ogTags.complete < socialStats.pagesChecked / 2 ? 'warning' : 'passed', message: `Checked ${socialStats.pagesChecked} pages for social SEO`, score, stats: socialStats, issues, summary: { high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length }, recommendations: [ 'Implement Open Graph tags on all public-facing pages', 'Add Twitter Card tags for better Twitter/X visibility', 'Use 1200x630px images for optimal social sharing', 'Test social previews with Facebook Debugger and Twitter Card Validator' ] }; } async checkPerformanceSEO() { const issues = []; const performanceFactors = { largeImages: [], renderBlocking: [], noLazyLoad: 0, noCaching: 0, noCompression: 0 }; // Check for large images const imageFiles = await this.findFiles(['.jpg', '.jpeg', '.png', '.gif', '.webp']); for (const imagePath of imageFiles.slice(0, 50)) { try { const stats = await fs.stat(imagePath); const sizeInKB = stats.size / 1024; if (sizeInKB > 500) { performanceFactors.largeImages.push({ file: path.relative(this.projectPath, imagePath), size: sizeInKB }); issues.push({ type: 'large-image', severity: sizeInKB > 1000 ? 'high' : 'medium', file: path.relative(this.projectPath, imagePath), message: `Large image file: ${Math.round(sizeInKB)} KB`, explanation: 'Large images slow down page load, affecting SEO rankings', recommendation: 'Compress images and use modern formats like WebP', tools: ['imagemin', 'squoosh', 'sharp'], impact: 'Slower page loads, higher bounce rates, lower Core Web Vitals scores' }); } // Check if it's not WebP format if (!imagePath.endsWith('.webp') && sizeInKB > 100) { issues.push({ type: 'non-webp-image', severity: 'low', file: path.relative(this.projectPath, imagePath), message: 'Consider using WebP format', explanation: 'WebP provides 25-35% better compression than JPEG/PNG', recommendation: 'Convert images to WebP with fallbacks for older browsers', example: `<picture> <source srcset="image.webp" type="image/webp"> <img src="image.jpg" alt="Description"> </picture>` }); } } catch {} } // Check HTML files for performance issues const htmlFiles = await this.findFiles(['.html', '.jsx', '.tsx']); for (const file of htmlFiles.slice(0, 20)) { try { const content = await fs.readFile(file, 'utf8'); const relativePath = path.relative(this.projectPath, file); // Check for render-blocking resources const scriptTags = content.match(/<script[^>]*>/gi) || []; const renderBlockingScripts = scriptTags.filter(tag => !tag.includes('async') && !tag.includes('defer') && tag.includes('src=') ); if (renderBlockingScripts.length > 2) { performanceFactors.renderBlocking.push(relativePath); issues.push({ type: 'render-blocking-scripts', severity: 'medium', file: relativePath, message: `${renderBlockingScripts.length} render-blocking scripts found`, explanation: 'Render-blocking resources delay First Contentful Paint, affecting SEO', recommendation: 'Add async or defer attributes to non-critical scripts', example: '<script src="script.js" defer></script>', impact: 'Lower PageSpeed scores, poor Core Web Vitals' }); } // Check for lazy loading const imgTags = content.match(/<img[^>]*>/gi) || []; const lazyImages = imgTags.filter(tag => tag.includes('loading=')); if (imgTags.length > 5 && lazyImages.length === 0) { performanceFactors.noLazyLoad++; issues.push({ type: 'no-lazy-loading', severity: 'medium', file: relativePath, message: 'Images not using lazy loading', explanation: 'Lazy loading improves initial page load and Core Web Vitals', recommendation: 'Add loading="lazy" to below-the-fold images', example: '<img src="image.jpg" loading="lazy" alt="Description">', impact: 'Unnecessary bandwidth usage, slower initial page loads' }); } // Check for preconnect/prefetch if (!content.includes('rel="preconnect"') && (content.includes('fonts.googleapis.com') || content.includes('cdn.'))) { issues.push({ type: 'missing-preconnect', severity: 'low', file: relativePath, message: 'Missing preconnect for external resources', explanation: 'Preconnect hints speed up connections to third-party domains', recommendation: 'Add preconnect links for critical third-party resources', example: '<link rel="preconnect" href="https://fonts.googleapis.com">' }); } } catch {} } // Check for caching headers (in server config files) const configFiles = ['.htaccess', 'nginx.conf', 'vercel.json', 'netlify.toml']; let cachingConfigFound = false; for (const configFile of configFiles) { try { const configPath = path.join(this.projectPath, configFile); const content = await fs.readFile(configPath, 'utf8'); if (content.includes('cache') || content.includes('Cache-Control') || content.includes('max-age')) { cachingConfigFound = true; break; } } catch {} } if (!cachingConfigFound) { performanceFactors.noCaching = 1; issues.push({ type: 'no-caching-config', severity: 'medium', message: 'No caching configuration found', explanation: 'Proper caching reduces server load and improves repeat visit performance', recommendation: 'Configure Cache-Control headers for static assets', example: `# .htaccess example <IfModule mod_expires.c> ExpiresActive On ExpiresByType image/jpg "access plus 1 year" ExpiresByType image/jpeg "access plus 1 year" ExpiresByType image/png "access plus 1 year" ExpiresByType text/css "access plus 1 month" </IfModule>` }); } const score = this.calculatePerformanceScore(performanceFactors, issues); return { status: issues.filter(i => i.severity === 'high').length > 3 ? 'failed' : issues.length > 10 ? 'warning' : 'passed', message: `Found ${issues.length} performance-related SEO issues`, score, factors: performanceFactors, issues, summary: { high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length }, coreWebVitals: { explanation: 'Core Web Vitals are now a ranking factor', metrics: [ 'LCP (Largest Contentful Paint): Aim for < 2.5s', 'FID (First Input Delay): Aim for < 100ms', 'CLS (Cumulative Layout Shift): Aim for < 0.1' ], tools: ['PageSpeed Insights', 'Lighthouse', 'Web Vitals Extension'] } }; } calculateOverallScore() { const weights = { technical: 0.3, content: 0.35, social: 0.15, performance: 0.2 }; const technicalScore = this.results.technical?.score || 0; const contentScore = this.results.content?.score || 0; const socialScore = this.results.social?.score || 0; const performanceScore = this.results.performance?.score || 0; const overallScore = Math.round( technicalScore * weights.technical + contentScore * weights.content + socialScore * weights.social + performanceScore * weights.performance ); const totalIssues = (this.results.technical?.issues?.length || 0) + (this.results.content?.issues?.length || 0) + (this.results.social?.issues?.length || 0) + (this.results.performance?.issues?.length || 0); const criticalIssues = [ ...(this.results.technical?.issues || []), ...(this.results.content?.issues || []), ...(this.results.social?.issues || []), ...(this.results.performance?.issues || []) ].filter(i => i.severity === 'critical' || i.severity === 'high').length; return { score: overallScore, grade: this.getGrade(overallScore), status: overallScore >= 80 ? 'passed' : overallScore >= 60 ? 'warning' : 'failed', message: `SEO Score: ${overallScore}/100 (${this.getGrade(overallScore)})`, breakdown: { technical: { score: technicalScore, weight: weights.technical }, content: { score: contentScore, weight: weights.content }, social: { score: socialScore, weight: weights.social }, performance: { score: performanceScore, weight: weights.performance } }, summary: { totalIssues, criticalIssues, topPriorities: this.getTopPriorities() }, recommendations: this.getOverallRecommendations() }; } calculateTechnicalScore(checks, issues) { let score = 100; // Deduct points for missing critical elements if (!checks.robots.found) score -= 15; if (!checks.sitemap.found) score -= 15; if (!checks.https.enabled) score -= 20; if (!checks.mobileConfig.found) score -= 20; if (!checks.structuredData.found) score -= 10; // Deduct for issues based on severity issues.forEach(issue => { switch (issue.severity) { case 'critical': score -= 10; break; case 'high': score -= 5; break; case 'medium': score -= 2; break; case 'low': score -= 1; break; } }); return Math.max(0, Math.min(100, score)); } calculateContentScore(stats, issues) { let score = 100; // Calculate ratios const titleRatio = stats.pagesAnalyzed > 0 ? stats.titlesFound / stats.pagesAnalyzed : 0; const descriptionRatio = stats.pagesAnalyzed > 0 ? stats.descriptionsFound / stats.pagesAnalyzed : 0; const h1Ratio = stats.pagesAnalyzed > 0 ? stats.h1Found / stats.pagesAnalyzed : 0; const altTextRatio = stats.totalImages > 0 ? stats.imagesWithAlt / stats.totalImages : 1; // Deduct points based on ratios score -= (1 - titleRatio) * 20; score -= (1 - descriptionRatio) * 15; score -= (1 - h1Ratio) * 15; score -= (1 - altTextRatio) * 10; // Deduct for issues issues.forEach(issue => { switch (issue.severity) { case 'critical': score -= 8; break; case 'high': score -= 4; break; case 'medium': score -= 2; break; case 'low': score -= 1; break; } }); return Math.max(0, Math.min(100, score)); } calculateSocialScore(stats, issues) { let score = 100; const ogRatio = stats.pagesChecked > 0 ? stats.ogTags.found / stats.pagesChecked : 0; const ogCompleteRatio = stats.ogTags.found > 0 ? stats.ogTags.complete / stats.ogTags.found : 0; score -= (1 - ogRatio) * 30; score -= (1 - ogCompleteRatio) * 20; // Twitter cards are less critical const twitterRatio = stats.pagesChecked > 0 ? stats.twitterCards.found / stats.pagesChecked : 0; score -= (1 - twitterRatio) * 10; issues.forEach(issue => { switch (issue.severity) { case 'high': score -= 5; break; case 'medium': score -= 3; break; case 'low': score -= 1; break; } }); return Math.max(0, Math.min(100, score)); } calculatePerformanceScore(factors, issues) { let score = 100; // Large images have significant impact score -= factors.largeImages.length * 3; // Render blocking resources score -= factors.renderBlocking.length * 2; // Other factors if (factors.noLazyLoad > 0) score -= 10; if (factors.noCaching > 0) score -= 15; issues.forEach(issue => { switch (issue.severity) { case 'high': score -= 5; break; case 'medium': score -= 3; break; case 'low': score -= 1; break; } }); return Math.max(0, Math.min(100, score)); } getGrade(score) { if (score >= 90) return 'A+'; if (score >= 85) return 'A'; if (score >= 80) return 'A-'; if (score >= 75) return 'B+'; if (score >= 70) return 'B'; if (score >= 65) return 'B-'; if (score >= 60) return 'C+'; if (score >= 55) return 'C'; if (score >= 50) return 'C-'; if (score >= 45) return 'D+'; if (score >= 40) return 'D'; return 'F'; } getContentRecommendations(stats, issues) { const recommendations = []; if (stats.titlesFound < stats.pagesAnalyzed * 0.9) { recommendations.push({ priority: 'high', action: 'Add unique title tags to all pages', impact: 'Significant improvement in search visibility and CTR' }); } if (stats.descriptionsFound < stats.pagesAnalyzed * 0.8) { recommendations.push({ priority: 'high', action: 'Write compelling meta descriptions for all pages', impact: 'Better click-through rates from search results' }); } if (stats.imagesWithAlt < stats.totalImages * 0.9) { recommendations.push({ priority: 'medium', action: 'Add descriptive alt text to all images', impact: 'Improved accessibility and image search rankings' }); } if (issues.some(i => i.type === 'keyword-stuffing')) { recommendations.push({ priority: 'high', action: 'Review and rewrite content to avoid keyword stuffing', impact: 'Avoid potential search engine penalties' }); } return recommendations; } getTopPriorities() { const priorities = []; const allIssues = [ ...(this.results.technical?.issues || []), ...(this.results.content?.issues || []), ...(this.results.social?.issues || []), ...(this.results.performance?.issues || []) ]; const criticalIssues = allIssues.filter(i => i.severity === 'critical'); const highIssues = allIssues.filter(i => i.severity === 'high'); if (criticalIssues.length > 0) { priorities.push({ level: 'critical', count: criticalIssues.length, examples: criticalIssues.slice(0, 3).map(i => i.message) }); } if (highIssues.length > 0) { priorities.push({ level: 'high', count: highIssues.length, examples: highIssues.slice(0, 3).map(i => i.message) }); } return priorities; } getOverallRecommendations() { const recommendations = []; const score = this.results.overall?.score || 0; if (score < 60) { recommendations.push({ priority: 'critical', title: 'Foundation First', description: 'Focus on technical SEO basics: robots.txt, sitemap.xml, and meta tags', timeframe: 'Immediate (1-2 days)', impact: 'Essential for search engine visibility' }); } if (!this.results.technical?.checks?.robots?.found) { recommendations.push({ priority: 'high', title: 'Create robots.txt', description: 'Add a robots.txt file to control search engine crawling', timeframe: '1 hour', impact: 'Prevents indexing of unwanted pages' }); } if (!this.results.technical?.checks?.sitemap?.found) { recommendations.push({ priority: 'high', title: 'Generate XML Sitemap', description: 'Create and submit an XML sitemap to search engines', timeframe: '2-3 hours', impact: 'Ensures all pages are discovered and indexed' }); } if (this.results.content?.stats?.titlesFound < this.results.content?.stats?.pagesAnalyzed * 0.8) { recommendations.push({ priority: 'high', title: 'Optimize Page Titles', description: 'Write unique, keyword-rich titles for all pages (50-60 chars)', timeframe: '1-2 days', impact: 'Major impact on rankings and click-through rates' }); } if (this.results.performance?.factors?.largeImages?.length > 5) { recommendations.push({ priority: 'medium', title: 'Optimize Images', description: 'Compress large images and implement lazy loading', timeframe: '1 day', impact: 'Faster page loads, better Core Web Vitals' }); } if (!this.results.social?.stats?.ogTags?.found) { recommendations.push({ priority: 'medium', title: 'Add Social Meta Tags', description: 'Implement Open Graph and Twitter Card tags', timeframe: '3-4 hours', impact: 'Better social media visibility and engagement' }); } // Sort by priority const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); return recommendations.slice(0, 10); // Top 10 recommendations } async findFiles(extensions) { const files = []; async function scan(dir) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'build' && entry.name !== '.git' && entry.name !== 'coverage') { await scan(fullPath); } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { files.push(fullPath); } } } catch {} } await scan(this.projectPath); return files; } formatResults() { const sections = []; const colors = { passed: chalk.green, warning: chalk.yellow, failed: chalk.red, critical: chalk.bgRed.white, high: chalk.red, medium: chalk.yellow, low: chalk.gray }; // Overall score if (this.results.overall) { const { score, grade, status } = this.results.overall; const color = colors[status] || chalk.white; sections.push(color.bold(`\nšŸŽÆ SEO Score: ${score}/100 (${grade})\n`)); } // Category summaries ['technical', 'content', 'social', 'performance'].forEach(category => { const result = this.results[category]; if (!result) return; const icon = result.status === 'passed' ? 'āœ…' : result.status === 'failed' ? 'āŒ' : 'āš ļø'; const color = colors[result.status] || chalk.white; sections.push(color(`${icon} ${category.toUpperCase()}: ${result.message}`)); if (result.score !== undefined) { sections.push(chalk.gray(` Score: ${result.score}/100`)); } // Show top issues if (result.issues && result.issues.length > 0) { const topIssues = result.issues .filter(i => i.severity === 'critical' || i.severity === 'high') .slice(0, 3); topIssues.forEach(issue => { const issueColor = colors[issue.severity] || chalk.white; sections.push(issueColor(` • ${issue.message}`)); if (issue.recommendation) { sections.push(chalk.gray(` → ${issue.recommendation}`)); } }); } }); // Top recommendations if (this.results.overall?.recommendations?.length > 0) { sections.push(chalk.bold('\nšŸ“‹ Top Recommendations:')); this.results.overall.recommendations.slice(0, 5).forEach((rec, i) => { const color = colors[rec.priority] || chalk.white; sections.push(color(`${i + 1}. ${rec.title}`)); sections.push(chalk.gray(` ${rec.description}`)); sections.push(chalk.gray(` ā±ļø ${rec.timeframe} | šŸ’” ${rec.impact}\n`)); }); } return sections.join('\n'); } }