aeo-ready 1.1.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/bin/cli.js CHANGED
@@ -2,12 +2,6 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { scan } from "../src/scan.js";
5
- import { fix } from "../src/fix/index.js";
6
- import {
7
- track,
8
- printTrackResults,
9
- getAvailableProviders,
10
- } from "../src/track/index.js";
11
5
  import { readFileSync } from "fs";
12
6
  import { dirname, join } from "path";
13
7
  import { fileURLToPath } from "url";
@@ -21,66 +15,33 @@ const program = new Command();
21
15
 
22
16
  program
23
17
  .name("aeo-ready")
24
- .description("Is your site AEO ready? Two scorecards, one score.")
18
+ .description("AEO benchmark aggregator. One scan, every score.")
25
19
  .version(pkg.version);
26
20
 
27
21
  program
28
22
  .command("scan [url]")
29
- .description("Run a full audit (Agent Readiness + AI Visibility)")
30
- .option("--fix", "Fix issues and show before/after")
31
- .option("--track", "Query AI models to see what they say about you")
32
- .option("--company <name>", "Company name for --track")
33
- .option("--category <cat>", "Industry category for --track")
23
+ .description("Run all AEO benchmarks against a URL")
34
24
  .option("--json", "Output results as JSON")
35
25
  .option(
36
26
  "--threshold <number>",
37
- "Minimum score to pass (exit 1 if below)",
27
+ "Minimum average score to pass (exit 1 if below)",
38
28
  parseInt,
39
29
  )
