aeo-ready 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.
Files changed (33) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/bin/cli.js +9 -62
  3. package/package.json +5 -4
  4. package/src/benchmark/cloudflare.js +75 -0
  5. package/src/benchmark/fern.js +51 -0
  6. package/src/benchmark/index.js +119 -0
  7. package/src/dashboard/generate.js +22 -48
  8. package/src/dashboard/sections/benchmark-details.js +79 -0
  9. package/src/dashboard/sections/history-table.js +10 -7
  10. package/src/dashboard/sections/overall-score.js +55 -118
  11. package/src/dashboard/sections/trend-chart.js +31 -46
  12. package/src/history/index.js +8 -11
  13. package/src/scan.js +58 -294
  14. package/.aeo-ready/dashboard.html +0 -339
  15. package/src/checks/agent-readiness/actionable.js +0 -165
  16. package/src/checks/agent-readiness/capability.js +0 -209
  17. package/src/checks/agent-readiness/content-structure.js +0 -242
  18. package/src/checks/agent-readiness/discovery.js +0 -231
  19. package/src/checks/ai-visibility/authority.js +0 -195
  20. package/src/checks/ai-visibility/citation-readiness.js +0 -228
  21. package/src/checks/ai-visibility/freshness.js +0 -182
  22. package/src/checks/ai-visibility/structured-data.js +0 -180
  23. package/src/dashboard/sections/agent-readiness.js +0 -71
  24. package/src/dashboard/sections/ai-visibility.js +0 -67
  25. package/src/dashboard/sections/recommendations.js +0 -49
  26. package/src/fix/generators/agents-json.js +0 -73
  27. package/src/fix/generators/agents-md.js +0 -85
  28. package/src/fix/generators/llms-txt.js +0 -166
  29. package/src/fix/generators/robots-txt.js +0 -64
  30. package/src/fix/index.js +0 -177
  31. package/src/track/index.js +0 -167
  32. package/src/utils/detect-type.js +0 -99
  33. package/src/utils/tokens.js +0 -18
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch"
5
+ ]
6
+ }
7
+ }
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.0.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"
@@ -29,7 +29,8 @@
29
29
  "node": ">=18.0.0"
30
30
  },
31
31
  "dependencies": {
32
- "commander": "^12.0.0",
33
- "chalk": "^5.3.0"
32
+ "afdocs": "^0.18.7",
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.0.0"
34
35
  }
35
36
  }
