UNPKG

@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

413 lines 18.1 kB
"use strict"; /** * Schema Markup Analysis and Validation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaMarkupAnalyzer = void 0; class SchemaMarkupAnalyzer { constructor() { this.commonSchemaTypes = [ 'Organization', 'LocalBusiness', 'Product', 'Article', 'BlogPosting', 'NewsArticle', 'Recipe', 'Event', 'Person', 'WebPage', 'WebSite', 'BreadcrumbList', 'Review', 'Rating', 'FAQ', 'HowTo', 'Service', 'ContactPoint' ]; this.requiredProperties = { 'Organization': ['name', 'url'], 'LocalBusiness': ['name', 'address', 'telephone'], 'Product': ['name', 'image', 'description'], 'Article': ['headline', 'author', 'datePublished'], 'BlogPosting': ['headline', 'author', 'datePublished'], 'Recipe': ['name', 'author', 'recipeIngredient', 'recipeInstructions'], 'Event': ['name', 'startDate', 'location'], 'Person': ['name'], 'Review': ['reviewBody', 'author', 'itemReviewed'], 'FAQ': ['mainEntity'], 'HowTo': ['name', 'step'] }; } /** * Analyze schema markup on a page */ async analyze(page) { console.log('🏷️ Analyzing schema markup...'); const schemas = await this.extractSchemaMarkup(page); const validatedSchemas = schemas.map(schema => this.validateSchema(schema)); const validSchemas = validatedSchemas.filter(s => s.isValid); const invalidSchemas = validatedSchemas.filter(s => !s.isValid); const supportedTypes = this.analyzeSupportedTypes(validatedSchemas); const richSnippetOpportunities = await this.identifyRichSnippetOpportunities(page, validatedSchemas); const recommendations = this.generateRecommendations(validatedSchemas, supportedTypes, richSnippetOpportunities); const score = this.calculateScore(validatedSchemas, supportedTypes); const grade = this.calculateGrade(score); return { hasStructuredData: validatedSchemas.length > 0, totalSchemas: validatedSchemas.length, validSchemas: validSchemas.length, invalidSchemas: invalidSchemas.length, schemas: validatedSchemas, recommendations, score, grade, supportedTypes, richSnippetOpportunities }; } /** * Extract schema markup from page */ async extractSchemaMarkup(page) { return await page.evaluate(() => { const schemas = []; // Extract JSON-LD schemas const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]'); jsonLdScripts.forEach((script, index) => { try { const content = script.textContent?.trim(); if (content) { const data = JSON.parse(content); const schemaArray = Array.isArray(data) ? data : [data]; schemaArray.forEach((schemaData, subIndex) => { schemas.push({ type: schemaData['@type'] || 'Unknown', context: schemaData['@context'] || undefined, properties: schemaData, errors: [], warnings: [], isValid: true, location: { selector: `script[type="application/ld+json"]:nth-child(${index + 1})`, line: undefined, column: undefined } }); }); } } catch (error) { schemas.push({ type: 'Invalid JSON-LD', properties: {}, errors: [`JSON parsing error: ${error?.message || 'Unknown error'}`], warnings: [], isValid: false, location: { selector: `script[type="application/ld+json"]:nth-child(${index + 1})` } }); } }); // Extract Microdata const microdataElements = document.querySelectorAll('[itemscope]'); microdataElements.forEach((element, index) => { const itemType = element.getAttribute('itemtype'); const properties = {}; // Extract properties const propertyElements = element.querySelectorAll('[itemprop]'); propertyElements.forEach(propElement => { const propName = propElement.getAttribute('itemprop'); const propValue = propElement.getAttribute('content') || propElement.textContent?.trim() || propElement.getAttribute('href') || propElement.getAttribute('src'); if (propName && propValue) { if (properties[propName]) { if (Array.isArray(properties[propName])) { properties[propName].push(propValue); } else { properties[propName] = [properties[propName], propValue]; } } else { properties[propName] = propValue; } } }); if (itemType) { const typeName = itemType.split('/').pop() || itemType; schemas.push({ type: typeName, properties, errors: [], warnings: [], isValid: true, location: { selector: `[itemscope]:nth-child(${index + 1})` } }); } }); // Extract RDFa (basic support) const rdfaElements = document.querySelectorAll('[typeof]'); rdfaElements.forEach((element, index) => { const typeOf = element.getAttribute('typeof'); const properties = {}; // Basic RDFa property extraction const propElements = element.querySelectorAll('[property]'); propElements.forEach(propElement => { const propName = propElement.getAttribute('property'); const propValue = propElement.getAttribute('content') || propElement.textContent?.trim(); if (propName && propValue) { properties[propName] = propValue; } }); if (typeOf) { schemas.push({ type: typeOf, properties, errors: [], warnings: [], isValid: true, location: { selector: `[typeof]:nth-child(${index + 1})` } }); } }); return schemas; }); } /** * Validate a schema against Schema.org requirements */ validateSchema(schema) { const errors = [...schema.errors]; const warnings = [...schema.warnings]; // Check for required context if (!schema.context && schema.type !== 'Invalid JSON-LD') { warnings.push('Missing @context property (recommended: "https://schema.org")'); } // Validate against required properties const requiredProps = this.requiredProperties[schema.type]; if (requiredProps) { for (const prop of requiredProps) { if (!schema.properties[prop]) { errors.push(`Missing required property: ${prop}`); } } } // Validate common properties if (schema.properties.url && !this.isValidUrl(schema.properties.url)) { errors.push('Invalid URL format'); } if (schema.properties.email && !this.isValidEmail(schema.properties.email)) { errors.push('Invalid email format'); } if (schema.properties.telephone && !this.isValidPhoneNumber(schema.properties.telephone)) { warnings.push('Phone number format may not be optimal'); } // Validate date formats const dateFields = ['datePublished', 'dateModified', 'startDate', 'endDate']; for (const field of dateFields) { if (schema.properties[field] && !this.isValidDate(schema.properties[field])) { errors.push(`Invalid date format for ${field} (use ISO 8601)`); } } // Validate image properties if (schema.properties.image) { const images = Array.isArray(schema.properties.image) ? schema.properties.image : [schema.properties.image]; for (const image of images) { if (typeof image === 'string' && !this.isValidUrl(image)) { errors.push('Invalid image URL'); } else if (typeof image === 'object' && !image.url) { errors.push('Image object missing URL property'); } } } return { ...schema, errors, warnings, isValid: errors.length === 0 }; } /** * Analyze supported schema types */ analyzeSupportedTypes(schemas) { const found = [...new Set(schemas.filter(s => s.isValid).map(s => s.type))]; const missing = this.commonSchemaTypes.filter(type => !found.includes(type)); // Recommend based on page content analysis const recommended = missing.filter(type => { // This would be enhanced with actual page content analysis return ['Organization', 'WebPage', 'BreadcrumbList'].includes(type); }); return { found, recommended, missing }; } /** * Identify rich snippet opportunities */ async identifyRichSnippetOpportunities(page, schemas) { const opportunities = []; const pageContent = await page.evaluate(() => { return { hasProducts: document.querySelectorAll('[class*="product"], .product-item, .product-card').length > 0, hasReviews: document.querySelectorAll('[class*="review"], .review, .rating').length > 0, hasRecipes: document.querySelectorAll('[class*="recipe"], .recipe').length > 0, hasEvents: document.querySelectorAll('[class*="event"], .event').length > 0, hasFAQ: document.querySelectorAll('[class*="faq"], .faq, .accordion').length > 0, hasHowTo: document.querySelectorAll('[class*="tutorial"], [class*="guide"], .step').length > 0, hasArticles: document.querySelectorAll('article, [class*="post"], [class*="article"]').length > 0, hasBreadcrumbs: document.querySelectorAll('[class*="breadcrumb"], .breadcrumb').length > 0 }; }); const existingTypes = schemas.map(s => s.type); if (pageContent.hasProducts && !existingTypes.includes('Product')) { opportunities.push({ type: 'Product', description: 'Add Product schema to enable rich snippets with price, availability, and ratings', priority: 'high', implementation: 'Add JSON-LD with @type: "Product" including name, image, description, offers' }); } if (pageContent.hasReviews && !existingTypes.includes('Review')) { opportunities.push({ type: 'Review', description: 'Add Review schema to display star ratings in search results', priority: 'high', implementation: 'Add JSON-LD with @type: "Review" including reviewBody, author, reviewRating' }); } if (pageContent.hasRecipes && !existingTypes.includes('Recipe')) { opportunities.push({ type: 'Recipe', description: 'Add Recipe schema for enhanced recipe search results with cooking time and ingredients', priority: 'medium', implementation: 'Add JSON-LD with @type: "Recipe" including ingredients, instructions, nutrition' }); } if (pageContent.hasFAQ && !existingTypes.includes('FAQPage')) { opportunities.push({ type: 'FAQPage', description: 'Add FAQ schema to display questions and answers directly in search results', priority: 'medium', implementation: 'Add JSON-LD with @type: "FAQPage" and mainEntity array of questions' }); } if (pageContent.hasHowTo && !existingTypes.includes('HowTo')) { opportunities.push({ type: 'HowTo', description: 'Add HowTo schema for step-by-step instruction rich snippets', priority: 'medium', implementation: 'Add JSON-LD with @type: "HowTo" including step array with instructions' }); } if (pageContent.hasBreadcrumbs && !existingTypes.includes('BreadcrumbList')) { opportunities.push({ type: 'BreadcrumbList', description: 'Add Breadcrumb schema to display navigation path in search results', priority: 'low', implementation: 'Add JSON-LD with @type: "BreadcrumbList" and itemListElement array' }); } return opportunities; } /** * Generate recommendations */ generateRecommendations(schemas, supportedTypes, opportunities) { const recommendations = []; if (schemas.length === 0) { recommendations.push('Add structured data markup to improve SEO and enable rich snippets'); recommendations.push('Start with basic Organization or WebPage schema markup'); } const invalidSchemas = schemas.filter(s => !s.isValid); if (invalidSchemas.length > 0) { recommendations.push(`Fix ${invalidSchemas.length} invalid schema markup(s) to ensure proper indexing`); } const schemasWithWarnings = schemas.filter(s => s.warnings.length > 0); if (schemasWithWarnings.length > 0) { recommendations.push('Address schema warnings to improve markup quality'); } if (supportedTypes.recommended.length > 0) { recommendations.push(`Consider adding ${supportedTypes.recommended.slice(0, 3).join(', ')} schema types`); } const highPriorityOpportunities = opportunities.filter(o => o.priority === 'high'); if (highPriorityOpportunities.length > 0) { recommendations.push(`Implement ${highPriorityOpportunities[0].type} schema for enhanced search visibility`); } if (schemas.some(s => s.context !== 'https://schema.org')) { recommendations.push('Use "https://schema.org" as @context for better compatibility'); } return recommendations.slice(0, 5); } /** * Calculate schema score */ calculateScore(schemas, supportedTypes) { if (schemas.length === 0) return 0; let score = 0; // Base score for having schemas score += Math.min(schemas.length * 10, 40); // Bonus for valid schemas const validSchemas = schemas.filter(s => s.isValid); score += (validSchemas.length / schemas.length) * 30; // Bonus for variety of schema types score += Math.min(supportedTypes.found.length * 5, 20); // Penalty for errors const totalErrors = schemas.reduce((sum, s) => sum + s.errors.length, 0); score -= Math.min(totalErrors * 5, 30); // Bonus for common important schemas const importantSchemas = ['Organization', 'WebPage', 'BreadcrumbList']; const hasImportantSchemas = importantSchemas.filter(type => supportedTypes.found.includes(type)).length; score += hasImportantSchemas * 3; return Math.max(0, Math.min(100, Math.round(score))); } /** * Calculate grade based on score */ calculateGrade(score) { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 70) return 'C'; if (score >= 60) return 'D'; return 'F'; } // Utility validation methods isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } isValidPhoneNumber(phone) { // Basic phone validation - could be enhanced return /^[+]?[\d\s\-()]{10,}$/.test(phone); } isValidDate(date) { // ISO 8601 date validation const isoDateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/; return isoDateRegex.test(date) && !isNaN(Date.parse(date)); } } exports.SchemaMarkupAnalyzer = SchemaMarkupAnalyzer; exports.default = SchemaMarkupAnalyzer; //# sourceMappingURL=schema-markup-analyzer.js.map