aeo-ready 1.2.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.
- package/README.md +52 -76
- package/bin/cli.js +13 -2
- package/package.json +1 -1
- package/src/benchmark/agentic-seo.js +107 -6
- package/src/benchmark/index.js +2 -2
- package/src/scan.js +2 -2
package/README.md
CHANGED
|
@@ -1,115 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# aeo-ready
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AEO benchmark aggregator. One scan, every score.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx
|
|
6
|
+
npx aeo-ready scan yoursite.com
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## What it
|
|
9
|
+
## What it does
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- Discovery: llms.txt, robots.txt AI crawlers, sitemap, meta tags
|
|
13
|
-
- Content Structure: markdown availability, heading hierarchy, token budgets, front-loading
|
|
14
|
-
- Capability Signaling: AGENTS.md, agents.json, OpenAPI, content negotiation
|
|
15
|
-
- Actionable: machine-readable contact, pricing, API endpoints, SDK manifest
|
|
11
|
+
Runs every major AEO (Agentic Engine Optimization) benchmark against your site in one command. Shows per-check pass/fail, company comparisons, and tracks scores over time.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
- Structured Data: schema.org, FAQ markup, rich schemas, Open Graph
|
|
19
|
-
- Citation Readiness: direct answer formatting, question headings, citable structure
|
|
20
|
-
- Authority: E-E-A-T signals, entity optimization, external validation
|
|
21
|
-
- Freshness: modification dates, publication cadence, content recency
|
|
13
|
+
## Sources
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
| Benchmark | What it checks | Checks |
|
|
16
|
+
|-----------|---------------|--------|
|
|
17
|
+
| **agentic-seo** (Addy Osmani) | Discovery, content structure, token economics, capability signaling, UX bridge | 10 |
|
|
18
|
+
| **Cloudflare** (isitagentready.com) | Discoverability, content accessibility, bot access, API/auth/MCP/A2A discovery, commerce | 19 |
|
|
19
|
+
| **Fern** (afdocs) | llms.txt quality, markdown availability, page size, content structure, URL stability, auth | 23 |
|
|
24
20
|
|
|
25
21
|
## Usage
|
|
26
22
|
|
|
27
23
|
```bash
|
|
28
|
-
#
|
|
29
|
-
npx
|
|
24
|
+
npx aeo-ready scan yoursite.com # scan a URL (remote checks)
|
|
25
|
+
npx aeo-ready scan yoursite.com --dir ./public # full scan (local + remote)
|
|
26
|
+
npx aeo-ready scan yoursite.com --json # JSON output for CI
|
|
27
|
+
npx aeo-ready scan yoursite.com --threshold 60 # exit 1 if below
|
|
28
|
+
```
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
npx agent-web scan
|
|
30
|
+
### Why `--dir`?
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
npx agent-web scan --json --threshold 60
|
|
32
|
+
agentic-seo scores ~23/100 in URL-only mode because most checks (content structure, token economics, capability signaling, UX bridge) need filesystem access. Pass `--dir` to your build output or public directory to get the real score.
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
```
|
|
35
|
+
URL-only: agentic-seo 23/100 (F)
|
|
36
|
+
With --dir: agentic-seo 92/100 (A)
|
|
39
37
|
```
|
|
40
38
|
|
|
41
39
|
## Output
|
|
42
40
|
|
|
43
41
|
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Mode: url | Type: saas
|
|
42
|
+
aeo-ready — AEO benchmark aggregator
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
─── Benchmarks ────────────────────────────────────
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
███████████████░ agentic-seo 92/100 (A)
|
|
47
|
+
✓ Discovery 25/25
|
|
48
|
+
◑ Content Structure 19/25
|
|
49
|
+
✓ Token Economics 25/25
|
|
50
|
+
✓ Capability Signaling 15/15
|
|
51
|
+
✓ UX Bridge 8/10
|
|
52
|
+
compare: Cloudflare 55 · Supabase 52 · Vercel 48
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
+ Rich schemas [3]
|
|
62
|
-
- Schema.org markup [0/4]
|
|
63
|
-
fix: type doesn't match site category.
|
|
64
|
-
|
|
65
|
-
Second opinion: agentic-seo scored you 45/100
|
|
66
|
-
```
|
|
54
|
+
█████████████░░░ Cloudflare 4/5 (B)
|
|
55
|
+
+ robotsTxt, + sitemap, + linkHeaders, + agentSkills...
|
|
56
|
+
compare: Cloudflare 5 · Vercel 4 · Supabase 3
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
█████████████░░░ Fern 83/100 (B)
|
|
59
|
+
+ llms-txt-exists, + rendering-strategy, - content-negotiation...
|
|
60
|
+
compare: Stripe 85 · Supabase 78 · Anthropic 72
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
Average across all sources: 85/100
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
npx
|
|
64
|
+
Fix it:
|
|
65
|
+
npx agentic-seo init scaffold llms.txt, AGENTS.md
|
|
66
|
+
Fern: 6 issues — run npx afdocs https://yoursite.com
|
|
74
67
|
```
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
## CI Mode
|
|
77
70
|
|
|
78
71
|
```yaml
|
|
79
|
-
- run: npx
|
|
72
|
+
- run: npx aeo-ready scan yoursite.com --dir ./public --threshold 50
|
|
80
73
|
```
|
|
81
74
|
|
|
82
|
-
##
|
|
75
|
+
## Dashboard
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
Each scan generates a self-contained HTML dashboard at `.aeo-ready/dashboard.html` with:
|
|
78
|
+
- Score cards for each benchmark
|
|
79
|
+
- Per-check detail (expandable)
|
|
80
|
+
- Company comparisons
|
|
81
|
+
- Score trends over time (inline SVG)
|
|
82
|
+
- Scan history with deltas
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
Auto-opens in browser after each scan.
|
|
89
85
|
|
|
90
|
-
##
|
|
91
|
-
|
|
92
|
-
Automatically infers site type from signals:
|
|
93
|
-
- **SaaS** — pricing + auth pages detected
|
|
94
|
-
- **API/Developer Tool** — /docs, /api, SDK references
|
|
95
|
-
- **Content/Blog** — articles, blog posts, publication cadence
|
|
96
|
-
- **Personal/Portfolio** — portfolio, about me, single person
|
|
97
|
-
|
|
98
|
-
Scoring adjusts by type (e.g., OpenAPI is required for SaaS, N/A for personal sites).
|
|
99
|
-
|
|
100
|
-
## Coming Soon
|
|
101
|
-
|
|
102
|
-
- `--fix` mode: generate missing files (robots.txt, meta tags, structured data)
|
|
103
|
-
- HTML dashboard with score trends over time
|
|
104
|
-
- Citation correlation: query AI models and track whether fixes improve citations
|
|
105
|
-
|
|
106
|
-
## Also available as a Claude Code skill
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
npx skills add katrinalaszlo/agent-web
|
|
110
|
-
```
|
|
86
|
+
## History
|
|
111
87
|
|
|
112
|
-
|
|
88
|
+
Scores persist in `.aeo-ready/history.json`. Re-scan to track improvement over time.
|
|
113
89
|
|
|
114
90
|
## Author
|
|
115
91
|
|
package/bin/cli.js
CHANGED
|
@@ -20,7 +20,13 @@ program
|
|
|
20
20
|
|
|
21
21
|
program
|
|
22
22
|
.command("scan [url]")
|
|
23
|
-
.description(
|
|
23
|
+
.description(
|
|
24
|
+
"Run all AEO benchmarks against a URL (add --dir for local scanning)",
|
|
25
|
+
)
|
|
26
|
+
.option(
|
|
27
|
+
"-d, --dir <path>",
|
|
28
|
+
"Local directory to scan (gives agentic-seo full access)",
|
|
29
|
+
)
|
|
24
30
|
.option("--json", "Output results as JSON")
|
|
25
31
|
.option(
|
|
26
32
|
"--threshold <number>",
|
|
@@ -32,10 +38,15 @@ program
|
|
|
32
38
|
if (url && !url.startsWith("http")) url = `https://${url}`;
|
|
33
39
|
if (!url) {
|
|
34
40
|
console.error(" Usage: npx aeo-ready scan <url>");
|
|
41
|
+
console.error(" npx aeo-ready scan <url> --dir ./public");
|
|
35
42
|
process.exit(1);
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
const result = await scan({
|
|
45
|
+
const result = await scan({
|
|
46
|
+
url,
|
|
47
|
+
dir: opts.dir || null,
|
|
48
|
+
json: opts.json || false,
|
|
49
|
+
});
|
|
39
50
|
|
|
40
51
|
if (opts.json) {
|
|
41
52
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
package/package.json
CHANGED
|
@@ -1,14 +1,109 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
|
|
6
|
+
const KNOWN_FILES = [
|
|
7
|
+
"robots.txt",
|
|
8
|
+
"llms.txt",
|
|
9
|
+
"llms-full.txt",
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"CLAUDE.md",
|
|
12
|
+
"skill.md",
|
|
13
|
+
"agent-permissions.json",
|
|
14
|
+
"agents.json",
|
|
15
|
+
"sitemap.xml",
|
|
16
|
+
".well-known/ai-plugin.json",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
async function fetchText(url) {
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
22
|
+
if (!res.ok) return null;
|
|
23
|
+
return await res.text();
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseSitemapUrls(xml, baseUrl) {
|
|
30
|
+
const urls = [];
|
|
31
|
+
const matches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
|
|
32
|
+
for (const m of matches) {
|
|
33
|
+
const loc = m[1].trim();
|
|
34
|
+
if (loc.startsWith(baseUrl)) urls.push(loc);
|
|
35
|
+
}
|
|
36
|
+
return urls;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function urlToFilePath(url, baseUrl) {
|
|
40
|
+
let path = new URL(url).pathname;
|
|
41
|
+
if (path.endsWith("/")) path += "index.html";
|
|
42
|
+
else if (!path.includes(".")) path += ".html";
|
|
43
|
+
return path.replace(/^\//, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchSiteToDir(baseUrl) {
|
|
47
|
+
const tempDir = mkdtempSync(join(tmpdir(), "aeo-"));
|
|
48
|
+
|
|
49
|
+
for (const file of KNOWN_FILES) {
|
|
50
|
+
const content = await fetchText(`${baseUrl}/${file}`);
|
|
51
|
+
if (content) {
|
|
52
|
+
const filePath = join(tempDir, file);
|
|
53
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
54
|
+
writeFileSync(filePath, content);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sitemap = await fetchText(`${baseUrl}/sitemap.xml`);
|
|
59
|
+
if (!sitemap) return tempDir;
|
|
60
|
+
|
|
61
|
+
const urls = parseSitemapUrls(sitemap, baseUrl);
|
|
62
|
+
|
|
63
|
+
await Promise.all(
|
|
64
|
+
urls.map(async (url) => {
|
|
65
|
+
const path = urlToFilePath(url, baseUrl);
|
|
66
|
+
if (KNOWN_FILES.includes(path)) return;
|
|
67
|
+
|
|
68
|
+
const html = await fetchText(url);
|
|
69
|
+
if (html) {
|
|
70
|
+
const htmlPath = join(tempDir, path);
|
|
71
|
+
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
72
|
+
writeFileSync(htmlPath, html);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mdUrl = url
|
|
76
|
+
.replace(/\.html$/, ".md")
|
|
77
|
+
.replace(/\/?$/, (m) => (m === "/" ? "/index.md" : ".md"));
|
|
78
|
+
if (mdUrl !== url) {
|
|
79
|
+
const md = await fetchText(mdUrl);
|
|
80
|
+
if (md) {
|
|
81
|
+
const mdPath = join(tempDir, path.replace(/\.html$/, ".md"));
|
|
82
|
+
mkdirSync(dirname(mdPath), { recursive: true });
|
|
83
|
+
writeFileSync(mdPath, md);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return tempDir;
|
|
90
|
+
}
|
|
2
91
|
|
|
3
92
|
export async function runBenchmark(target) {
|
|
93
|
+
let tempDir = null;
|
|
94
|
+
|
|
4
95
|
try {
|
|
5
|
-
|
|
6
|
-
target && target.startsWith("http")
|
|
7
|
-
? `--url ${target} --json`
|
|
8
|
-
: `${target || "."} --json`;
|
|
96
|
+
let scanDir;
|
|
9
97
|
|
|
10
|
-
|
|
11
|
-
|
|
98
|
+
if (target && target.startsWith("http")) {
|
|
99
|
+
tempDir = await fetchSiteToDir(target);
|
|
100
|
+
scanDir = tempDir;
|
|
101
|
+
} else {
|
|
102
|
+
scanDir = target || ".";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const output = execSync(`npx agentic-seo ${scanDir} --json`, {
|
|
106
|
+
timeout: 60000,
|
|
12
107
|
encoding: "utf8",
|
|
13
108
|
stdio: ["pipe", "pipe", "pipe"],
|
|
14
109
|
});
|
|
@@ -36,5 +131,11 @@ export async function runBenchmark(target) {
|
|
|
36
131
|
available: false,
|
|
37
132
|
reason: err.message?.slice(0, 100),
|
|
38
133
|
};
|
|
134
|
+
} finally {
|
|
135
|
+
if (tempDir) {
|
|
136
|
+
try {
|
|
137
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
39
140
|
}
|
|
40
141
|
}
|
package/src/benchmark/index.js
CHANGED
|
@@ -27,11 +27,11 @@ const REFERENCE_SCORES = {
|
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
export async function runAllBenchmarks(target) {
|
|
30
|
+
export async function runAllBenchmarks(target, dir) {
|
|
31
31
|
const isUrl = target && target.startsWith("http");
|
|
32
32
|
|
|
33
33
|
const results = await Promise.allSettled([
|
|
34
|
-
runAgenticSeo(target),
|
|
34
|
+
runAgenticSeo(dir || target),
|
|
35
35
|
isUrl ? runCloudflare(target) : Promise.resolve(null),
|
|
36
36
|
isUrl ? runFern(target) : Promise.resolve(null),
|
|
37
37
|
]);
|
package/src/scan.js
CHANGED
|
@@ -5,7 +5,7 @@ import { generateDashboard } from "./dashboard/generate.js";
|
|
|
5
5
|
import { exec } from "child_process";
|
|
6
6
|
|
|
7
7
|
export async function scan(opts) {
|
|
8
|
-
const { url, json } = opts;
|
|
8
|
+
const { url, dir, json } = opts;
|
|
9
9
|
|
|
10
10
|
if (!json) {
|
|
11
11
|
console.log(
|
|
@@ -14,7 +14,7 @@ export async function scan(opts) {
|
|
|
14
14
|
console.log(chalk.dim(` Scanning ${url}...\n`));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const benchmarks = await runAllBenchmarks(url);
|
|
17
|
+
const benchmarks = await runAllBenchmarks(url, dir);
|
|
18
18
|
const scores = collectScores(benchmarks);
|
|
19
19
|
const averageScore =
|
|
20
20
|
scores.length > 0
|