40
- .option("--no-benchmark", "Skip agentic-seo benchmark")
41
30
  .action(async (url, opts) => {
42
31
  try {
43
- const isUrl = url && url.startsWith("http");
44
- const dir = isUrl ? null : process.cwd();
45
- const quiet = opts.fix && dir;
46
- const result = await scan({
47
- url: isUrl ? url : null,
48
- dir,
49
- json: opts.json || quiet,
50
- benchmark: quiet ? false : opts.benchmark !== false,
51
- });
52
-
53
- if (opts.track) {
54
- const providers = getAvailableProviders();
55
- if (providers.length === 0) {
56
- console.log("\n --track needs API keys. Set one or more:");
57
- console.log(" ANTHROPIC_API_KEY (Claude)");
58
- console.log(" OPENAI_API_KEY (ChatGPT)");
59
- console.log(" GOOGLE_API_KEY (Gemini)\n");
60
- } else {
61
- const trackResults = await track(result, {
62
- company: opts.company || null,
63
- category: opts.category || null,
64
- });
65
- printTrackResults(trackResults, opts.company);
66
- }
32
+ if (url && !url.startsWith("http")) url = `https://${url}`;
33
+ if (!url) {
34
+ console.error(" Usage: npx aeo-ready scan <url>");
35
+ process.exit(1);
67
36
  }
68
37
 
69
- if (opts.fix && dir) {
70
- const rescan = () =>
71
- scan({ url: null, dir, json: true, benchmark: false });
72
- await fix(result, dir, rescan);
73
- } else if (opts.fix && !dir) {
74
- console.log(
75
- "\n --fix requires repo mode (no --url). Files are written to current directory.\n",
76
- );
77
- }
38
+ const result = await scan({ url, json: opts.json || false });
78
39
 
79
40
  if (opts.json) {
80
41
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
81
42
  }
82
43
 
83
- if (opts.threshold && result.score < opts.threshold) {
44
+ if (opts.threshold && result.averageScore < opts.threshold) {
84
45
  process.exit(1);
85
46
  }
86
47
  } catch (err) {
@@ -89,18 +50,4 @@ program
89
50
  }
90
51
  });
91
52
 
92
- program
93
- .command("init")
94
- .description("Create .agent-web/config.yaml for citation tracking")
95
- .action(() => {
96
- console.log("Coming in v2.0 — citation tracking config");
97
- });
98
-
99
- program
100
- .command("history")
101
- .description("List past scan results")
102
- .action(() => {
103
- console.log("Coming in v1.2 — scan history");
104
- });
105
-
106
53
  program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aeo-ready",
3
- "version": "1.1.0",
4
- "description": "Is your site AEO ready? Two scorecards: Agent Readiness + AI Visibility. One scan, one score.",
3
+ "version": "1.2.0",
4
+ "description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, and Fern in one report.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "aeo-ready": "./bin/cli.js"
@@ -2,11 +2,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { getHistory } from "../history/index.js";
4
4
  import { renderOverallScore } from "./sections/overall-score.js";
5
- import { renderAgentReadiness } from "./sections/agent-readiness.js";
6
- import { renderAiVisibility } from "./sections/ai-visibility.js";
7
5
  import { renderHistoryTable } from "./sections/history-table.js";
8
6
  import { renderTrendChart } from "./sections/trend-chart.js";
9
- import { renderRecommendations } from "./sections/recommendations.js";
7
+ import { renderBenchmarkDetails } from "./sections/benchmark-details.js";
10
8
 
11
9
  const DASHBOARD_DIR = ".aeo-ready";
12
10
  const DASHBOARD_FILE = "dashboard.html";
@@ -19,12 +17,10 @@ export async function generateDashboard(scanResult, dir, opts = {}) {
19
17
  const history = getHistory(dir);
20
18
 
21
19
  const sections = {
22
- "overall-score": renderOverallScore(scanResult, opts.beforeResult || null),
23
- "agent-readiness-scorecard": renderAgentReadiness(scanResult),
24
- "ai-visibility-scorecard": renderAiVisibility(scanResult),
25
- "history-table": renderHistoryTable(history),
20
+ "overall-score": renderOverallScore(scanResult),
21
+ "benchmark-details": renderBenchmarkDetails(scanResult),
26
22
  "trend-chart": renderTrendChart(history),
27
- recommendations: renderRecommendations(scanResult),
23
+ "history-table": renderHistoryTable(history),
28
24
  };
29
25
 
30
26
  let html;
@@ -54,13 +50,12 @@ function replaceSection(html, name, content) {
54
50
 
55
51
  function buildFullDashboard(sections, scanResult) {
56
52
  const timestamp = new Date().toISOString().slice(0, 10);
57
- const target = scanResult.target || "Local";
58
53
 
59
54
  return `<!DOCTYPE html>
60
55
  <html lang="en">
61
56
  <head>
62
57
  <meta charset="UTF-8">
63
- <title>aeo-ready — AI Readiness Dashboard</title>
58
+ <title>aeo-ready — AEO Benchmark Dashboard</title>
64
59
  <style>
65
60
  ${CSS}
66
61
  </style>
@@ -69,30 +64,24 @@ ${CSS}
69
64
 
70
65
  <nav>
71
66
  <h2>aeo-ready</h2>
72
- <a href="#overall" class="section-head">Overall Score</a>
73
- <a href="#agent-readiness" class="section-head">Agent Readiness</a>
74
- <a href="#ai-visibility" class="section-head">AI Visibility</a>
67
+ <a href="#overall" class="section-head">Scores</a>
68
+ <a href="#details" class="section-head">Benchmark Details</a>
75
69
  <a href="#trends" class="section-head">Trends</a>
76
70
  <a href="#history" class="section-head">History</a>
77
- <a href="#recommendations" class="section-head">Recommendations</a>
78
71
  </nav>
79
72
 
80
73
  <main>
81
74
 
82
- <h1>AI Readiness Dashboard</h1>
83
- <p class="subtitle">${target} · Updated ${timestamp}</p>
75
+ <h1>AEO Benchmark Dashboard</h1>
76
+ <p class="subtitle">${scanResult.url} · ${timestamp}</p>
84
77
 
85
78
  <!-- SECTION:overall-score -->
86
79
  ${sections["overall-score"]}
87
80
  <!-- /SECTION:overall-score -->
88
81
 
89
- <!-- SECTION:agent-readiness-scorecard -->
90
- ${sections["agent-readiness-scorecard"]}
91
- <!-- /SECTION:agent-readiness-scorecard -->
92
-
93
- <!-- SECTION:ai-visibility-scorecard -->
94
- ${sections["ai-visibility-scorecard"]}
95
- <!-- /SECTION:ai-visibility-scorecard -->
82
+ <!-- SECTION:benchmark-details -->
83
+ ${sections["benchmark-details"]}
84
+ <!-- /SECTION:benchmark-details -->
96
85
 
97
86
  <!-- SECTION:trend-chart -->
98
87
  ${sections["trend-chart"]}
@@ -102,10 +91,6 @@ ${sections["trend-chart"]}
102
91
  ${sections["history-table"]}
103
92
  <!-- /SECTION:history-table -->
104
93
 
105
- <!-- SECTION:recommendations -->
106
- ${sections["recommendations"]}
107
- <!-- /SECTION:recommendations -->
108
-
109
94
  </main>
110
95
  </body>
111
96
  </html>`;
@@ -123,34 +108,23 @@ h1 { color: #f0f6fc; font-size: 24px; margin-bottom: 4px; }
123
108
  .subtitle { color: #8b949e; font-size: 13px; margin-bottom: 32px; }
124
109
  h2 { color: #f0f6fc; font-size: 18px; margin-top: 40px; margin-bottom: 16px; padding-top: 16px; border-top: 1px solid #21262d; }
125
110
  h3 { color: #f0f6fc; font-size: 14px; margin-top: 16px; margin-bottom: 8px; }
126
- .score-hero { text-align: center; padding: 32px 0; }
127
- .score-hero .grade { font-size: 64px; font-weight: 700; }
128
- .score-hero .number { font-size: 24px; color: #8b949e; margin-top: 4px; }
129
- .score-hero .bar { margin: 16px auto; width: 300px; height: 8px; background: #21262d; border-radius: 4px; overflow: hidden; }
130
- .score-hero .bar-fill { height: 100%; border-radius: 4px; }
111
+ .scores { display: flex; gap: 16px; flex-wrap: wrap; margin: 16px 0; }
112
+ .score-card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; flex: 1; min-width: 180px; text-align: center; }
113
+ .score-card .name { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #8b949e; margin-bottom: 8px; }
114
+ .score-card .grade { font-size: 36px; font-weight: 700; }
115
+ .score-card .number { font-size: 14px; color: #8b949e; margin-top: 4px; }
116
+ .score-card .bar { margin: 8px auto; width: 100%; height: 4px; background: #21262d; border-radius: 2px; overflow: hidden; }
117
+ .score-card .bar-fill { height: 100%; border-radius: 2px; }
131
118
  .grade-a { color: #3fb950; } .grade-b { color: #79c0ff; } .grade-c { color: #d29922; } .grade-d { color: #f85149; } .grade-f { color: #f85149; }
132
119
  .bar-a { background: #3fb950; } .bar-b { background: #79c0ff; } .bar-c { background: #d29922; } .bar-d { background: #f85149; } .bar-f { background: #f85149; }
133
- .scorecard { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; margin: 16px 0; }
134
- .scorecard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
135
- .scorecard-header h3 { margin: 0; }
136
- .scorecard-header .pct { font-size: 20px; font-weight: 600; }
137
- .category { margin: 12px 0; padding: 8px 0; border-bottom: 1px solid #21262d; }
138
- .category:last-child { border-bottom: none; }
139
- .cat-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
140
- .cat-name { font-size: 13px; font-weight: 500; flex: 1; }
141
- .cat-score { font-size: 12px; color: #8b949e; }
142
- .cat-bar { width: 120px; height: 4px; background: #21262d; border-radius: 2px; overflow: hidden; }
143
- .cat-bar-fill { height: 100%; border-radius: 2px; }
120
+ details { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; margin: 12px 0; cursor: pointer; }
121
+ details summary { font-size: 14px; font-weight: 500; color: #f0f6fc; display: flex; justify-content: space-between; }
144
122
  .check { font-size: 12px; padding: 2px 0 2px 16px; color: #8b949e; }
145
123
  .check.pass { color: #3fb950; }
146
124
  .check.fail { color: #f85149; }
147
- .check .fix { display: block; color: #8b949e; font-size: 11px; padding-left: 16px; margin-top: 2px; }
125
+ .compare { font-size: 11px; color: #8b949e; margin-top: 8px; padding-top: 8px; border-top: 1px solid #21262d; }
148
126
  table { width: 100%; border-collapse: collapse; margin: 12px 0; }
149
127
  th, td { text-align: left; padding: 8px 12px; font-size: 12px; border-bottom: 1px solid #21262d; }
150
128
  th { color: #8b949e; font-weight: 600; }
151
129
  .trend { color: #3fb950; } .regression { color: #f85149; }
152
- .rec { background: #161b22; border: 1px solid #21262d; border-radius: 6px; padding: 12px 16px; margin: 8px 0; }
153
- .rec-title { font-size: 13px; font-weight: 500; color: #f0f6fc; }
154
- .rec-fix { font-size: 12px; color: #8b949e; margin-top: 4px; }
155
- .rec-impact { font-size: 11px; color: #d29922; margin-top: 4px; }
156
130
  svg { display: block; margin: 16px 0; }`;
@@ -0,0 +1,79 @@
1
+ const REFERENCE_SCORES = {
2
+ agenticSeo: {
3
+ Cloudflare: 55,
4
+ Supabase: 52,
5
+ Vercel: 48,
6
+ Average: 25,
7
+ Stripe: 17,
8
+ },
9
+ cloudflare: { Cloudflare: 5, Vercel: 4, Supabase: 3, Stripe: 2, Average: 2 },
10
+ fern: { Stripe: 85, Supabase: 78, Anthropic: 72, Vercel: 60, Average: 45 },
11
+ };
12
+
13
+ export function renderBenchmarkDetails(result) {
14
+ const { benchmarks } = result;
15
+
16
+ let html = `<h2 id="details">Benchmark Details</h2>
17
+ <p style="color:#8b949e;font-size:12px;margin-bottom:12px;">Expand each source to see per-check results and how other companies score.</p>`;
18
+
19
+ if (benchmarks.agenticSeo?.available) {
20
+ html += renderSource("agentic-seo", "agenticSeo", benchmarks.agenticSeo);
21
+ }
22
+ if (benchmarks.cloudflare?.available) {
23
+ html += renderSource("Cloudflare", "cloudflare", benchmarks.cloudflare);
24
+ }
25
+ if (benchmarks.fern?.available) {
26
+ html += renderSource("Fern", "fern", benchmarks.fern);
27
+ }
28
+
29
+ return html;
30
+ }
31
+
32
+ function renderSource(name, key, data) {
33
+ const pct =
34
+ data.maxScore > 0 ? Math.round((data.score / data.maxScore) * 100) : 0;
35
+
36
+ let html = `\n<details>
37
+ <summary>
38
+ <span>${esc(name)}</span>
39
+ <span style="color:#8b949e;">${data.score}/${data.maxScore}${data.grade ? ` (${data.grade})` : ""}</span>
40
+ </summary>
41
+ <div style="margin-top:12px;">`;
42
+
43
+ if (data.checks && data.checks.length > 0) {
44
+ for (const check of data.checks) {
45
+ if (check.status === "pass") {
46
+ html += `\n <div class="check pass">+ ${esc(check.id)} ${check.message ? `<span style="color:#8b949e;font-size:11px;">${esc(check.message.slice(0, 80))}</span>` : ""}</div>`;
47
+ } else if (check.status === "fail") {
48
+ html += `\n <div class="check fail">- ${esc(check.id)} ${check.message ? `<span style="color:#8b949e;font-size:11px;">${esc(check.message.slice(0, 80))}</span>` : ""}</div>`;
49
+ }
50
+ }
51
+ } else if (data.categories) {
52
+ for (const [, cat] of Object.entries(data.categories)) {
53
+ const catPct =
54
+ cat.maxScore > 0 ? Math.round((cat.score / cat.maxScore) * 100) : 0;
55
+ const icon = catPct >= 80 ? "+" : catPct >= 40 ? "~" : "-";
56
+ const cls = catPct >= 80 ? "pass" : "fail";
57
+ html += `\n <div class="check ${cls}">${icon} ${esc(cat.name || "")} ${cat.score}/${cat.maxScore}</div>`;
58
+ }
59
+ }
60
+
61
+ const refs = REFERENCE_SCORES[key];
62
+ if (refs) {
63
+ const lines = Object.entries(refs)
64
+ .sort((a, b) => b[1] - a[1])
65
+ .map(([n, s]) => `${n}: ${s}`)
66
+ .join(" · ");
67
+ html += `\n <div class="compare">Others: ${lines}</div>`;
68
+ }
69
+
70
+ html += `\n </div>\n</details>`;
71
+ return html;
72
+ }
73
+
74
+ function esc(s) {
75
+ return (s || "")
76
+ .replace(/&/g, "&amp;")
77
+ .replace(/</g, "&lt;")
78
+ .replace(/>/g, "&gt;");
79
+ }
@@ -5,12 +5,12 @@ export function renderHistoryTable(history) {
5
5
 
6
6
  const scans = [...history.scans].reverse().slice(0, 20);
7
7
 
8
- let html = `<h2 id="history">Scan History</h2>\n<table>\n<tr><th>Date</th><th>Score</th><th>Grade</th><th>Agent</th><th>Visibility</th><th>Delta</th></tr>`;
8
+ let html = `<h2 id="history">Scan History</h2>\n<table>\n<tr><th>Date</th><th>agentic-seo</th><th>Cloudflare</th><th>Fern</th><th>Avg</th><th>Delta</th></tr>`;
9
9
 
10
10
  for (let i = 0; i < scans.length; i++) {
11
11
  const s = scans[i];
12
12
  const prev = scans[i + 1];
13
- const delta = prev ? s.score - prev.score : 0;
13
+ const delta = prev ? s.averageScore - prev.averageScore : 0;
14
14
  const deltaStr =
15
15
  delta > 0
16
16
  ? `<span class="trend">+${delta}</span>`
@@ -19,14 +19,17 @@ export function renderHistoryTable(history) {
19
19
  : `<span style="color:#8b949e">—</span>`;
20
20
 
21
21
  const date = s.timestamp ? s.timestamp.slice(0, 10) : "—";
22
- const gradeClass = `grade-${(s.grade || "f").toLowerCase()}`;
22
+ const cf =
23
+ s.cloudflare != null && s.cloudflareMax
24
+ ? `${s.cloudflare}/${s.cloudflareMax}`
25
+ : "—";
23
26
 
24
27
  html += `\n<tr>
25
28
  <td>${date}</td>
26
- <td>${s.score}/100</td>
27
- <td class="${gradeClass}">${s.grade || "?"}</td>
28
- <td>${s.agentReadiness ?? "—"}/50</td>
29
- <td>${s.aiVisibility ?? "—"}/50</td>
29
+ <td>${s.agenticSeo ?? "—"}</td>
30
+ <td>${cf}</td>
31
+ <td>${s.fern ?? "—"}</td>
32
+ <td>${s.averageScore ?? "—"}</td>
30
33
  <td>${deltaStr}</td>
31
34
  </tr>`;
32
35
  }
@@ -1,128 +1,65 @@
1
- export function renderOverallScore(result, beforeResult) {
2
- if (beforeResult) {
3
- return renderBeforeAfter(beforeResult, result);
1
+ export function renderOverallScore(result) {
2
+ const { benchmarks, averageScore } = result;
3
+
4
+ let html = `<div id="overall" class="scores">`;
5
+
6
+ if (benchmarks.agenticSeo?.available) {
7
+ html += scoreCard(
8
+ "agentic-seo",
9
+ benchmarks.agenticSeo.score,
10
+ 100,
11
+ benchmarks.agenticSeo.grade,
12
+ );
4
13
  }
5
- return renderSingle(result);
6
- }
7
-
8
- function renderSingle(result) {
9
- const { score, grade } = result;
10
- const gradeClass = `grade-${grade.toLowerCase()}`;
11
- const barClass = `bar-${grade.toLowerCase()}`;
12
-
13
- return `<div class="score-hero" id="overall">
14
- <div class="grade ${gradeClass}">${grade}</div>
15
- <div class="number">${score} / 100</div>
16
- <div class="bar"><div class="bar-fill ${barClass}" style="width:${score}%"></div></div>
17
- <div style="display:flex;justify-content:center;gap:40px;margin-top:16px;font-size:13px;">
18
- <span>Agent Readiness: <strong>${result.agentReadiness.score}/50</strong></span>
19
- <span>AI Visibility: <strong>${result.aiVisibility.score}/50</strong></span>
20
- </div>
21
- </div>
22
- ${renderInsight(result)}
23
- ${renderBenchmarkComparison(score)}`;
24
- }
25
-
26
- function renderBeforeAfter(before, after) {
27
- const delta = after.score - before.score;
28
- const deltaClass = delta > 0 ? "trend" : delta < 0 ? "regression" : "";
29
- const deltaStr = delta > 0 ? `+${delta}` : `${delta}`;
30
-
31
- return `<div id="overall" style="display:flex;align-items:center;justify-content:center;gap:24px;padding:32px 0;">
32
-
33
- <div style="text-align:center;flex:1;">
34
- <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:#8b949e;margin-bottom:8px;">Before</div>
35
- <div class="grade-${before.grade.toLowerCase()}" style="font-size:48px;font-weight:700;">${before.grade}</div>
36
- <div style="font-size:20px;color:#8b949e;margin-top:4px;">${before.score} / 100</div>
37
- <div class="bar" style="margin:12px auto;width:200px;height:6px;background:#21262d;border-radius:3px;overflow:hidden;">
38
- <div class="bar-fill bar-${before.grade.toLowerCase()}" style="width:${before.score}%;height:100%;border-radius:3px;"></div>
39
- </div>
40
- <div style="font-size:12px;color:#8b949e;">
41
- Agent: ${before.agentReadiness.score}/50 · Visibility: ${before.aiVisibility.score}/50
42
- </div>
43
- </div>
44
-
45
- <div style="font-size:32px;color:#8b949e;padding:0 8px;">→</div>
46
-
47
- <div style="text-align:center;flex:1;">
48
- <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:#8b949e;margin-bottom:8px;">After</div>
49
- <div class="grade-${after.grade.toLowerCase()}" style="font-size:48px;font-weight:700;">${after.grade}</div>
50
- <div style="font-size:20px;color:#8b949e;margin-top:4px;">${after.score} / 100</div>
51
- <div class="bar" style="margin:12px auto;width:200px;height:6px;background:#21262d;border-radius:3px;overflow:hidden;">
52
- <div class="bar-fill bar-${after.grade.toLowerCase()}" style="width:${after.score}%;height:100%;border-radius:3px;"></div>
53
- </div>
54
- <div style="font-size:12px;color:#8b949e;">
55
- Agent: ${after.agentReadiness.score}/50 · Visibility: ${after.aiVisibility.score}/50
56
- </div>
57
- </div>
58
-
59
- </div>
60
- ${delta !== 0 ? `<div style="text-align:center;margin-top:-16px;margin-bottom:16px;"><span class="${deltaClass}" style="font-size:16px;font-weight:600;">${deltaStr} points</span></div>` : ""}
61
- ${renderInsight(after)}
62
- ${renderBenchmarkComparison(after.score)}`;
63
- }
64
-
65
- function renderInsight(result) {
66
- const agent = result.agentReadiness.score;
67
- const vis = result.aiVisibility.score;
68
-
69
- if (agent < 25) {
70
- return `<div style="text-align:center;padding:12px 24px;margin:16px auto;max-width:500px;background:#d2992210;border:1px solid #d2992233;border-radius:6px;font-size:13px;color:#d29922;">
71
- <strong>Agent Readiness is low.</strong> AI engines can't cite what they can't read.<br>
72
- <span style="color:#8b949e;">Fix the agent side first. Visibility follows.</span>
73
- </div>`;
74
- }
75
-
76
- if (agent >= 40 && vis < 25) {
77
- return `<div style="text-align:center;padding:12px 24px;margin:16px auto;max-width:500px;background:#79c0ff10;border:1px solid #79c0ff33;border-radius:6px;font-size:13px;color:#79c0ff;">
78
- <strong>Agents can read your site — now make it citable.</strong><br>
79
- <span style="color:#8b949e;">Add direct answer summaries, question headings, and structured claims.</span>
80
- </div>`;
14
+ if (benchmarks.cloudflare?.available) {
15
+ html += scoreCard(
16
+ "Cloudflare",
17
+ benchmarks.cloudflare.score,
18
+ benchmarks.cloudflare.maxScore,
19
+ benchmarks.cloudflare.grade,
20
+ );
81
21
  }
82
-
83
- if (agent >= 40 && vis >= 40) {
84
- return `<div style="text-align:center;padding:12px 24px;margin:16px auto;max-width:500px;background:#3fb95010;border:1px solid #3fb95033;border-radius:6px;font-size:13px;color:#3fb950;">
85
- <strong>Strong on both sides.</strong> Agents can find you and AI engines can cite you.
86
- </div>`;
22
+ if (benchmarks.fern?.available) {
23
+ html += scoreCard(
24
+ "Fern",
25
+ benchmarks.fern.score,
26
+ 100,
27
+ benchmarks.fern.grade,
28
+ );
87
29
  }
88
30
 
89
- return "";
90
- }
91
-
92
- const REFERENCE_SCORES = [
93
- { name: "Stripe", score: 33, type: "SaaS" },
94
- { name: "Anthropic Docs", score: 43, type: "API" },
95
- { name: "Vercel", score: 48, type: "SaaS" },
96
- { name: "Supabase", score: 52, type: "API" },
97
- { name: "Cloudflare", score: 55, type: "SaaS" },
98
- { name: "Average site", score: 25, type: "" },
99
- ];
31
+ html += `</div>`;
100
32
 
101
- function renderBenchmarkComparison(yourScore) {
102
- const sorted = [
103
- ...REFERENCE_SCORES,
104
- { name: "You", score: yourScore, type: "", you: true },
105
- ].sort((a, b) => b.score - a.score);
33
+ const gc =
34
+ averageScore >= 80 ? "grade-a" : averageScore >= 50 ? "grade-c" : "grade-f";
35
+ html += `<div style="text-align:center;margin:16px 0;font-size:14px;color:#8b949e;">Average across sources: <strong class="${gc}">${averageScore}/100</strong></div>`;
106
36
 
107
- let html = `<div style="margin-top:24px;padding:20px;background:#161b22;border:1px solid #21262d;border-radius:8px;">
108
- <div style="font-size:13px;font-weight:600;color:#f0f6fc;margin-bottom:12px;">How you compare</div>`;
109
-
110
- for (const entry of sorted) {
111
- const isYou = entry.you;
112
- const color = isYou ? "#d29922" : "#30363d";
113
- const nameColor = isYou ? "#f0f6fc" : "#8b949e";
114
- const weight = isYou ? "font-weight:600;" : "";
37
+ return html;
38
+ }
115
39
 
116
- html += `
117
- <div style="display:flex;align-items:center;gap:8px;margin:6px 0;">
118
- <span style="width:120px;font-size:12px;color:${nameColor};${weight}">${entry.name}</span>
119
- <div style="flex:1;height:4px;background:#21262d;border-radius:2px;overflow:hidden;">
120
- <div style="width:${entry.score}%;height:100%;background:${color};border-radius:2px;"></div>
121
- </div>
122
- <span style="font-size:11px;color:${nameColor};width:32px;text-align:right;${weight}">${entry.score}</span>
40
+ function scoreCard(name, score, maxScore, grade) {
41
+ const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
42
+ const gc = grade
43
+ ? grade.toLowerCase()
44
+ : pct >= 80
45
+ ? "a"
46
+ : pct >= 65
47
+ ? "b"
48
+ : pct >= 50
49
+ ? "c"
50
+ : pct >= 35
51
+ ? "d"
52
+ : "f";
53
+
54
+ return `
55
+ <div class="score-card">
56
+ <div class="name">${esc(name)}</div>
57
+ <div class="grade grade-${gc}">${score}/${maxScore}</div>
58
+ <div class="number">${grade || ""}</div>
59
+ <div class="bar"><div class="bar-fill bar-${gc}" style="width:${pct}%"></div></div>
123
60
  </div>`;
124
- }
61
+ }
125
62
 
126
- html += `\n</div>`;
127
- return html;
63
+ function esc(s) {
64
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
128
65
  }