aeorank 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -1
- package/data/benchmark.json +342302 -0
- package/data/sectors.json +1006 -0
- package/data/yc.json +168015 -0
- package/dist/cli.js +406 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +329 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { writeFileSync } from "fs";
|
|
5
|
+
|
|
3
6
|
// src/parked-domain.ts
|
|
4
7
|
var PARKING_PATHS = ["/lander", "/parking", "/park", "/sedoparking"];
|
|
5
8
|
var PARKING_SERVICE_DOMAINS = [
|
|
@@ -2495,18 +2498,343 @@ async function audit(domain, options) {
|
|
|
2495
2498
|
};
|
|
2496
2499
|
}
|
|
2497
2500
|
|
|
2501
|
+
// src/compare.ts
|
|
2502
|
+
async function compare(domainA, domainB, options) {
|
|
2503
|
+
const [siteA, siteB] = await Promise.all([
|
|
2504
|
+
audit(domainA, options),
|
|
2505
|
+
audit(domainB, options)
|
|
2506
|
+
]);
|
|
2507
|
+
const criteria = [];
|
|
2508
|
+
const siteAAdvantages = [];
|
|
2509
|
+
const siteBAdvantages = [];
|
|
2510
|
+
const tied = [];
|
|
2511
|
+
for (let i = 0; i < siteA.scorecard.length; i++) {
|
|
2512
|
+
const a = siteA.scorecard[i];
|
|
2513
|
+
const b = siteB.scorecard[i];
|
|
2514
|
+
if (!a || !b) continue;
|
|
2515
|
+
const delta = a.score - b.score;
|
|
2516
|
+
criteria.push({
|
|
2517
|
+
id: a.id,
|
|
2518
|
+
criterion: a.criterion,
|
|
2519
|
+
scoreA: a.score,
|
|
2520
|
+
scoreB: b.score,
|
|
2521
|
+
delta,
|
|
2522
|
+
statusA: a.status,
|
|
2523
|
+
statusB: b.status
|
|
2524
|
+
});
|
|
2525
|
+
if (delta > 0) siteAAdvantages.push(a.criterion);
|
|
2526
|
+
else if (delta < 0) siteBAdvantages.push(a.criterion);
|
|
2527
|
+
else tied.push(a.criterion);
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
siteA,
|
|
2531
|
+
siteB,
|
|
2532
|
+
comparison: {
|
|
2533
|
+
scoreDelta: siteA.overallScore - siteB.overallScore,
|
|
2534
|
+
criteria,
|
|
2535
|
+
siteAAdvantages,
|
|
2536
|
+
siteBAdvantages,
|
|
2537
|
+
tied
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/html-report.ts
|
|
2543
|
+
function scoreColor(score) {
|
|
2544
|
+
if (score <= 40) return "#F44336";
|
|
2545
|
+
if (score <= 55) return "#FF9800";
|
|
2546
|
+
if (score <= 70) return "#FFC107";
|
|
2547
|
+
if (score <= 85) return "#4CAF50";
|
|
2548
|
+
return "#2E7D32";
|
|
2549
|
+
}
|
|
2550
|
+
function criterionColor(score) {
|
|
2551
|
+
return scoreColor(score * 10);
|
|
2552
|
+
}
|
|
2553
|
+
function escapeHtml(str) {
|
|
2554
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2555
|
+
}
|
|
2556
|
+
function scoreCircleSvg(score, size = 160) {
|
|
2557
|
+
const radius = (size - 16) / 2;
|
|
2558
|
+
const circumference = 2 * Math.PI * radius;
|
|
2559
|
+
const progress = score / 100 * circumference;
|
|
2560
|
+
const color = scoreColor(score);
|
|
2561
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
2562
|
+
<circle cx="${size / 2}" cy="${size / 2}" r="${radius}" fill="none" stroke="#e0e0e0" stroke-width="10"/>
|
|
2563
|
+
<circle cx="${size / 2}" cy="${size / 2}" r="${radius}" fill="none" stroke="${color}" stroke-width="10"
|
|
2564
|
+
stroke-dasharray="${circumference}" stroke-dashoffset="${circumference - progress}"
|
|
2565
|
+
stroke-linecap="round" transform="rotate(-90 ${size / 2} ${size / 2})"/>
|
|
2566
|
+
<text x="${size / 2}" y="${size / 2 + 2}" text-anchor="middle" dominant-baseline="middle"
|
|
2567
|
+
font-size="42" font-weight="700" fill="${color}">${score}</text>
|
|
2568
|
+
<text x="${size / 2}" y="${size / 2 + 24}" text-anchor="middle" dominant-baseline="middle"
|
|
2569
|
+
font-size="13" fill="#666">/100</text>
|
|
2570
|
+
</svg>`;
|
|
2571
|
+
}
|
|
2572
|
+
var CSS = `
|
|
2573
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2574
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a1a; background: #f8f9fa; line-height: 1.6; }
|
|
2575
|
+
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
|
|
2576
|
+
.header { text-align: center; margin-bottom: 40px; }
|
|
2577
|
+
.header h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
2578
|
+
.header .date { color: #666; font-size: 14px; }
|
|
2579
|
+
.score-section { display: flex; justify-content: center; margin: 24px 0 32px; }
|
|
2580
|
+
.verdict { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px 24px; margin-bottom: 32px; font-size: 15px; color: #333; }
|
|
2581
|
+
.section-title { font-size: 20px; font-weight: 700; margin-bottom: 16px; color: #1a1a1a; }
|
|
2582
|
+
.scorecard-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; margin-bottom: 32px; }
|
|
2583
|
+
.criterion-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; }
|
|
2584
|
+
.criterion-bar { width: 80px; height: 8px; background: #e0e0e0; border-radius: 4px; flex-shrink: 0; overflow: hidden; }
|
|
2585
|
+
.criterion-bar-fill { height: 100%; border-radius: 4px; }
|
|
2586
|
+
.criterion-score { font-size: 14px; font-weight: 700; min-width: 32px; text-align: center; }
|
|
2587
|
+
.criterion-name { font-size: 13px; flex: 1; }
|
|
2588
|
+
.criterion-status { font-size: 11px; color: #666; background: #f0f0f0; padding: 2px 8px; border-radius: 10px; white-space: nowrap; }
|
|
2589
|
+
table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; margin-bottom: 32px; }
|
|
2590
|
+
th { background: #f5f5f5; text-align: left; padding: 10px 14px; font-size: 13px; font-weight: 600; color: #555; border-bottom: 1px solid #e0e0e0; }
|
|
2591
|
+
td { padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #f0f0f0; }
|
|
2592
|
+
.impact-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; color: #fff; }
|
|
2593
|
+
.impact-critical { background: #F44336; }
|
|
2594
|
+
.impact-high { background: #FF5722; }
|
|
2595
|
+
.impact-quick-win { background: #4CAF50; }
|
|
2596
|
+
.impact-core-aeo { background: #2196F3; }
|
|
2597
|
+
.impact-medium { background: #FF9800; }
|
|
2598
|
+
.impact-low { background: #9E9E9E; }
|
|
2599
|
+
.impact-big-opportunity { background: #9C27B0; }
|
|
2600
|
+
.impact-measurement { background: #607D8B; }
|
|
2601
|
+
.bottom-line { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px 24px; margin-bottom: 32px; font-size: 15px; }
|
|
2602
|
+
.footer { text-align: center; color: #999; font-size: 12px; padding: 24px 0; border-top: 1px solid #e0e0e0; margin-top: 24px; }
|
|
2603
|
+
.compare-header { display: flex; justify-content: center; align-items: center; gap: 48px; margin-bottom: 32px; }
|
|
2604
|
+
.compare-site { text-align: center; }
|
|
2605
|
+
.compare-site h2 { font-size: 20px; margin-bottom: 8px; }
|
|
2606
|
+
.compare-vs { font-size: 24px; font-weight: 700; color: #999; }
|
|
2607
|
+
.delta-positive { color: #4CAF50; font-weight: 600; }
|
|
2608
|
+
.delta-negative { color: #F44336; font-weight: 600; }
|
|
2609
|
+
.delta-zero { color: #999; }
|
|
2610
|
+
.summary-box { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px 20px; margin-bottom: 24px; display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; }
|
|
2611
|
+
.summary-stat { text-align: center; }
|
|
2612
|
+
.summary-stat .num { font-size: 28px; font-weight: 700; }
|
|
2613
|
+
.summary-stat .label { font-size: 12px; color: #666; }
|
|
2614
|
+
@media print {
|
|
2615
|
+
body { background: #fff; }
|
|
2616
|
+
.container { padding: 0; }
|
|
2617
|
+
.criterion-card, .verdict, .bottom-line, table { break-inside: avoid; }
|
|
2618
|
+
}
|
|
2619
|
+
@media (max-width: 640px) {
|
|
2620
|
+
.scorecard-grid { grid-template-columns: 1fr; }
|
|
2621
|
+
.compare-header { flex-direction: column; gap: 16px; }
|
|
2622
|
+
}
|
|
2623
|
+
`;
|
|
2624
|
+
function impactClass(impact) {
|
|
2625
|
+
const key = impact.toLowerCase().replace(/\s+/g, "-");
|
|
2626
|
+
return `impact-${key}`;
|
|
2627
|
+
}
|
|
2628
|
+
function generateHtmlReport(result) {
|
|
2629
|
+
const domain = escapeHtml(result.site);
|
|
2630
|
+
const date = escapeHtml(result.auditDate);
|
|
2631
|
+
const scorecardCards = result.scorecard.map((item) => {
|
|
2632
|
+
const color = criterionColor(item.score);
|
|
2633
|
+
const width = item.score * 10;
|
|
2634
|
+
return `<div class="criterion-card">
|
|
2635
|
+
<div class="criterion-bar"><div class="criterion-bar-fill" style="width:${width}%;background:${color}"></div></div>
|
|
2636
|
+
<span class="criterion-score" style="color:${color}">${item.score}/10</span>
|
|
2637
|
+
<span class="criterion-name">${escapeHtml(item.criterion)}</span>
|
|
2638
|
+
<span class="criterion-status">${escapeHtml(item.status)}</span>
|
|
2639
|
+
</div>`;
|
|
2640
|
+
}).join("\n");
|
|
2641
|
+
const opportunityRows = result.opportunities.map((opp) => {
|
|
2642
|
+
const cls = impactClass(opp.impact);
|
|
2643
|
+
return `<tr>
|
|
2644
|
+
<td>${opp.id}</td>
|
|
2645
|
+
<td>${escapeHtml(opp.name)}</td>
|
|
2646
|
+
<td><span class="impact-badge ${cls}">${escapeHtml(opp.impact)}</span></td>
|
|
2647
|
+
<td>${escapeHtml(opp.effort)}</td>
|
|
2648
|
+
<td>${escapeHtml(opp.description)}</td>
|
|
2649
|
+
</tr>`;
|
|
2650
|
+
}).join("\n");
|
|
2651
|
+
const pagesRows = (result.pagesReviewed || []).map((page) => {
|
|
2652
|
+
const issueCount = page.issues.length;
|
|
2653
|
+
const strengthCount = page.strengths.length;
|
|
2654
|
+
return `<tr>
|
|
2655
|
+
<td>${escapeHtml(page.url)}</td>
|
|
2656
|
+
<td>${escapeHtml(page.category)}</td>
|
|
2657
|
+
<td>${page.wordCount}</td>
|
|
2658
|
+
<td>${issueCount}</td>
|
|
2659
|
+
<td>${strengthCount}</td>
|
|
2660
|
+
</tr>`;
|
|
2661
|
+
}).join("\n");
|
|
2662
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2663
|
+
return `<!DOCTYPE html>
|
|
2664
|
+
<html lang="en">
|
|
2665
|
+
<head>
|
|
2666
|
+
<meta charset="UTF-8">
|
|
2667
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2668
|
+
<title>AEORank Report - ${domain}</title>
|
|
2669
|
+
<style>${CSS}</style>
|
|
2670
|
+
</head>
|
|
2671
|
+
<body>
|
|
2672
|
+
<div class="container">
|
|
2673
|
+
<div class="header">
|
|
2674
|
+
<h1>${domain}</h1>
|
|
2675
|
+
<div class="date">AEO Audit - ${date}</div>
|
|
2676
|
+
</div>
|
|
2677
|
+
|
|
2678
|
+
<div class="score-section">
|
|
2679
|
+
${scoreCircleSvg(result.overallScore)}
|
|
2680
|
+
</div>
|
|
2681
|
+
|
|
2682
|
+
<div class="verdict">${escapeHtml(result.verdict)}</div>
|
|
2683
|
+
|
|
2684
|
+
<h2 class="section-title">Scorecard (23 Criteria)</h2>
|
|
2685
|
+
<div class="scorecard-grid">
|
|
2686
|
+
${scorecardCards}
|
|
2687
|
+
</div>
|
|
2688
|
+
|
|
2689
|
+
${result.opportunities.length > 0 ? `
|
|
2690
|
+
<h2 class="section-title">Opportunities (${result.opportunities.length})</h2>
|
|
2691
|
+
<table>
|
|
2692
|
+
<thead><tr><th>#</th><th>Opportunity</th><th>Impact</th><th>Effort</th><th>Description</th></tr></thead>
|
|
2693
|
+
<tbody>${opportunityRows}</tbody>
|
|
2694
|
+
</table>
|
|
2695
|
+
` : ""}
|
|
2696
|
+
|
|
2697
|
+
${(result.pagesReviewed || []).length > 0 ? `
|
|
2698
|
+
<h2 class="section-title">Pages Reviewed (${(result.pagesReviewed || []).length})</h2>
|
|
2699
|
+
<table>
|
|
2700
|
+
<thead><tr><th>URL</th><th>Category</th><th>Words</th><th>Issues</th><th>Strengths</th></tr></thead>
|
|
2701
|
+
<tbody>${pagesRows}</tbody>
|
|
2702
|
+
</table>
|
|
2703
|
+
` : ""}
|
|
2704
|
+
|
|
2705
|
+
<div class="bottom-line"><strong>Bottom line:</strong> ${escapeHtml(result.bottomLine)}</div>
|
|
2706
|
+
|
|
2707
|
+
<div class="footer">Generated by AEORank - ${now}</div>
|
|
2708
|
+
</div>
|
|
2709
|
+
</body>
|
|
2710
|
+
</html>`;
|
|
2711
|
+
}
|
|
2712
|
+
function generateComparisonHtmlReport(result) {
|
|
2713
|
+
const domainA = escapeHtml(result.siteA.site);
|
|
2714
|
+
const domainB = escapeHtml(result.siteB.site);
|
|
2715
|
+
const scoreA = result.siteA.overallScore;
|
|
2716
|
+
const scoreB = result.siteB.overallScore;
|
|
2717
|
+
const criteriaRows = result.comparison.criteria.map((c) => {
|
|
2718
|
+
const colorA = criterionColor(c.scoreA);
|
|
2719
|
+
const colorB = criterionColor(c.scoreB);
|
|
2720
|
+
const widthA = c.scoreA * 10;
|
|
2721
|
+
const widthB = c.scoreB * 10;
|
|
2722
|
+
let deltaHtml;
|
|
2723
|
+
if (c.delta > 0) deltaHtml = `<span class="delta-positive">+${c.delta}</span>`;
|
|
2724
|
+
else if (c.delta < 0) deltaHtml = `<span class="delta-negative">${c.delta}</span>`;
|
|
2725
|
+
else deltaHtml = `<span class="delta-zero">0</span>`;
|
|
2726
|
+
return `<tr>
|
|
2727
|
+
<td>${c.id}</td>
|
|
2728
|
+
<td>${escapeHtml(c.criterion)}</td>
|
|
2729
|
+
<td>
|
|
2730
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
2731
|
+
<div class="criterion-bar"><div class="criterion-bar-fill" style="width:${widthA}%;background:${colorA}"></div></div>
|
|
2732
|
+
<span style="color:${colorA};font-weight:600">${c.scoreA}</span>
|
|
2733
|
+
</div>
|
|
2734
|
+
</td>
|
|
2735
|
+
<td>
|
|
2736
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
2737
|
+
<div class="criterion-bar"><div class="criterion-bar-fill" style="width:${widthB}%;background:${colorB}"></div></div>
|
|
2738
|
+
<span style="color:${colorB};font-weight:600">${c.scoreB}</span>
|
|
2739
|
+
</div>
|
|
2740
|
+
</td>
|
|
2741
|
+
<td style="text-align:center">${deltaHtml}</td>
|
|
2742
|
+
</tr>`;
|
|
2743
|
+
}).join("\n");
|
|
2744
|
+
const advantagesA = result.comparison.siteAAdvantages.length;
|
|
2745
|
+
const advantagesB = result.comparison.siteBAdvantages.length;
|
|
2746
|
+
const tied = result.comparison.tied.length;
|
|
2747
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2748
|
+
return `<!DOCTYPE html>
|
|
2749
|
+
<html lang="en">
|
|
2750
|
+
<head>
|
|
2751
|
+
<meta charset="UTF-8">
|
|
2752
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2753
|
+
<title>AEORank Comparison - ${domainA} vs ${domainB}</title>
|
|
2754
|
+
<style>${CSS}</style>
|
|
2755
|
+
</head>
|
|
2756
|
+
<body>
|
|
2757
|
+
<div class="container">
|
|
2758
|
+
<div class="header">
|
|
2759
|
+
<h1>AEO Comparison</h1>
|
|
2760
|
+
</div>
|
|
2761
|
+
|
|
2762
|
+
<div class="compare-header">
|
|
2763
|
+
<div class="compare-site">
|
|
2764
|
+
<h2>${domainA}</h2>
|
|
2765
|
+
${scoreCircleSvg(scoreA, 120)}
|
|
2766
|
+
</div>
|
|
2767
|
+
<div class="compare-vs">vs</div>
|
|
2768
|
+
<div class="compare-site">
|
|
2769
|
+
<h2>${domainB}</h2>
|
|
2770
|
+
${scoreCircleSvg(scoreB, 120)}
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>
|
|
2773
|
+
|
|
2774
|
+
<div class="summary-box">
|
|
2775
|
+
<div class="summary-stat">
|
|
2776
|
+
<div class="num" style="color:${scoreColor(scoreA)}">${advantagesA}</div>
|
|
2777
|
+
<div class="label">${domainA} leads</div>
|
|
2778
|
+
</div>
|
|
2779
|
+
<div class="summary-stat">
|
|
2780
|
+
<div class="num" style="color:${scoreColor(scoreB)}">${advantagesB}</div>
|
|
2781
|
+
<div class="label">${domainB} leads</div>
|
|
2782
|
+
</div>
|
|
2783
|
+
<div class="summary-stat">
|
|
2784
|
+
<div class="num" style="color:#999">${tied}</div>
|
|
2785
|
+
<div class="label">Tied</div>
|
|
2786
|
+
</div>
|
|
2787
|
+
</div>
|
|
2788
|
+
|
|
2789
|
+
<h2 class="section-title">Per-Criterion Comparison</h2>
|
|
2790
|
+
<table>
|
|
2791
|
+
<thead>
|
|
2792
|
+
<tr>
|
|
2793
|
+
<th>#</th>
|
|
2794
|
+
<th>Criterion</th>
|
|
2795
|
+
<th>${domainA}</th>
|
|
2796
|
+
<th>${domainB}</th>
|
|
2797
|
+
<th>Delta</th>
|
|
2798
|
+
</tr>
|
|
2799
|
+
</thead>
|
|
2800
|
+
<tbody>${criteriaRows}</tbody>
|
|
2801
|
+
</table>
|
|
2802
|
+
|
|
2803
|
+
${result.comparison.siteAAdvantages.length > 0 ? `
|
|
2804
|
+
<h2 class="section-title">${domainA} Advantages</h2>
|
|
2805
|
+
<div class="verdict">${result.comparison.siteAAdvantages.map((c) => escapeHtml(c)).join(", ")}</div>
|
|
2806
|
+
` : ""}
|
|
2807
|
+
|
|
2808
|
+
${result.comparison.siteBAdvantages.length > 0 ? `
|
|
2809
|
+
<h2 class="section-title">${domainB} Advantages</h2>
|
|
2810
|
+
<div class="verdict">${result.comparison.siteBAdvantages.map((c) => escapeHtml(c)).join(", ")}</div>
|
|
2811
|
+
` : ""}
|
|
2812
|
+
|
|
2813
|
+
${result.comparison.tied.length > 0 ? `
|
|
2814
|
+
<h2 class="section-title">Tied Criteria</h2>
|
|
2815
|
+
<div class="verdict">${result.comparison.tied.map((c) => escapeHtml(c)).join(", ")}</div>
|
|
2816
|
+
` : ""}
|
|
2817
|
+
|
|
2818
|
+
<div class="footer">Generated by AEORank - ${now}</div>
|
|
2819
|
+
</div>
|
|
2820
|
+
</body>
|
|
2821
|
+
</html>`;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2498
2824
|
// src/cli.ts
|
|
2499
|
-
var VERSION = "1.
|
|
2825
|
+
var VERSION = "1.2.0";
|
|
2500
2826
|
function printHelp() {
|
|
2501
2827
|
console.log(`
|
|
2502
2828
|
aeorank - AI Engine Optimization audit
|
|
2503
2829
|
|
|
2504
2830
|
USAGE
|
|
2505
2831
|
aeorank <domain> [options]
|
|
2832
|
+
aeorank <domain-a> <domain-b> [options] # comparison mode
|
|
2506
2833
|
|
|
2507
2834
|
OPTIONS
|
|
2508
2835
|
--json Output raw JSON to stdout
|
|
2509
2836
|
--summary Print human-readable scorecard
|
|
2837
|
+
--html Generate standalone HTML report file
|
|
2510
2838
|
--ci CI mode: JSON + exit 1 if score < threshold
|
|
2511
2839
|
--threshold <N> Score threshold for --ci (default: 70)
|
|
2512
2840
|
--no-headless Skip Puppeteer SPA rendering
|
|
@@ -2517,18 +2845,33 @@ function printHelp() {
|
|
|
2517
2845
|
EXAMPLES
|
|
2518
2846
|
aeorank example.com
|
|
2519
2847
|
aeorank example.com --json
|
|
2848
|
+
aeorank example.com --html
|
|
2520
2849
|
aeorank example.com --ci --threshold 80
|
|
2521
|
-
aeorank
|
|
2850
|
+
aeorank site-a.com site-b.com
|
|
2851
|
+
aeorank site-a.com site-b.com --html
|
|
2852
|
+
aeorank site-a.com site-b.com --json
|
|
2522
2853
|
`);
|
|
2523
2854
|
}
|
|
2524
2855
|
function parseArgs(argv) {
|
|
2856
|
+
const defaults = { domain: "", domainB: null, json: false, summary: false, html: false, ci: false, threshold: 70, noHeadless: false, noMultiPage: false, version: false, help: false };
|
|
2525
2857
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
2526
|
-
return {
|
|
2858
|
+
return { ...defaults, version: true };
|
|
2527
2859
|
}
|
|
2528
2860
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
2529
|
-
return {
|
|
2861
|
+
return { ...defaults, help: true };
|
|
2530
2862
|
}
|
|
2531
|
-
const
|
|
2863
|
+
const nonFlags = [];
|
|
2864
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2865
|
+
if (argv[i] === "--threshold") {
|
|
2866
|
+
i++;
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
if (!argv[i].startsWith("-")) {
|
|
2870
|
+
nonFlags.push(argv[i]);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
const domain = nonFlags[0] || "";
|
|
2874
|
+
const domainB = nonFlags[1] || null;
|
|
2532
2875
|
function getArg(name) {
|
|
2533
2876
|
const idx = argv.indexOf(`--${name}`);
|
|
2534
2877
|
if (idx === -1 || idx + 1 >= argv.length) return void 0;
|
|
@@ -2537,8 +2880,10 @@ function parseArgs(argv) {
|
|
|
2537
2880
|
const threshold = parseInt(getArg("threshold") || "70", 10);
|
|
2538
2881
|
return {
|
|
2539
2882
|
domain,
|
|
2883
|
+
domainB,
|
|
2540
2884
|
json: argv.includes("--json"),
|
|
2541
2885
|
summary: argv.includes("--summary"),
|
|
2886
|
+
html: argv.includes("--html"),
|
|
2542
2887
|
ci: argv.includes("--ci"),
|
|
2543
2888
|
threshold: isNaN(threshold) ? 70 : threshold,
|
|
2544
2889
|
noHeadless: argv.includes("--no-headless"),
|
|
@@ -2547,6 +2892,9 @@ function parseArgs(argv) {
|
|
|
2547
2892
|
help: false
|
|
2548
2893
|
};
|
|
2549
2894
|
}
|
|
2895
|
+
function sanitizeFilename(domain) {
|
|
2896
|
+
return domain.replace(/[^a-zA-Z0-9.-]/g, "-");
|
|
2897
|
+
}
|
|
2550
2898
|
function printSummary(result) {
|
|
2551
2899
|
const log = (msg) => process.stderr.write(msg + "\n");
|
|
2552
2900
|
log("");
|
|
@@ -2582,6 +2930,28 @@ function printSummary(result) {
|
|
|
2582
2930
|
log(` Completed in ${result.elapsed}s`);
|
|
2583
2931
|
log("");
|
|
2584
2932
|
}
|
|
2933
|
+
function printComparisonSummary(result) {
|
|
2934
|
+
const log = (msg) => process.stderr.write(msg + "\n");
|
|
2935
|
+
const { siteA, siteB, comparison } = result;
|
|
2936
|
+
log("");
|
|
2937
|
+
log(` ${siteA.site} (${siteA.overallScore}) vs ${siteB.site} (${siteB.overallScore})`);
|
|
2938
|
+
log(" " + "\u2500".repeat(60));
|
|
2939
|
+
log("");
|
|
2940
|
+
const maxNameLen = Math.max(...comparison.criteria.map((c) => c.criterion.length), 20);
|
|
2941
|
+
log(` ${"#".padStart(3)} ${"Criterion".padEnd(maxNameLen)} ${siteA.site.padStart(10)} ${siteB.site.padStart(10)} ${"Delta".padStart(5)}`);
|
|
2942
|
+
log(" " + "\u2500".repeat(3 + 2 + maxNameLen + 2 + 10 + 2 + 10 + 2 + 5));
|
|
2943
|
+
for (const c of comparison.criteria) {
|
|
2944
|
+
const barA = "\u2588".repeat(c.scoreA) + "\u2591".repeat(10 - c.scoreA);
|
|
2945
|
+
const barB = "\u2588".repeat(c.scoreB) + "\u2591".repeat(10 - c.scoreB);
|
|
2946
|
+
const delta = c.delta > 0 ? `+${c.delta}` : c.delta === 0 ? " 0" : `${c.delta}`;
|
|
2947
|
+
log(` ${c.id.toString().padStart(3)} ${c.criterion.padEnd(maxNameLen)} ${barA} ${c.scoreA.toString().padStart(2)} ${barB} ${c.scoreB.toString().padStart(2)} ${delta.padStart(5)}`);
|
|
2948
|
+
}
|
|
2949
|
+
log("");
|
|
2950
|
+
log(` ${siteA.site} leads in ${comparison.siteAAdvantages.length} criteria`);
|
|
2951
|
+
log(` ${siteB.site} leads in ${comparison.siteBAdvantages.length} criteria`);
|
|
2952
|
+
log(` Tied: ${comparison.tied.length} criteria`);
|
|
2953
|
+
log("");
|
|
2954
|
+
}
|
|
2585
2955
|
async function main() {
|
|
2586
2956
|
const args = parseArgs(process.argv.slice(2));
|
|
2587
2957
|
if (args.version) {
|
|
@@ -2599,12 +2969,33 @@ async function main() {
|
|
|
2599
2969
|
}
|
|
2600
2970
|
const log = (msg) => process.stderr.write(`[aeorank] ${msg}
|
|
2601
2971
|
`);
|
|
2972
|
+
const auditOptions = { noHeadless: args.noHeadless, noMultiPage: args.noMultiPage };
|
|
2602
2973
|
try {
|
|
2974
|
+
if (args.domainB) {
|
|
2975
|
+
log(`Comparing ${args.domain} vs ${args.domainB}...`);
|
|
2976
|
+
const result2 = await compare(args.domain, args.domainB, auditOptions);
|
|
2977
|
+
log(`${args.domain}: ${result2.siteA.overallScore}/100 (${result2.siteA.elapsed}s)`);
|
|
2978
|
+
log(`${args.domainB}: ${result2.siteB.overallScore}/100 (${result2.siteB.elapsed}s)`);
|
|
2979
|
+
if (args.json || args.ci) {
|
|
2980
|
+
process.stdout.write(JSON.stringify(result2, null, 2) + "\n");
|
|
2981
|
+
}
|
|
2982
|
+
if (args.summary || !args.json && !args.ci) {
|
|
2983
|
+
printComparisonSummary(result2);
|
|
2984
|
+
}
|
|
2985
|
+
if (args.html) {
|
|
2986
|
+
const filename = `aeorank-${sanitizeFilename(args.domain)}-vs-${sanitizeFilename(args.domainB)}.html`;
|
|
2987
|
+
const html = generateComparisonHtmlReport(result2);
|
|
2988
|
+
writeFileSync(filename, html, "utf-8");
|
|
2989
|
+
log(`HTML report: ${filename}`);
|
|
2990
|
+
}
|
|
2991
|
+
if (args.ci && result2.siteA.overallScore < args.threshold) {
|
|
2992
|
+
log(`FAIL: ${args.domain} score ${result2.siteA.overallScore} is below threshold ${args.threshold}`);
|
|
2993
|
+
process.exit(1);
|
|
2994
|
+
}
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2603
2997
|
log(`Auditing ${args.domain}...`);
|
|
2604
|
-
const result = await audit(args.domain,
|
|
2605
|
-
noHeadless: args.noHeadless,
|
|
2606
|
-
noMultiPage: args.noMultiPage
|
|
2607
|
-
});
|
|
2998
|
+
const result = await audit(args.domain, auditOptions);
|
|
2608
2999
|
log(`Score: ${result.overallScore}/100 (${result.elapsed}s)`);
|
|
2609
3000
|
if (args.json || args.ci) {
|
|
2610
3001
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
@@ -2612,6 +3003,12 @@ async function main() {
|
|
|
2612
3003
|
if (args.summary || !args.json && !args.ci) {
|
|
2613
3004
|
printSummary(result);
|
|
2614
3005
|
}
|
|
3006
|
+
if (args.html) {
|
|
3007
|
+
const filename = `aeorank-${sanitizeFilename(args.domain)}.html`;
|
|
3008
|
+
const html = generateHtmlReport(result);
|
|
3009
|
+
writeFileSync(filename, html, "utf-8");
|
|
3010
|
+
log(`HTML report: ${filename}`);
|
|
3011
|
+
}
|
|
2615
3012
|
if (args.ci && result.overallScore < args.threshold) {
|
|
2616
3013
|
log(`FAIL: Score ${result.overallScore} is below threshold ${args.threshold}`);
|
|
2617
3014
|
process.exit(1);
|