aeo-ready 1.1.0 → 1.3.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.
@@ -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
  }
@@ -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 padding = { top: 20, right: 20, bottom: 30, left: 40 };
10
- const chartW = width - padding.left - padding.right;
11
- const chartH = height - padding.top - padding.bottom;
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 toPoint(index, value) {
17
- const x = padding.left + index * xStep;
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 points = values.map((v, i) => toPoint(i, v));
24
- const d = points
25
- .map(
26
- (p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`,
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 overall = scans.map((s) => s.score);
42
- const agent = scans.map((s) => (s.agentReadiness ?? 0) * 2);
43
- const vis = scans.map((s) => (s.aiVisibility ?? 0) * 2);
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 gridLines = [0, 25, 50, 75, 100]
34
+ const grid = [0, 25, 50, 75, 100]
46
35
  .map((v) => {
47
- const y = padding.top + chartH - (v / maxScore) * chartH;
48
- return `<line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="#21262d" stroke-width="1"/>
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 xLabels = scans
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 = padding.left + i * xStep;
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
- const svg = `<h2 id="trends">Score Trends</h2>
52
+ return `<h2 id="trends">Score Trends</h2>
65
53
  <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
66
- ${gridLines}
67
- ${xLabels}
68
- ${polyline(agent, "#79c0ff")}
69
- ${polyline(vis, "#d29922")}
70
- ${polyline(overall, "#f0f6fc")}
71
- ${dots(overall, "#f0f6fc")}
72
- <text x="${width - padding.right}" y="${padding.top - 5}" text-anchor="end" fill="#f0f6fc" font-size="10">Overall</text>
73
- <text x="${width - padding.right - 80}" y="${padding.top - 5}" text-anchor="end" fill="#79c0ff" font-size="10">Agent (x2)</text>
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
  }
@@ -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 = ".agent-web";
4
+ const DIR_NAME = ".aeo-ready";
5
5
  const HISTORY_FILE = "history.json";
6
6
 
7
7
  export async function saveResult(result, baseDir) {
@@ -14,17 +14,12 @@ export async function saveResult(result, baseDir) {
14
14
  history.scans.push({
15
15
  id: generateId(),
16
16
  timestamp: result.timestamp,
17
- score: result.score,
18
- grade: result.grade,
19
- siteType: result.siteType,
20
- target: result.target,
21
- agentReadiness: result.agentReadiness.score,
22
- aiVisibility: result.aiVisibility.score,
23
- benchmarks: {
24
- agenticSeo: result.benchmarks?.agenticSeo?.score ?? null,
25
- cloudflare: result.benchmarks?.cloudflare?.score ?? null,
26
- fern: result.benchmarks?.fern?.score ?? null,
27
- },
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,
28
23
  });
29
24
 
30
25
  writeFileSync(historyPath, JSON.stringify(history, null, 2));
@@ -34,9 +29,7 @@ export function loadHistory(historyPath) {
34
29
  if (existsSync(historyPath)) {
35
30
  try {
36
31
  return JSON.parse(readFileSync(historyPath, "utf8"));
37
- } catch {
38
- /* corrupted — start fresh */
39
- }
32
+ } catch {}
40
33
  }
41
34
  return { scans: [] };
42
35
  }