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/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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.0.0";
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 example.com --summary --no-headless
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 { domain: "", json: false, summary: false, ci: false, threshold: 70, noHeadless: false, noMultiPage: false, version: true, help: false };
2858
+ return { ...defaults, version: true };
2527
2859
  }
2528
2860
  if (argv.includes("--help") || argv.includes("-h")) {
2529
- return { domain: "", json: false, summary: false, ci: false, threshold: 70, noHeadless: false, noMultiPage: false, version: false, help: true };
2861
+ return { ...defaults, help: true };
2530
2862
  }
2531
- const domain = argv.find((a) => !a.startsWith("-")) || "";
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);