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 +9 -62
- package/package.json +2 -2
- package/src/dashboard/generate.js +22 -48
- package/src/dashboard/sections/benchmark-details.js +79 -0
- package/src/dashboard/sections/history-table.js +10 -7
- package/src/dashboard/sections/overall-score.js +55 -118
- package/src/dashboard/sections/trend-chart.js +31 -46
- package/src/history/index.js +8 -15
- package/src/scan.js +59 -293
- package/src/checks/agent-readiness/actionable.js +0 -165
- package/src/checks/agent-readiness/capability.js +0 -209
- package/src/checks/agent-readiness/content-structure.js +0 -242
- package/src/checks/agent-readiness/discovery.js +0 -231
- package/src/checks/ai-visibility/authority.js +0 -195
- package/src/checks/ai-visibility/citation-readiness.js +0 -228
- package/src/checks/ai-visibility/freshness.js +0 -182
- package/src/checks/ai-visibility/structured-data.js +0 -180
- package/src/dashboard/sections/agent-readiness.js +0 -71
- package/src/dashboard/sections/ai-visibility.js +0 -67
- package/src/dashboard/sections/recommendations.js +0 -196
- package/src/fix/generators/agents-json.js +0 -73
- package/src/fix/generators/agents-md.js +0 -85
- package/src/fix/generators/llms-txt.js +0 -166
- package/src/fix/generators/robots-txt.js +0 -64
- package/src/fix/index.js +0 -177
- package/src/track/index.js +0 -167
- package/src/utils/detect-type.js +0 -99
- package/src/utils/tokens.js +0 -18
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("
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4
|
-
"description": "
|
|
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 {
|
|
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
|
|
23
|
-
"
|
|
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
|
-
|
|
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 —
|
|
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">
|
|
73
|
-
<a href="#
|
|
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>
|
|
83
|
-
<p class="subtitle">${
|
|
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:
|
|
90
|
-
${sections["
|
|
91
|
-
<!-- /SECTION:
|
|
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
|
-
.
|
|
127
|
-
.score-
|
|
128
|
-
.score-
|
|
129
|
-
.score-
|
|
130
|
-
.score-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
.
|
|
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, "&")
|
|
77
|
+
.replace(/</g, "<")
|
|
78
|
+
.replace(/>/g, ">");
|
|
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>
|
|
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.
|
|
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
|
|
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.
|
|
27
|
-
<td
|
|
28
|
-
<td>${s.
|
|
29
|
-
<td>${s.
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
return
|
|
63
|
+
function esc(s) {
|
|
64
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
128
65
|
}
|