@@ -0,0 +1,75 @@
1
+ export async function runCloudflare(url) {
2
+ try {
3
+ const res = await fetch("https://isitagentready.com/mcp", {
4
+ method: "POST",
5
+ headers: {
6
+ "Content-Type": "application/json",
7
+ Accept: "application/json, text/event-stream",
8
+ },
9
+ body: JSON.stringify({
10
+ jsonrpc: "2.0",
11
+ id: 1,
12
+ method: "tools/call",
13
+ params: {
14
+ name: "scan_site",
15
+ arguments: { url },
16
+ },
17
+ }),
18
+ });
19
+
20
+ const text = await res.text();
21
+ const dataLine = text.split("\n").find((l) => l.startsWith("data:"));
22
+ if (!dataLine) return { available: false, reason: "no data in response" };
23
+
24
+ const json = JSON.parse(dataLine.slice(5));
25
+ const content = json.result?.content?.[0]?.text || "";
26
+
27
+ const levelMatch = content.match(/Level (\d)\/5/i);
28
+ const level = levelMatch ? parseInt(levelMatch[1]) : 0;
29
+
30
+ const categories = {};
31
+ const catRegex = /## (.+?) \((\d+)\/(\d+) passing\)/g;
32
+ let match;
33
+ while ((match = catRegex.exec(content)) !== null) {
34
+ const name = match[1];
35
+ categories[name.toLowerCase().replace(/[^a-z]+/g, "-")] = {
36
+ name,
37
+ score: parseInt(match[2]),
38
+ maxScore: parseInt(match[3]),
39
+ percentage: Math.round((parseInt(match[2]) / parseInt(match[3])) * 100),
40
+ };
41
+ }
42
+
43
+ const checks = [];
44
+ const checkRegex =
45
+ /- (PASS|FAIL|OK) (\w+)(?:: (.+?))?(?:\n\s+(.+?))?(?=\n- |\n##|\n$)/g;
46
+ let cm;
47
+ while ((cm = checkRegex.exec(content)) !== null) {
48
+ checks.push({
49
+ status: cm[1].toLowerCase(),
50
+ id: cm[2],
51
+ message: cm[3] || "",
52
+ });
53
+ }
54
+
55
+ return {
56
+ score: level,
57
+ maxScore: 5,
58
+ grade: levelToGrade(level),
59
+ level,
60
+ checks,
61
+ categories,
62
+ available: true,
63
+ };
64
+ } catch (err) {
65
+ return { available: false, reason: err.message?.slice(0, 100) };
66
+ }
67
+ }
68
+
69
+ function levelToGrade(level) {
70
+ if (level >= 5) return "A";
71
+ if (level >= 4) return "B";
72
+ if (level >= 3) return "C";
73
+ if (level >= 2) return "D";
74
+ return "F";
75
+ }
@@ -0,0 +1,51 @@
1
+ export async function runFern(url) {
2
+ try {
3
+ const { runChecks, computeScore, toGrade, CATEGORIES } =
4
+ await import("afdocs");
5
+
6
+ const report = await runChecks(url);
7
+ const checks = report.results || [];
8
+ const computed = computeScore(report);
9
+ const score = computed.overall ?? 0;
10
+ const maxScore = 100;
11
+ const grade = computed.grade || toGrade(score);
12
+
13
+ const catMap = {};
14
+ for (const cat of CATEGORIES) {
15
+ catMap[cat.id] = { name: cat.name, score: 0, maxScore: 0 };
16
+ }
17
+
18
+ for (const check of checks) {
19
+ const catId = check.category || "other";
20
+ if (!catMap[catId]) {
21
+ catMap[catId] = { name: catId, score: 0, maxScore: 0 };
22
+ }
23
+ catMap[catId].maxScore += 1;
24
+ if (check.status === "pass") catMap[catId].score += 1;
25
+ }
26
+
27
+ for (const cat of Object.values(catMap)) {
28
+ cat.percentage =
29
+ cat.maxScore > 0 ? Math.round((cat.score / cat.maxScore) * 100) : 0;
30
+ }
31
+
32
+ const categories = Object.fromEntries(
33
+ Object.entries(catMap).filter(([, v]) => v.maxScore > 0),
34
+ );
35
+
36
+ return {
37
+ score,
38
+ maxScore,
39
+ grade,
40
+ categories,
41
+ checks: checks.map((c) => ({
42
+ id: c.id,
43
+ status: c.status,
44
+ message: c.message || "",
45
+ })),
46
+ available: true,
47
+ };
48
+ } catch (err) {
49
+ return { available: false, reason: err.message?.slice(0, 100) };
50
+ }
51
+ }
@@ -0,0 +1,119 @@
1
+ import chalk from "chalk";
2
+ import { runBenchmark as runAgenticSeo } from "./agentic-seo.js";
3
+ import { runCloudflare } from "./cloudflare.js";
4
+ import { runFern } from "./fern.js";
5
+
6
+ const REFERENCE_SCORES = {
7
+ agenticSeo: {
8
+ Stripe: 17,
9
+ Vercel: 48,
10
+ Supabase: 52,
11
+ Cloudflare: 55,
12
+ Average: 25,
13
+ },
14
+ cloudflare: {
15
+ Stripe: 2,
16
+ Vercel: 4,
17
+ Supabase: 3,
18
+ Cloudflare: 5,
19
+ Average: 2,
20
+ },
21
+ fern: {
22
+ Stripe: 85,
23
+ Vercel: 60,
24
+ Supabase: 78,
25
+ Anthropic: 72,
26
+ Average: 45,
27
+ },
28
+ };
29
+
30
+ export async function runAllBenchmarks(target) {
31
+ const isUrl = target && target.startsWith("http");
32
+
33
+ const results = await Promise.allSettled([
34
+ runAgenticSeo(target),
35
+ isUrl ? runCloudflare(target) : Promise.resolve(null),
36
+ isUrl ? runFern(target) : Promise.resolve(null),
37
+ ]);
38
+
39
+ return {
40
+ agenticSeo: results[0].status === "fulfilled" ? results[0].value : null,
41
+ cloudflare: results[1].status === "fulfilled" ? results[1].value : null,
42
+ fern: results[2].status === "fulfilled" ? results[2].value : null,
43
+ };
44
+ }
45
+
46
+ export function printBenchmarks(benchmarks) {
47
+ const any = Object.values(benchmarks).some((b) => b && b.available);
48
+ if (!any) return;
49
+
50
+ console.log(
51
+ chalk.bold("\n ─── Benchmarks ────────────────────────────────────\n"),
52
+ );
53
+
54
+ if (benchmarks.agenticSeo?.available) {
55
+ printBenchmarkBlock("agentic-seo", "agenticSeo", benchmarks.agenticSeo);
56
+ }
57
+ if (benchmarks.cloudflare?.available) {
58
+ printBenchmarkBlock("Cloudflare", "cloudflare", benchmarks.cloudflare);
59
+ }
60
+ if (benchmarks.fern?.available) {
61
+ printBenchmarkBlock("Fern", "fern", benchmarks.fern);
62
+ }
63
+ }
64
+
65
+ function printBenchmarkBlock(name, key, b) {
66
+ const pct = b.maxScore > 0 ? Math.round((b.score / b.maxScore) * 100) : 0;
67
+ const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
68
+ const barW = 16;
69
+ const filled = Math.round((pct / 100) * barW);
70
+ const bar = color("█".repeat(filled)) + chalk.dim("░".repeat(barW - filled));
71
+
72
+ console.log(
73
+ ` ${bar} ${chalk.bold(name.padEnd(16))} ${chalk.dim(`${b.score}/${b.maxScore}`)}${b.grade ? chalk.dim(` (${b.grade})`) : ""}`,
74
+ );
75
+
76
+ if (b.checks && b.checks.length > 0) {
77
+ for (const check of b.checks) {
78
+ if (check.status === "pass") {
79
+ console.log(
80
+ chalk.green(` + ${check.id}`) +
81
+ chalk.dim(check.message ? ` ${check.message.slice(0, 60)}` : ""),
82
+ );
83
+ } else if (check.status === "fail") {
84
+ console.log(
85
+ chalk.red(` - ${check.id}`) +
86
+ chalk.dim(check.message ? ` ${check.message.slice(0, 60)}` : ""),
87
+ );
88
+ }
89
+ }
90
+ } else if (b.categories) {
91
+ for (const [, cat] of Object.entries(b.categories)) {
92
+ const catPct =
93
+ cat.percentage ??
94
+ (cat.maxScore > 0 ? Math.round((cat.score / cat.maxScore) * 100) : 0);
95
+ const icon =
96
+ catPct >= 80
97
+ ? chalk.green("✓")
98
+ : catPct >= 40
99
+ ? chalk.yellow("◑")
100
+ : chalk.red("✗");
101
+ console.log(
102
+ chalk.dim(
103
+ ` ${icon} ${(cat.name || "").padEnd(22)} ${cat.score}/${cat.maxScore}`,
104
+ ),
105
+ );
106
+ }
107
+ }
108
+
109
+ const refs = REFERENCE_SCORES[key];
110
+ if (refs) {
111
+ const names = Object.entries(refs)
112
+ .sort((a, b) => b[1] - a[1])
113
+ .map(([n, s]) => `${n} ${s}`)
114
+ .join(chalk.dim(" · "));
115
+ console.log(chalk.dim(` compare: ${names}`));
116
+ }
117
+
118
+ console.log("");
119
+ }
@@ -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
  }