139 lines
4.0 KiB
TypeScript
139 lines
4.0 KiB
TypeScript
|
|
/**
|
||
|
|
* SEO Audit Script
|
||
|
|
* Runs comprehensive SEO audits across all content
|
||
|
|
*/
|
||
|
|
import { db } from '../db/index.js';
|
||
|
|
import { pages, translations } from '../db/schema.js';
|
||
|
|
import { eq } from 'drizzle-orm';
|
||
|
|
import { runSeoAudit, generateSitemap } from '../services/seo-service.js';
|
||
|
|
import { ALL_SITES } from '../config/sites.js';
|
||
|
|
import { writeFileSync, mkdirSync } from 'fs';
|
||
|
|
import { join } from 'path';
|
||
|
|
|
||
|
|
async function runSeoAuditBatch(options: {
|
||
|
|
domain?: string;
|
||
|
|
language?: string;
|
||
|
|
generateSitemaps?: boolean;
|
||
|
|
outputDir?: string;
|
||
|
|
} = {}) {
|
||
|
|
console.log('🔍 Starting SEO audit...\n');
|
||
|
|
|
||
|
|
const outputDir = options.outputDir || './output/seo';
|
||
|
|
mkdirSync(outputDir, { recursive: true });
|
||
|
|
|
||
|
|
// Audit source pages
|
||
|
|
const sourcePages = db.select().from(pages)
|
||
|
|
.where(eq(pages.status, 'published'))
|
||
|
|
.all();
|
||
|
|
|
||
|
|
console.log(`Auditing ${sourcePages.length} source pages...\n`);
|
||
|
|
|
||
|
|
const results: Array<{
|
||
|
|
url: string;
|
||
|
|
language: string;
|
||
|
|
score: number;
|
||
|
|
issues: number;
|
||
|
|
}> = [];
|
||
|
|
|
||
|
|
for (let i = 0; i < sourcePages.length; i++) {
|
||
|
|
const page = sourcePages[i];
|
||
|
|
console.log(` [${i + 1}/${sourcePages.length}] ${page.title}`);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Audit source
|
||
|
|
const sourceAudit = runSeoAudit(page.id);
|
||
|
|
results.push({
|
||
|
|
url: sourceAudit.url,
|
||
|
|
language: sourceAudit.language,
|
||
|
|
score: sourceAudit.score,
|
||
|
|
issues: sourceAudit.issues.length
|
||
|
|
});
|
||
|
|
|
||
|
|
// Audit translations
|
||
|
|
const pageTranslations = db.select().from(translations)
|
||
|
|
.where(eq(translations.pageId, page.id))
|
||
|
|
.all();
|
||
|
|
|
||
|
|
for (const translation of pageTranslations) {
|
||
|
|
if (options.language && translation.targetLanguage !== options.language) continue;
|
||
|
|
|
||
|
|
const transAudit = runSeoAudit(page.id, translation.targetLanguage);
|
||
|
|
results.push({
|
||
|
|
url: transAudit.url,
|
||
|
|
language: transAudit.language,
|
||
|
|
score: transAudit.score,
|
||
|
|
issues: transAudit.issues.length
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.log(` ✗ Error: ${err instanceof Error ? err.message : 'Unknown'}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Summary
|
||
|
|
const avgScore = results.length > 0
|
||
|
|
? results.reduce((sum, r) => sum + r.score, 0) / results.length
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
const lowScorePages = results.filter(r => r.score < 70);
|
||
|
|
|
||
|
|
console.log(`\n📊 SEO Audit Summary`);
|
||
|
|
console.log(` Total audited: ${results.length}`);
|
||
|
|
console.log(` Average score: ${Math.round(avgScore * 10) / 10}/100`);
|
||
|
|
console.log(` Pages needing attention: ${lowScorePages.length}`);
|
||
|
|
|
||
|
|
if (lowScorePages.length > 0) {
|
||
|
|
console.log(`\n⚠️ Low scores:`);
|
||
|
|
lowScorePages.forEach(r => {
|
||
|
|
console.log(` - ${r.url} (${r.language}): ${Math.round(r.score)}/100`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate sitemaps
|
||
|
|
if (options.generateSitemaps) {
|
||
|
|
console.log(`\n🗺️ Generating sitemaps...`);
|
||
|
|
|
||
|
|
for (const site of ALL_SITES) {
|
||
|
|
const sitemap = generateSitemap(site.domain);
|
||
|
|
const filename = join(outputDir, `sitemap-${site.domain}.xml`);
|
||
|
|
writeFileSync(filename, sitemap);
|
||
|
|
console.log(` ✓ ${filename}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save report
|
||
|
|
const report = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
summary: {
|
||
|
|
totalAudited: results.length,
|
||
|
|
averageScore: Math.round(avgScore * 10) / 10,
|
||
|
|
needsAttention: lowScorePages.length
|
||
|
|
},
|
||
|
|
results
|
||
|
|
};
|
||
|
|
|
||
|
|
const reportFile = join(outputDir, `audit-report-${new Date().toISOString().split('T')[0]}.json`);
|
||
|
|
writeFileSync(reportFile, JSON.stringify(report, null, 2));
|
||
|
|
console.log(`\n📝 Report saved: ${reportFile}`);
|
||
|
|
|
||
|
|
console.log('\n✅ SEO audit complete!');
|
||
|
|
}
|
||
|
|
|
||
|
|
// CLI
|
||
|
|
const args = process.argv.slice(2);
|
||
|
|
const options: {
|
||
|
|
domain?: string;
|
||
|
|
language?: string;
|
||
|
|
generateSitemaps?: boolean;
|
||
|
|
} = {};
|
||
|
|
|
||
|
|
if (args.includes('--sitemaps')) options.generateSitemaps = true;
|
||
|
|
|
||
|
|
const domainArg = args.find(a => a.startsWith('--domain='));
|
||
|
|
if (domainArg) options.domain = domainArg.split('=')[1];
|
||
|
|
|
||
|
|
const langArg = args.find(a => a.startsWith('--lang='));
|
||
|
|
if (langArg) options.language = langArg.split('=')[1];
|
||
|
|
|
||
|
|
runSeoAuditBatch(options).catch(console.error);
|