aeorank 1.4.0 → 1.6.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
@@ -2504,12 +2504,345 @@ async function fetchMultiPageData(siteData, options) {
2504
2504
  return added;
2505
2505
  }
2506
2506
 
2507
+ // src/page-scorer.ts
2508
+ var PAGE_CRITERIA = {
2509
+ schema_markup: { weight: 0.15, label: "Schema.org Structured Data" },
2510
+ qa_content_format: { weight: 0.15, label: "Q&A Content Format" },
2511
+ clean_html: { weight: 0.1, label: "Clean, Crawlable HTML" },
2512
+ faq_section: { weight: 0.1, label: "FAQ Section Content" },
2513
+ original_data: { weight: 0.1, label: "Original Data & Expert Content" },
2514
+ query_answer_alignment: { weight: 0.08, label: "Query-Answer Alignment" },
2515
+ content_freshness: { weight: 0.07, label: "Content Freshness Signals" },
2516
+ table_list_extractability: { weight: 0.07, label: "Table & List Extractability" },
2517
+ direct_answer_density: { weight: 0.07, label: "Direct Answer Paragraphs" },
2518
+ semantic_html: { weight: 0.05, label: "Semantic HTML5 & Accessibility" },
2519
+ fact_density: { weight: 0.05, label: "Fact & Data Density" },
2520
+ definition_patterns: { weight: 0.04, label: "Definition Patterns" },
2521
+ canonical_url: { weight: 0.04, label: "Canonical URL Strategy" },
2522
+ visible_date_signal: { weight: 0.04, label: "Visible Date Signal" }
2523
+ };
2524
+ function extractJsonLdBlocks(html) {
2525
+ const blocks = [];
2526
+ const regex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
2527
+ let match;
2528
+ while ((match = regex.exec(html)) !== null) {
2529
+ blocks.push(match[1]);
2530
+ }
2531
+ return blocks;
2532
+ }
2533
+ function extractTypesFromJsonLd(blocks) {
2534
+ const types = /* @__PURE__ */ new Set();
2535
+ for (const block of blocks) {
2536
+ const typeMatches = block.match(/"@type"\s*:\s*"([^"]+)"/g) || [];
2537
+ for (const m of typeMatches) {
2538
+ const t = m.match(/"@type"\s*:\s*"([^"]+)"/);
2539
+ if (t) types.add(t[1]);
2540
+ }
2541
+ }
2542
+ return types;
2543
+ }
2544
+ function getTextContent(html) {
2545
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
2546
+ }
2547
+ function extractQuestionHeadings2(html) {
2548
+ const headings = html.match(/<h[2-3][^>]*>([\s\S]*?)<\/h[2-3]>/gi) || [];
2549
+ const questions = [];
2550
+ for (const h of headings) {
2551
+ const text = h.replace(/<[^>]*>/g, "").trim();
2552
+ if (/\?$/.test(text) || /^(what|how|why|when|where|who|which|can|do|does|is|are|should|will)\b/i.test(text)) {
2553
+ questions.push(text);
2554
+ }
2555
+ }
2556
+ return questions;
2557
+ }
2558
+ function countAnsweredQuestions(html) {
2559
+ const questions = extractQuestionHeadings2(html);
2560
+ if (questions.length === 0) return { total: 0, answered: 0 };
2561
+ let answered = 0;
2562
+ for (const q of questions) {
2563
+ const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2564
+ const pattern = new RegExp(escaped + "[\\s\\S]*?</h[2-3]>\\s*<p[^>]*>([\\s\\S]*?)</p>", "i");
2565
+ const match = html.match(pattern);
2566
+ if (match && match[1].replace(/<[^>]*>/g, "").trim().length >= 20) {
2567
+ answered++;
2568
+ }
2569
+ }
2570
+ return { total: questions.length, answered };
2571
+ }
2572
+ function cap(value, max) {
2573
+ return Math.min(value, max);
2574
+ }
2575
+ function scoreSchemaMarkup(html) {
2576
+ const blocks = extractJsonLdBlocks(html);
2577
+ if (blocks.length === 0) return 0;
2578
+ let score = 3;
2579
+ const types = extractTypesFromJsonLd(blocks);
2580
+ const knownTypes = [
2581
+ "Organization",
2582
+ "LocalBusiness",
2583
+ "Article",
2584
+ "FAQPage",
2585
+ "Product",
2586
+ "WebPage",
2587
+ "BreadcrumbList",
2588
+ "HowTo",
2589
+ "Person",
2590
+ "WebSite",
2591
+ "BlogPosting",
2592
+ "Service"
2593
+ ];
2594
+ let knownCount = 0;
2595
+ for (const t of types) {
2596
+ if (knownTypes.includes(t)) knownCount++;
2597
+ }
2598
+ score += cap(knownCount * 2, 4);
2599
+ if (types.has("Organization") || types.has("LocalBusiness")) score += 2;
2600
+ if (types.has("FAQPage")) score += 1;
2601
+ return cap(score, 10);
2602
+ }
2603
+ function scoreQAFormat(html) {
2604
+ const questions = extractQuestionHeadings2(html);
2605
+ let score = 0;
2606
+ if (questions.length >= 10) score += 5;
2607
+ else if (questions.length >= 3) score += 3;
2608
+ else if (questions.length >= 1) score += 1;
2609
+ const { answered } = countAnsweredQuestions(html);
2610
+ if (answered >= 1) score += 3;
2611
+ const h1Matches = html.match(/<h1[\s>]/gi) || [];
2612
+ if (h1Matches.length === 1) score += 2;
2613
+ return cap(score, 10);
2614
+ }
2615
+ function scoreCleanHtml(html) {
2616
+ let score = 0;
2617
+ const semantics = ["<main", "<article", "<section"];
2618
+ let semCount = 0;
2619
+ for (const tag of semantics) {
2620
+ if (html.toLowerCase().includes(tag)) semCount++;
2621
+ }
2622
+ score += cap(semCount, 3);
2623
+ const h1Matches = html.match(/<h1[\s>]/gi) || [];
2624
+ if (h1Matches.length === 1) score += 2;
2625
+ const text = getTextContent(html);
2626
+ if (text.length > 500) score += 3;
2627
+ const hasTitle = /<title[^>]*>[^<]+<\/title>/i.test(html);
2628
+ const hasDesc = /<meta\s[^>]*name=["']description["'][^>]*content=["'][^"']+["']/i.test(html) || /<meta\s[^>]*content=["'][^"']+["'][^>]*name=["']description["']/i.test(html);
2629
+ if (hasTitle && hasDesc) score += 2;
2630
+ return cap(score, 10);
2631
+ }
2632
+ function scoreFaqSection(html) {
2633
+ let score = 0;
2634
+ const lowerHtml = html.toLowerCase();
2635
+ if (/frequently\s*asked|faq/i.test(html)) score += 2;
2636
+ const blocks = extractJsonLdBlocks(html);
2637
+ const types = extractTypesFromJsonLd(blocks);
2638
+ if (types.has("FAQPage")) score += 3;
2639
+ const questions = extractQuestionHeadings2(html);
2640
+ if (questions.length >= 10) score += 1;
2641
+ if (/<details[\s>]/i.test(html) || /accordion|collapsible|toggle/i.test(lowerHtml)) score += 1;
2642
+ return cap(score, 10);
2643
+ }
2644
+ function scoreOriginalData(html) {
2645
+ let score = 0;
2646
+ const text = getTextContent(html);
2647
+ if (/\b(our (study|analysis|research|survey|data|findings))\b/i.test(text)) {
2648
+ score += 3;
2649
+ } else if (/\d+(\.\d+)?%|\$[\d,.]+|\b\d{1,3}(,\d{3})+\b/.test(text)) {
2650
+ score += 1;
2651
+ }
2652
+ if (/\bcase\s+stud(y|ies)\b/i.test(text) && /\d+(\.\d+)?%|\$[\d,.]+/.test(text)) {
2653
+ score += 3;
2654
+ } else if (/\bcase\s+stud(y|ies)\b/i.test(text)) {
2655
+ score += 1;
2656
+ }
2657
+ if (/\baccording\s+to\b|\bexpert|\b(Ph\.?D|MD|professor|analyst|researcher)\b/i.test(text)) {
2658
+ score += 2;
2659
+ }
2660
+ if (/href=["'][^"']*\/blog\b/i.test(html)) {
2661
+ score += 2;
2662
+ }
2663
+ return cap(score, 10);
2664
+ }
2665
+ function scoreQueryAnswerAlignment(html) {
2666
+ const { total, answered } = countAnsweredQuestions(html);
2667
+ if (total === 0) return 5;
2668
+ const ratio = answered / total;
2669
+ if (ratio >= 0.8) return 10;
2670
+ if (ratio >= 0.5) return 7;
2671
+ if (answered > 0) return 4;
2672
+ return 0;
2673
+ }
2674
+ function scoreContentFreshness(html) {
2675
+ let score = 0;
2676
+ const blocks = extractJsonLdBlocks(html);
2677
+ const allJsonLd = blocks.join(" ");
2678
+ if (/datePublished|dateModified/i.test(allJsonLd)) score += 3;
2679
+ const timeElements = html.match(/<time[\s>]/gi) || [];
2680
+ if (timeElements.length >= 2) score += 3;
2681
+ else if (timeElements.length === 1) score += 1;
2682
+ if (/<meta\s[^>]*property=["']article:(published_time|modified_time)["']/i.test(html)) score += 2;
2683
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
2684
+ const yearPattern = new RegExp(`\\b(${currentYear}|${currentYear - 1})\\b`);
2685
+ if (yearPattern.test(html)) score += 2;
2686
+ return cap(score, 10);
2687
+ }
2688
+ function scoreTableListExtractability(html) {
2689
+ let score = 0;
2690
+ const tablesWithHeaders = html.match(/<table[\s\S]*?<th[\s>]/gi) || [];
2691
+ if (tablesWithHeaders.length >= 2) score += 4;
2692
+ else if (tablesWithHeaders.length === 1) score += 3;
2693
+ if (tablesWithHeaders.length === 0 && /<table[\s>]/i.test(html)) score += 1;
2694
+ if (/<ol[\s>]/i.test(html)) score += 2;
2695
+ if (/<ul[\s>]/i.test(html)) score += 2;
2696
+ const listItems = html.match(/<li[\s>]/gi) || [];
2697
+ if (listItems.length >= 10) score += 1;
2698
+ if (/<dl[\s>]/i.test(html)) score += 1;
2699
+ return cap(score, 10);
2700
+ }
2701
+ function scoreDirectAnswerDensity(html) {
2702
+ let score = 0;
2703
+ const { answered } = countAnsweredQuestions(html);
2704
+ if (answered >= 3) score += 6;
2705
+ else if (answered >= 1) score += 3;
2706
+ const paragraphs = html.match(/<p[^>]*>([\s\S]*?)<\/p>/gi) || [];
2707
+ let snippetCount = 0;
2708
+ for (const p of paragraphs) {
2709
+ const text = p.replace(/<[^>]*>/g, "").trim();
2710
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
2711
+ if (words >= 40 && words <= 150) snippetCount++;
2712
+ }
2713
+ if (snippetCount >= 3) score += 2;
2714
+ else if (snippetCount >= 1) score += 1;
2715
+ const directOpeners = getTextContent(html).match(/\b(yes|no|in short|the answer is|simply put|in summary)\b/gi) || [];
2716
+ if (directOpeners.length >= 2) score += 2;
2717
+ return cap(score, 10);
2718
+ }
2719
+ function scoreSemanticHtml(html) {
2720
+ let score = 0;
2721
+ const lowerHtml = html.toLowerCase();
2722
+ const elements = ["<main", "<article", "<time", "<nav", "<header", "<footer"];
2723
+ let count = 0;
2724
+ for (const el of elements) {
2725
+ if (lowerHtml.includes(el)) count++;
2726
+ }
2727
+ score += cap(Math.floor(count * 0.7), 4);
2728
+ const imgTags = html.match(/<img\s[^>]*>/gi) || [];
2729
+ if (imgTags.length > 0) {
2730
+ let withAlt = 0;
2731
+ for (const img of imgTags) {
2732
+ if (/\salt=["'][^"']*["']/i.test(img)) withAlt++;
2733
+ }
2734
+ if (withAlt / imgTags.length >= 0.8) score += 2;
2735
+ }
2736
+ if (/<html[^>]*\slang=["'][^"']+["']/i.test(html)) score += 2;
2737
+ if (/\baria-/i.test(html)) score += 2;
2738
+ return cap(score, 10);
2739
+ }
2740
+ function scoreFactDensity(html) {
2741
+ let score = 0;
2742
+ const text = getTextContent(html);
2743
+ const numericPatterns = text.match(/\d+(\.\d+)?%|\$[\d,.]+|\b\d{1,3}(,\d{3})+\b|\b\d+\s*(million|billion|thousand|users|customers|employees)\b/gi) || [];
2744
+ if (numericPatterns.length >= 6) score += 5;
2745
+ else if (numericPatterns.length >= 3) score += 3;
2746
+ else if (numericPatterns.length >= 1) score += 1;
2747
+ const years = /* @__PURE__ */ new Set();
2748
+ const yearMatches = text.match(/\b(19|20)\d{2}\b/g) || [];
2749
+ for (const y of yearMatches) years.add(y);
2750
+ if (years.size >= 2) score += 2;
2751
+ else if (years.size === 1) score += 1;
2752
+ if (/\baccording to\b|\bsource:\s|\bcited\b|\breported by\b/i.test(text)) score += 2;
2753
+ const units = text.match(/\b\d+\s*(kg|lb|miles|km|hours|minutes|days|months|years|GB|MB|TB)\b/gi) || [];
2754
+ if (units.length >= 2) score += 1;
2755
+ return cap(score, 10);
2756
+ }
2757
+ function scoreDefinitionPatterns(html) {
2758
+ let score = 0;
2759
+ const text = getTextContent(html);
2760
+ const defPatterns = text.match(/\b(is a|is an|refers to|defined as|means that|also known as|abbreviated as)\b/gi) || [];
2761
+ if (defPatterns.length >= 3) score += 5;
2762
+ else if (defPatterns.length >= 1) score += 3;
2763
+ const early = text.slice(0, 2e3);
2764
+ if (/\b(is a|is an|refers to|defined as)\b/i.test(early)) score += 2;
2765
+ if (/<dfn[\s>]/i.test(html) || /<abbr[\s>]/i.test(html)) score += 1;
2766
+ if (/<dl[\s>]/i.test(html) || /glossary/i.test(html)) score += 2;
2767
+ return cap(score, 10);
2768
+ }
2769
+ function scoreCanonicalUrl(html, url) {
2770
+ let score = 0;
2771
+ const canonicalMatch = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']+)["']/i) || html.match(/<link[^>]*href=["']([^"']+)["'][^>]*rel=["']canonical["']/i);
2772
+ if (!canonicalMatch) return 0;
2773
+ score += 4;
2774
+ const canonicalHref = canonicalMatch[1];
2775
+ if (url) {
2776
+ try {
2777
+ const canonicalUrl = new URL(canonicalHref, url);
2778
+ const pageUrl = new URL(url);
2779
+ if (canonicalUrl.pathname === pageUrl.pathname && canonicalUrl.hostname === pageUrl.hostname) {
2780
+ score += 3;
2781
+ }
2782
+ } catch {
2783
+ }
2784
+ }
2785
+ if (canonicalHref.startsWith("https://")) score += 2;
2786
+ const allCanonicals = html.match(/<link[^>]*rel=["']canonical["'][^>]*>/gi) || [];
2787
+ if (allCanonicals.length === 1) score += 1;
2788
+ return cap(score, 10);
2789
+ }
2790
+ function scoreVisibleDateSignal(html) {
2791
+ let score = 0;
2792
+ const timeWithDatetime = html.match(/<time[^>]*datetime=["'][^"']+["'][^>]*>[^<]+<\/time>/gi) || [];
2793
+ if (timeWithDatetime.length > 0) score += 5;
2794
+ const blocks = extractJsonLdBlocks(html);
2795
+ const allJsonLd = blocks.join(" ");
2796
+ if (/datePublished|dateModified/i.test(allJsonLd)) score += 3;
2797
+ if (/<meta\s[^>]*property=["']article:(published_time|modified_time)["']/i.test(html)) score += 2;
2798
+ const modifiedMatch = allJsonLd.match(/"dateModified"\s*:\s*"([^"]+)"/i);
2799
+ if (modifiedMatch) {
2800
+ try {
2801
+ const modified = new Date(modifiedMatch[1]);
2802
+ const daysDiff = (Date.now() - modified.getTime()) / (1e3 * 60 * 60 * 24);
2803
+ if (daysDiff <= 180) score += 1;
2804
+ } catch {
2805
+ }
2806
+ }
2807
+ return cap(score, 10);
2808
+ }
2809
+ var SCORING_FUNCTIONS = {
2810
+ schema_markup: scoreSchemaMarkup,
2811
+ qa_content_format: scoreQAFormat,
2812
+ clean_html: scoreCleanHtml,
2813
+ faq_section: scoreFaqSection,
2814
+ original_data: scoreOriginalData,
2815
+ query_answer_alignment: scoreQueryAnswerAlignment,
2816
+ content_freshness: scoreContentFreshness,
2817
+ table_list_extractability: scoreTableListExtractability,
2818
+ direct_answer_density: scoreDirectAnswerDensity,
2819
+ semantic_html: scoreSemanticHtml,
2820
+ fact_density: scoreFactDensity,
2821
+ definition_patterns: scoreDefinitionPatterns,
2822
+ canonical_url: scoreCanonicalUrl,
2823
+ visible_date_signal: scoreVisibleDateSignal
2824
+ };
2825
+ function scorePage(html, url) {
2826
+ let totalWeight = 0;
2827
+ let weightedSum = 0;
2828
+ const criterionScores = [];
2829
+ for (const [criterion, { weight, label }] of Object.entries(PAGE_CRITERIA)) {
2830
+ const fn = SCORING_FUNCTIONS[criterion];
2831
+ const score = fn(html, url);
2832
+ criterionScores.push({ criterion, criterion_label: label, score, weight });
2833
+ weightedSum += score / 10 * weight * 100;
2834
+ totalWeight += weight;
2835
+ }
2836
+ const aeoScore = totalWeight === 0 ? 0 : Math.round(weightedSum / totalWeight);
2837
+ return { aeoScore, criterionScores };
2838
+ }
2839
+
2507
2840
  // src/page-analyzer.ts
2508
2841
  function extractTitle(html) {
2509
2842
  const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
2510
2843
  return match ? match[1].replace(/\s+/g, " ").trim() : "";
2511
2844
  }
2512
- function getTextContent(html) {
2845
+ function getTextContent2(html) {
2513
2846
  return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
2514
2847
  }
2515
2848
  function countWords(text) {
@@ -2662,7 +2995,7 @@ function checkHasQuestionHeadings(html) {
2662
2995
  }
2663
2996
  function analyzePage(html, url, category) {
2664
2997
  const title = extractTitle(html);
2665
- const textContent = getTextContent(html);
2998
+ const textContent = getTextContent2(html);
2666
2999
  const wordCount = countWords(textContent);
2667
3000
  const issues = [];
2668
3001
  const strengths = [];
@@ -2688,7 +3021,8 @@ function analyzePage(html, url, category) {
2688
3021
  for (const result of strengthChecks) {
2689
3022
  if (result) strengths.push(result);
2690
3023
  }
2691
- return { url, title, category, wordCount, issues, strengths };
3024
+ const { aeoScore, criterionScores } = scorePage(html, url);
3025
+ return { url, title, category, wordCount, issues, strengths, aeoScore, criterionScores };
2692
3026
  }
2693
3027
  function analyzeAllPages(siteData) {
2694
3028
  const reviews = [];
@@ -2937,17 +3271,22 @@ function generateHtmlReport(result) {
2937
3271
  <td>${escapeHtml(opp.description)}</td>
2938
3272
  </tr>`;
2939
3273
  }).join("\n");
2940
- const pagesRows = (result.pagesReviewed || []).map((page) => {
3274
+ const pagesReviewed = result.pagesReviewed || [];
3275
+ const pagesRows = pagesReviewed.map((page) => {
2941
3276
  const issueCount = page.issues.length;
2942
3277
  const strengthCount = page.strengths.length;
3278
+ const aeoDisplay = page.aeoScore != null ? `<span style="font-weight:600;color:${scoreColor(page.aeoScore)}">${page.aeoScore}</span>` : "-";
2943
3279
  return `<tr>
2944
3280
  <td>${escapeHtml(page.url)}</td>
2945
3281
  <td>${escapeHtml(page.category)}</td>
2946
3282
  <td>${page.wordCount}</td>
3283
+ <td>${aeoDisplay}</td>
2947
3284
  <td>${issueCount}</td>
2948
3285
  <td>${strengthCount}</td>
2949
3286
  </tr>`;
2950
3287
  }).join("\n");
3288
+ const scoredPages = pagesReviewed.filter((p) => p.aeoScore != null);
3289
+ const avgPageScore = scoredPages.length > 0 ? Math.round(scoredPages.reduce((sum, p) => sum + p.aeoScore, 0) / scoredPages.length) : null;
2951
3290
  const now = (/* @__PURE__ */ new Date()).toISOString();
2952
3291
  return `<!DOCTYPE html>
2953
3292
  <html lang="en">
@@ -2983,10 +3322,11 @@ function generateHtmlReport(result) {
2983
3322
  </table>
2984
3323
  ` : ""}
2985
3324
 
2986
- ${(result.pagesReviewed || []).length > 0 ? `
2987
- <h2 class="section-title">Pages Reviewed (${(result.pagesReviewed || []).length})</h2>
3325
+ ${pagesReviewed.length > 0 ? `
3326
+ <h2 class="section-title">Pages Reviewed (${pagesReviewed.length})</h2>
3327
+ ${avgPageScore != null ? `<div class="summary-box"><div class="summary-stat"><div class="num" style="color:${scoreColor(avgPageScore)}">${avgPageScore}</div><div class="label">Avg Page AEO Score</div></div></div>` : ""}
2988
3328
  <table>
2989
- <thead><tr><th>URL</th><th>Category</th><th>Words</th><th>Issues</th><th>Strengths</th></tr></thead>
3329
+ <thead><tr><th>URL</th><th>Category</th><th>Words</th><th>AEO Score</th><th>Issues</th><th>Strengths</th></tr></thead>
2990
3330
  <tbody>${pagesRows}</tbody>
2991
3331
  </table>
2992
3332
  ` : ""}
@@ -3111,7 +3451,7 @@ function generateComparisonHtmlReport(result) {
3111
3451
  }
3112
3452
 
3113
3453
  // src/cli.ts
3114
- var VERSION = "1.4.0";
3454
+ var VERSION = "1.5.0";
3115
3455
  function printHelp() {
3116
3456
  console.log(`
3117
3457
  aeorank - AI Engine Optimization audit
@@ -3221,7 +3561,27 @@ function printSummary(result) {
3221
3561
  const cat = page.category.charAt(0).toUpperCase() + page.category.slice(1);
3222
3562
  const issueCount = page.issues.length;
3223
3563
  const issueLabel = issueCount === 0 ? "0 issues" : issueCount === 1 ? "1 issue" : `${issueCount} issues`;
3224
- log(` ${cat.padEnd(10)} ${page.url.padEnd(50)} ${issueLabel}`);
3564
+ const aeoLabel = page.aeoScore != null ? ` [AEO: ${page.aeoScore}]` : "";
3565
+ log(` ${cat.padEnd(10)} ${page.url.padEnd(50)} ${issueLabel}${aeoLabel}`);
3566
+ }
3567
+ const scored = result.pagesReviewed.filter((p) => p.aeoScore != null);
3568
+ if (scored.length > 0) {
3569
+ const avg = Math.round(scored.reduce((sum, p) => sum + p.aeoScore, 0) / scored.length);
3570
+ const sorted = [...scored].sort((a, b) => b.aeoScore - a.aeoScore);
3571
+ const top = sorted[0];
3572
+ const bottom = sorted[sorted.length - 1];
3573
+ log("");
3574
+ log(` Average page AEO score: ${avg}/100`);
3575
+ try {
3576
+ log(` Top: ${new URL(top.url).pathname} (${top.aeoScore})`);
3577
+ } catch {
3578
+ log(` Top: ${top.url} (${top.aeoScore})`);
3579
+ }
3580
+ try {
3581
+ log(` Bottom: ${new URL(bottom.url).pathname} (${bottom.aeoScore})`);
3582
+ } catch {
3583
+ log(` Bottom: ${bottom.url} (${bottom.aeoScore})`);
3584
+ }
3225
3585
  }
3226
3586
  log("");
3227
3587
  }