import { Page } from '@playwright/test'; import { SEOResult, MetaTagResult, HeadingResult, LinkResult, ImageResult } from '../../types'; export class SEOValidator { constructor(private page: Page) {} async validateSEO(): Promise { const metaTags = await this.validateMetaTags(); const headings = await this.validateHeadings(); const links = await this.validateLinks(); const images = await this.validateImages(); const score = this.calculateScore(metaTags, headings, links, images); return { score, metaTags, headings, links, images }; } async validateMetaTags(): Promise { const title = await this.page.title(); const description = await this.page.getAttribute('meta[name="description"]', 'content'); const keywords = await this.page.getAttribute('meta[name="keywords"]', 'content'); const ogTitle = await this.page.getAttribute('meta[property="og:title"]', 'content'); const ogDescription = await this.page.getAttribute('meta[property="og:description"]', 'content'); const canonical = await this.page.getAttribute('link[rel="canonical"]', 'href'); return { title: !!title && title.length >= 10 && title.length <= 60, description: !!description && description.length >= 50 && description.length <= 160, keywords: !!keywords, ogTitle: !!ogTitle, ogDescription: !!ogDescription, canonical: !!canonical }; } async validateHeadings(): Promise { const h1Count = await this.page.locator('h1').count(); const hasH1 = h1Count > 0; const multipleH1 = h1Count > 1; const headings = await this.page.evaluate(() => { const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); return Array.from(elements).map(el => el.tagName); }); let headingStructure = true; let previousLevel = 0; for (const heading of headings) { const level = parseInt(heading.charAt(1)); if (level > previousLevel + 1) { headingStructure = false; break; } previousLevel = level; } return { hasH1, headingStructure, multipleH1 }; } async validateLinks(): Promise { const links = await this.page.locator('a').all(); let internal = 0; let external = 0; let broken = 0; for (const link of links) { const href = await link.getAttribute('href'); if (!href) continue; if (href.startsWith('http')) { external++; } else { internal++; } } return { total: links.length, broken, internal, external }; } async validateImages(): Promise { const images = await this.page.locator('img').all(); let withAlt = 0; let withoutAlt = 0; for (const image of images) { const alt = await image.getAttribute('alt'); if (alt && alt.trim() !== '') { withAlt++; } else { withoutAlt++; } } return { total: images.length, withAlt, withoutAlt }; } private calculateScore(metaTags: MetaTagResult, headings: HeadingResult, _links: LinkResult, images: ImageResult): number { let score = 0; let total = 0; const metaTagValues = Object.values(metaTags); score += metaTagValues.filter(v => v).length; total += metaTagValues.length; if (headings.hasH1) score++; if (headings.headingStructure) score++; if (!headings.multipleH1) score++; total += 3; if (images.withoutAlt === 0) score++; total++; return Math.round((score / total) * 100); } }