118 lines
4.0 KiB
JavaScript
118 lines
4.0 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
const DIST_DIR = path.resolve('dist');
|
||
|
||
if (!fs.existsSync(DIST_DIR)) {
|
||
console.error(`❌ Dist directory not found at ${DIST_DIR}. Please run "pnpm build" first.`);
|
||
process.exit(1);
|
||
}
|
||
|
||
function getAllHtmlFiles(dir, fileList = []) {
|
||
const files = fs.readdirSync(dir);
|
||
for (const file of files) {
|
||
const fullPath = path.join(dir, file);
|
||
const stat = fs.statSync(fullPath);
|
||
if (stat.isDirectory()) {
|
||
getAllHtmlFiles(fullPath, fileList);
|
||
} else if (file.endsWith('.html')) {
|
||
fileList.push(fullPath);
|
||
}
|
||
}
|
||
return fileList;
|
||
}
|
||
|
||
const htmlFiles = getAllHtmlFiles(DIST_DIR);
|
||
console.log(`\n======================================================`);
|
||
console.log(`🔍 Programmatic SEO Automated Audit`);
|
||
console.log(`======================================================`);
|
||
console.log(`📁 Scanning static output directory: ${DIST_DIR}`);
|
||
console.log(`📄 Total HTML Pages Generated (N): ${htmlFiles.length}\n`);
|
||
|
||
let passedCount = 0;
|
||
let warningCount = 0;
|
||
let errorCount = 0;
|
||
|
||
const issues = [];
|
||
|
||
for (const filePath of htmlFiles) {
|
||
const relPath = path.relative(DIST_DIR, filePath);
|
||
const content = fs.readFileSync(filePath, 'utf-8');
|
||
const is404 = relPath.includes('404.html');
|
||
|
||
// 1. Check Title
|
||
const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
|
||
if (!titleMatch && !is404) {
|
||
issues.push({ file: relPath, level: 'ERROR', message: 'Missing <title> tag' });
|
||
errorCount++;
|
||
}
|
||
|
||
// 2. Check Description
|
||
const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i);
|
||
if (!descMatch && !is404) {
|
||
issues.push({ file: relPath, level: 'WARNING', message: 'Missing meta description' });
|
||
warningCount++;
|
||
}
|
||
|
||
// 3. Check Canonical
|
||
const canonicalMatch = content.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']*)["']/i);
|
||
if (!canonicalMatch && !is404) {
|
||
issues.push({ file: relPath, level: 'ERROR', message: 'Missing canonical URL link tag' });
|
||
errorCount++;
|
||
} else if (canonicalMatch) {
|
||
const canonical = canonicalMatch[1];
|
||
if (canonical.includes('?')) {
|
||
issues.push({ file: relPath, level: 'ERROR', message: `Canonical contains query parameters: ${canonical}` });
|
||
errorCount++;
|
||
}
|
||
if (!canonical.startsWith('http://') && !canonical.startsWith('https://')) {
|
||
issues.push({ file: relPath, level: 'ERROR', message: `Canonical is relative instead of absolute: ${canonical}` });
|
||
errorCount++;
|
||
}
|
||
}
|
||
|
||
// 4. Check Robots (Accidental noindex detection)
|
||
const robotsMatch = content.match(/<meta[^>]*name=["']robots["'][^>]*content=["']([^"']*)["']/i);
|
||
if (robotsMatch) {
|
||
const robots = robotsMatch[1].toLowerCase();
|
||
if (robots.includes('noindex') && !is404) {
|
||
issues.push({ file: relPath, level: 'WARNING', message: `Page has noindex tag: "${robots}"` });
|
||
warningCount++;
|
||
}
|
||
}
|
||
|
||
// 5. Check JSON-LD
|
||
const hasJsonLd = content.includes('application/ld+json');
|
||
if (!hasJsonLd && !is404) {
|
||
issues.push({ file: relPath, level: 'INFO', message: 'Missing Schema.org JSON-LD structured data' });
|
||
}
|
||
|
||
if (issues.filter(i => i.file === relPath).length === 0) {
|
||
passedCount++;
|
||
}
|
||
}
|
||
|
||
// Print results
|
||
console.log(`📊 Audit Summary:`);
|
||
console.log(` - Perfect Pages: ${passedCount}`);
|
||
console.log(` - Warnings: ${warningCount}`);
|
||
console.log(` - Critical Errors: ${errorCount}`);
|
||
|
||
if (issues.length > 0) {
|
||
console.log(`\n📋 Detailed Findings:`);
|
||
for (const issue of issues.slice(0, 15)) {
|
||
const icon = issue.level === 'ERROR' ? '❌' : issue.level === 'WARNING' ? '⚠️' : 'ℹ️';
|
||
console.log(` ${icon} [${issue.level}] ${issue.file}: ${issue.message}`);
|
||
}
|
||
if (issues.length > 15) {
|
||
console.log(` ... and ${issues.length - 15} more findings.`);
|
||
}
|
||
}
|
||
|
||
if (errorCount > 0) {
|
||
console.log(`\n❌ Audit failed with ${errorCount} critical error(s).`);
|
||
process.exit(1);
|
||
} else {
|
||
console.log(`\n✅ SEO Pre-flight Audit Passed! All pages ready for Googlebot indexing.`);
|
||
}
|