@casoon/auditmysite
Version:
Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe
464 lines • 19 kB
JavaScript
;
/**
* 📊 Content Weight Analyzer
*
* Analyzes the weight and composition of webpage content including:
* - Resource sizes (HTML, CSS, JS, images, fonts)
* - Content quality metrics
* - Text-to-code ratios
* - Performance impact analysis
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContentWeightAnalyzer = void 0;
const base_types_1 = require("../types/base-types");
class ContentWeightAnalyzer {
constructor() {
this.resourceTimings = [];
this.responses = [];
}
// BaseAnalyzer interface implementations
getName() {
return 'ContentWeightAnalyzer';
}
getVersion() {
return '2.0.0';
}
getScore(result) {
return result.overallScore;
}
getGrade(score) {
return (0, base_types_1.calculateGrade)(score);
}
getCertificateLevel(score) {
return (0, base_types_1.calculateCertificateLevel)(score);
}
getRecommendations(result) {
return result.recommendations;
}
/**
* Main analyze method implementing BaseAnalyzer interface
*/
async analyze(page, url, options = {}) {
return this.analyzeWithResponses(page, url, options, []);
}
/**
* Enhanced analyze method that accepts pre-captured network responses
* This fixes the issue where network monitoring starts after page load
*/
async analyzeWithResponses(page, url, options = {}, networkResponses = []) {
// Extract URL string from URL object if needed
const urlString = (typeof url === 'object' && url.loc ? url.loc : url);
const startTime = Date.now();
// Use a separate analysis page when we must reload to capture all responses
let analysisPage = page;
let tempPage = null;
try {
// 🔧 CRITICAL FIX: Use pre-captured network responses instead of setting up tracking
// This ensures we capture all network requests from the beginning of page load
if (networkResponses.length > 0) {
this.responses = networkResponses;
if (options.verbose) {
console.log(`📊 Using ${this.responses.length} pre-captured network responses for analysis`);
}
}
else {
// Strict capture on an isolated page to avoid interfering with other analyzers
const context = page.context();
tempPage = await context.newPage();
analysisPage = tempPage;
this.setupResponseTracking(analysisPage);
const tsParam = (urlString.includes('?') ? '&' : '?') + 'ams_nocache=' + Date.now();
const freshUrl = urlString + tsParam;
await analysisPage.goto(freshUrl, { waitUntil: 'networkidle', timeout: options.analysisTimeout || 30000 });
if (options.verbose) {
console.log(`📊 Captured ${this.responses.length} network responses for analysis`);
}
}
// Collect resource data from the analysis page
const contentWeight = await this.calculateContentWeight(analysisPage);
const contentAnalysis = await this.analyzeContentComposition(analysisPage);
const resourceTimings = options.includeResourceAnalysis ? await this.extractResourceTimings(analysisPage) : [];
const duration = Date.now() - startTime;
// Calculate overall score based on content weight metrics
const overallScore = this.calculateOverallScore(contentWeight, contentAnalysis);
const grade = (0, base_types_1.calculateGrade)(overallScore);
const certificate = (0, base_types_1.calculateCertificateLevel)(overallScore);
// Generate recommendations
const recommendations = this.generateRecommendations(contentWeight, contentAnalysis);
const result = {
overallScore,
grade,
certificate,
analyzedAt: new Date().toISOString(),
duration,
status: 'completed',
contentWeight,
contentAnalysis,
resourceTimings,
recommendations
};
return result;
}
catch (error) {
console.error('❌ Content weight analysis failed:', error);
throw new Error(`Content weight analysis failed: ${error}`);
}
finally {
if (tempPage) {
try {
await tempPage.close();
}
catch { }
}
}
}
/**
* Set up response tracking to capture all network requests
*/
setupResponseTracking(page) {
this.responses = [];
this.resourceTimings = [];
page.on('response', (response) => {
this.responses.push(response);
});
}
/**
* Calculate the weight of different content types
*/
async calculateContentWeight(page) {
const weights = {
html: 0,
css: 0,
javascript: 0,
images: 0,
fonts: 0,
other: 0,
total: 0,
gzipTotal: 0,
compressionRatio: 0
};
let totalTransferSize = 0;
// Analyze all captured responses
for (const response of this.responses) {
try {
const url = response.url();
const headers = await response.headers();
const size = await this.getResponseSize(response);
const transferSize = this.getTransferSize(headers, size);
totalTransferSize += transferSize;
// Categorize by content type
const contentType = headers['content-type'] || '';
const category = this.categorizeResource(url, contentType);
weights[category] += size;
weights.total += size;
}
catch (error) {
console.warn(`Failed to analyze response ${response.url()}:`, error);
}
}
// Calculate compression metrics
weights.gzipTotal = totalTransferSize || weights.gzipTotal || 0;
weights.compressionRatio = weights.total > 0 ? (weights.gzipTotal / weights.total) : 0;
return weights;
}
/**
* Analyze content composition and quality metrics
*/
async analyzeContentComposition(page) {
const analysis = await page.evaluate(() => {
// Count text content
const bodyText = document.body.innerText || '';
const textContent = bodyText.length;
const wordCount = bodyText.trim().split(/\s+/).filter(word => word.length > 0).length;
// Count various elements
const imageCount = document.querySelectorAll('img').length;
const linkCount = document.querySelectorAll('a').length;
const domElements = document.querySelectorAll('*').length;
// Get HTML size for ratio calculation
const htmlSize = new TextEncoder().encode(document.documentElement.outerHTML).length;
return {
textContent,
wordCount,
imageCount,
linkCount,
domElements,
htmlSize
};
});
// Calculate text-to-code ratio
const textToCodeRatio = analysis.htmlSize > 0
? analysis.textContent / analysis.htmlSize
: 0;
// Calculate content quality score
const contentQualityScore = this.calculateContentQualityScore({
...analysis,
textToCodeRatio
});
return {
textContent: analysis.textContent,
imageCount: analysis.imageCount,
linkCount: analysis.linkCount,
domElements: analysis.domElements,
textToCodeRatio,
contentQualityScore,
wordCount: analysis.wordCount
};
}
/**
* Extract detailed resource timing information
*/
async extractResourceTimings(page) {
const resourceTimings = [];
// Get performance entries from the page
const performanceEntries = await page.evaluate(() => {
const entries = performance.getEntriesByType('resource');
return entries.map(entry => ({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
transferSize: entry.transferSize || 0,
encodedBodySize: entry.encodedBodySize || 0,
decodedBodySize: entry.decodedBodySize || 0
}));
});
// Combine with response data
for (const entry of performanceEntries) {
const matchingResponse = this.responses.find(r => r.url() === entry.name);
resourceTimings.push({
url: entry.name,
type: this.getResourceType(entry.name),
size: entry.decodedBodySize || entry.encodedBodySize,
duration: entry.duration,
transferSize: entry.transferSize,
cached: entry.transferSize === 0 && entry.duration < 10
});
}
return resourceTimings.sort((a, b) => b.size - a.size);
}
/**
* Get the size of a response
*/
async getResponseSize(response) {
try {
const buffer = await response.body();
return buffer.length;
}
catch {
// Fallback to content-length header
const headers = await response.headers();
return parseInt(headers['content-length'] || '0', 10);
}
}
/**
* Get transfer size from headers
*/
getTransferSize(headers, bodySize) {
// If gzipped, estimate compression
const isCompressed = headers['content-encoding']?.includes('gzip') ||
headers['content-encoding']?.includes('br') ||
headers['content-encoding']?.includes('deflate');
if (isCompressed && bodySize > 0) {
// Typical compression ratios: text ~70%, images ~5%
const contentType = headers['content-type'] || '';
if (contentType.includes('text') || contentType.includes('javascript') || contentType.includes('css')) {
return Math.round(bodySize * 0.3); // ~70% compression
}
return Math.round(bodySize * 0.95); // ~5% compression for images
}
return bodySize;
}
/**
* Categorize resource by URL and content type
*/
categorizeResource(url, contentType) {
// Remove the computed properties from the type check
if (contentType.includes('text/html'))
return 'html';
if (contentType.includes('text/css') || url.includes('.css'))
return 'css';
if (contentType.includes('javascript') || url.includes('.js') || url.includes('.mjs'))
return 'javascript';
if (contentType.includes('image/') || /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i.test(url))
return 'images';
if (contentType.includes('font/') || /\.(woff|woff2|ttf|otf|eot)(\?|$)/i.test(url))
return 'fonts';
return 'other';
}
/**
* Get resource type for timing analysis
*/
getResourceType(url) {
if (/\.(css)(\?|$)/i.test(url))
return 'stylesheet';
if (/\.(js|mjs)(\?|$)/i.test(url))
return 'script';
if (/\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i.test(url))
return 'image';
if (/\.(woff|woff2|ttf|otf|eot)(\?|$)/i.test(url))
return 'font';
if (/\.(mp4|mov|avi|webm)(\?|$)/i.test(url))
return 'video';
if (/\.(mp3|wav|ogg)(\?|$)/i.test(url))
return 'audio';
return 'other';
}
/**
* Calculate content quality score based on various factors
*/
calculateContentQualityScore(analysis) {
let score = 100;
// Text content scoring (40% of total)
if (analysis.wordCount < 200)
score -= 20;
else if (analysis.wordCount < 300)
score -= 10;
else if (analysis.wordCount > 2000)
score += 10;
// Text-to-code ratio scoring (30% of total)
if (analysis.textToCodeRatio < 0.1)
score -= 20;
else if (analysis.textToCodeRatio < 0.2)
score -= 10;
else if (analysis.textToCodeRatio > 0.4)
score += 15;
// DOM complexity scoring (20% of total)
if (analysis.domElements > 2000)
score -= 15;
else if (analysis.domElements > 1500)
score -= 10;
else if (analysis.domElements < 500)
score += 5;
// Media balance scoring (10% of total)
const imageToTextRatio = analysis.wordCount > 0 ? analysis.imageCount / analysis.wordCount : 0;
if (imageToTextRatio > 0.1)
score -= 10; // Too many images relative to text
else if (imageToTextRatio > 0.05)
score -= 5;
return Math.max(0, Math.min(100, score));
}
/**
* Format bytes to human readable string
*/
formatBytes(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* Calculate overall score from content weight and analysis data
*/
calculateOverallScore(contentWeight, contentAnalysis) {
let score = 100;
// Size scoring (50% weight)
const totalSizeMB = contentWeight.total / (1024 * 1024);
if (totalSizeMB > 5)
score -= 30;
else if (totalSizeMB > 3)
score -= 20;
else if (totalSizeMB > 1.5)
score -= 10;
else if (totalSizeMB < 0.5)
score += 10;
// Compression scoring (20% weight)
const compressionScore = (contentWeight.compressionRatio || 1) < 0.7 ? 15 : 0;
score += compressionScore;
// Content quality scoring (30% weight)
score = Math.round(score * 0.7 + contentAnalysis.contentQualityScore * 0.3);
return Math.max(0, Math.min(100, score));
}
/**
* Generate optimization recommendations
*/
generateRecommendations(contentWeight, contentAnalysis) {
const recommendations = [];
// Large image recommendations
if (contentWeight.images > 1024 * 1024) { // > 1MB images
recommendations.push({
id: 'optimize-images',
priority: 'high',
category: 'Performance',
issue: 'Large image files detected',
recommendation: 'Optimize images by compressing, using modern formats (WebP/AVIF), and implementing responsive images',
impact: 'Reduce page load time and bandwidth usage',
effort: 4,
scoreImprovement: 15
});
}
// Large JavaScript bundles
if (contentWeight.javascript > 500 * 1024) { // > 500KB JS
recommendations.push({
id: 'optimize-javascript',
priority: 'medium',
category: 'Performance',
issue: 'Large JavaScript bundles detected',
recommendation: 'Split JavaScript into smaller chunks, remove unused code, and implement code splitting',
impact: 'Improve initial page load time and reduce bundle size',
effort: 6,
scoreImprovement: 10
});
}
// Poor compression
if ((contentWeight.compressionRatio || 1) > 0.8) {
recommendations.push({
id: 'enable-compression',
priority: 'medium',
category: 'Performance',
issue: 'Poor or missing text compression',
recommendation: 'Enable gzip/brotli compression on your web server for text resources',
impact: 'Significantly reduce transferred file sizes',
effort: 2,
scoreImprovement: 8
});
}
// Low text-to-code ratio
if (contentAnalysis.textToCodeRatio < 0.2) {
recommendations.push({
id: 'improve-content-ratio',
priority: 'low',
category: 'Content Quality',
issue: 'Low text-to-code ratio detected',
recommendation: 'Increase meaningful text content or reduce excessive markup and scripts',
impact: 'Improve content quality and user experience',
effort: 3,
scoreImprovement: 5
});
}
return recommendations;
}
/**
* Get performance recommendations based on content weight analysis
*/
static generateContentRecommendations(contentWeight, contentAnalysis) {
const recommendations = [];
const totalMB = contentWeight.total / (1024 * 1024);
// Size-based recommendations
if (totalMB > 3) {
recommendations.push(`📏 Page size is ${totalMB.toFixed(1)}MB - consider optimizing large resources`);
}
if (contentWeight.images > contentWeight.total * 0.6) {
recommendations.push(`🖼️ Images comprise ${((contentWeight.images / contentWeight.total) * 100).toFixed(0)}% of page weight - optimize image sizes`);
}
if (contentWeight.javascript > 1024 * 1024) {
recommendations.push(`📜 JavaScript bundle is ${(contentWeight.javascript / (1024 * 1024)).toFixed(1)}MB - consider code splitting`);
}
if (contentWeight.compressionRatio && contentWeight.compressionRatio > 0.8) {
recommendations.push(`🗜️ Enable better compression - current ratio: ${(contentWeight.compressionRatio * 100).toFixed(0)}%`);
}
// Content quality recommendations
if (contentAnalysis.textToCodeRatio < 0.15) {
recommendations.push(`📝 Low text-to-code ratio (${(contentAnalysis.textToCodeRatio * 100).toFixed(0)}%) - add more meaningful content`);
}
if (contentAnalysis.domElements > 1500) {
recommendations.push(`🏗️ High DOM complexity (${contentAnalysis.domElements} elements) - simplify page structure`);
}
if (contentAnalysis.wordCount < 300) {
recommendations.push(`💬 Low word count (${contentAnalysis.wordCount} words) - add more content for better SEO`);
}
return recommendations;
}
}
exports.ContentWeightAnalyzer = ContentWeightAnalyzer;
//# sourceMappingURL=content-weight-analyzer.js.map