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.
- package/.claude/settings.local.json +7 -0
- package/bin/cli.js +9 -62
- package/package.json +5 -4
- package/src/benchmark/cloudflare.js +75 -0
- package/src/benchmark/fern.js +51 -0
- package/src/benchmark/index.js +119 -0
- 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 -11
- package/src/scan.js +58 -294
- package/.aeo-ready/dashboard.html +0 -339
- 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 -49
- 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
|
@@ -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
|
}
|
|
@@ -6,54 +6,42 @@ export function renderTrendChart(history) {
|
|
|
6
6
|
const scans = history.scans.slice(-20);
|
|
7
7
|
const width = 700;
|
|
8
8
|
const height = 200;
|
|
9
|
-
const
|
|
10
|
-
const chartW = width -
|
|
11
|
-
const chartH = height -
|
|
12
|
-
|
|
13
|
-
const maxScore = 100;
|
|
9
|
+
const pad = { top: 20, right: 20, bottom: 30, left: 40 };
|
|
10
|
+
const chartW = width - pad.left - pad.right;
|
|
11
|
+
const chartH = height - pad.top - pad.bottom;
|
|
14
12
|
const xStep = scans.length > 1 ? chartW / (scans.length - 1) : chartW;
|
|
15
13
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
const y = padding.top + chartH - (value / maxScore) * chartH;
|
|
19
|
-
return { x, y };
|
|
14
|
+
function toY(value) {
|
|
15
|
+
return pad.top + chartH - (value / 100) * chartH;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function polyline(values, color) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.join(" ");
|
|
29
|
-
return `<path d="${d}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function dots(values, color) {
|
|
33
|
-
return values
|
|
34
|
-
.map((v, i) => {
|
|
35
|
-
const p = toPoint(i, v);
|
|
36
|
-
return `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}"/>`;
|
|
37
|
-
})
|
|
38
|
-
.join("\n");
|
|
19
|
+
const pts = values.map(
|
|
20
|
+
(v, i) =>
|
|
21
|
+
`${i === 0 ? "M" : "L"} ${(pad.left + i * xStep).toFixed(1)} ${toY(v ?? 0).toFixed(1)}`,
|
|
22
|
+
);
|
|
23
|
+
return `<path d="${pts.join(" ")}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
39
24
|
}
|
|
40
25
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
26
|
+
const agenticSeo = scans.map((s) => s.agenticSeo ?? 0);
|
|
27
|
+
const cloudflare = scans.map((s) =>
|
|
28
|
+
s.cloudflare != null && s.cloudflareMax
|
|
29
|
+
? Math.round((s.cloudflare / s.cloudflareMax) * 100)
|
|
30
|
+
: 0,
|
|
31
|
+
);
|
|
32
|
+
const fern = scans.map((s) => s.fern ?? 0);
|
|
44
33
|
|
|
45
|
-
const
|
|
34
|
+
const grid = [0, 25, 50, 75, 100]
|
|
46
35
|
.map((v) => {
|
|
47
|
-
const y =
|
|
48
|
-
return `<line x1="${
|
|
49
|
-
<text x="${padding.left - 8}" y="${y + 4}" text-anchor="end" fill="#8b949e" font-size="10">${v}</text>`;
|
|
36
|
+
const y = toY(v);
|
|
37
|
+
return `<line x1="${pad.left}" y1="${y}" x2="${width - pad.right}" y2="${y}" stroke="#21262d"/><text x="${pad.left - 8}" y="${y + 4}" text-anchor="end" fill="#8b949e" font-size="10">${v}</text>`;
|
|
50
38
|
})
|
|
51
39
|
.join("\n");
|
|
52
40
|
|
|
53
|
-
const
|
|
41
|
+
const labels = scans
|
|
54
42
|
.map((s, i) => {
|
|
55
43
|
if (scans.length <= 10 || i % Math.ceil(scans.length / 8) === 0) {
|
|
56
|
-
const x =
|
|
44
|
+
const x = pad.left + i * xStep;
|
|
57
45
|
const label = s.timestamp ? s.timestamp.slice(5, 10) : "";
|
|
58
46
|
return `<text x="${x}" y="${height - 5}" text-anchor="middle" fill="#8b949e" font-size="10">${label}</text>`;
|
|
59
47
|
}
|
|
@@ -61,18 +49,15 @@ export function renderTrendChart(history) {
|
|
|
61
49
|
})
|
|
62
50
|
.join("\n");
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
return `<h2 id="trends">Score Trends</h2>
|
|
65
53
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
66
|
-
${
|
|
67
|
-
${
|
|
68
|
-
${polyline(
|
|
69
|
-
${polyline(
|
|
70
|
-
${polyline(
|
|
71
|
-
${
|
|
72
|
-
<text x="${width -
|
|
73
|
-
<text x="${width -
|
|
74
|
-
<text x="${width - padding.right - 180}" y="${padding.top - 5}" text-anchor="end" fill="#d29922" font-size="10">Visibility (x2)</text>
|
|
54
|
+
${grid}
|
|
55
|
+
${labels}
|
|
56
|
+
${polyline(agenticSeo, "#f85149")}
|
|
57
|
+
${polyline(cloudflare, "#3fb950")}
|
|
58
|
+
${polyline(fern, "#79c0ff")}
|
|
59
|
+
<text x="${width - pad.right}" y="${pad.top - 5}" text-anchor="end" fill="#f85149" font-size="10">agentic-seo</text>
|
|
60
|
+
<text x="${width - pad.right - 90}" y="${pad.top - 5}" text-anchor="end" fill="#3fb950" font-size="10">Cloudflare</text>
|
|
61
|
+
<text x="${width - pad.right - 170}" y="${pad.top - 5}" text-anchor="end" fill="#79c0ff" font-size="10">Fern</text>
|
|
75
62
|
</svg>`;
|
|
76
|
-
|
|
77
|
-
return svg;
|
|
78
63
|
}
|
package/src/history/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
const DIR_NAME = ".
|
|
4
|
+
const DIR_NAME = ".aeo-ready";
|
|
5
5
|
const HISTORY_FILE = "history.json";
|
|
6
6
|
|
|
7
7
|
export async function saveResult(result, baseDir) {
|
|
@@ -14,13 +14,12 @@ export async function saveResult(result, baseDir) {
|
|
|
14
14
|
history.scans.push({
|
|
15
15
|
id: generateId(),
|
|
16
16
|
timestamp: result.timestamp,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
benchmark: result.benchmark?.score ?? null,
|
|
17
|
+
url: result.url,
|
|
18
|
+
averageScore: result.averageScore,
|
|
19
|
+
agenticSeo: result.benchmarks?.agenticSeo?.score ?? null,
|
|
20
|
+
cloudflare: result.benchmarks?.cloudflare?.score ?? null,
|
|
21
|
+
cloudflareMax: result.benchmarks?.cloudflare?.maxScore ?? null,
|
|
22
|
+
fern: result.benchmarks?.fern?.score ?? null,
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
@@ -30,9 +29,7 @@ export function loadHistory(historyPath) {
|
|
|
30
29
|
if (existsSync(historyPath)) {
|
|
31
30
|
try {
|
|
32
31
|
return JSON.parse(readFileSync(historyPath, "utf8"));
|
|
33
|
-
} catch {
|
|
34
|
-
/* corrupted — start fresh */
|
|
35
|
-
}
|
|
32
|
+
} catch {}
|
|
36
33
|
}
|
|
37
34
|
return { scans: [] };
|
|
38
35
|
}
|