aeo-ready 1.4.0 → 1.5.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/CHANGELOG.md +12 -0
- package/README.md +6 -1
- package/package.json +2 -1
- package/src/fix.js +307 -0
- package/src/scan.js +2 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
- Smart auto-fix engine — "Fix now?" actually fixes things
|
|
6
|
+
- Auto-creates agents.txt, sitemap.md, patches robots.txt for AI bots, scaffolds llms.txt
|
|
7
|
+
- Deduplicates overlapping failures across all 5 benchmarks into unified fix actions
|
|
8
|
+
- Prints actionable manual instructions for server-config and platform-specific issues
|
|
9
|
+
- Requires `--dir` for file-based fixes (won't blindly write to CWD)
|
|
10
|
+
|
|
11
|
+
## 1.4.1
|
|
12
|
+
|
|
13
|
+
- Fix false "afdocs failed" message — `afdocs check` exits 1 when it finds failures, which is expected
|
|
14
|
+
|
|
3
15
|
## 1.4.0
|
|
4
16
|
|
|
5
17
|
- Add Vercel Agent Readability benchmark (`@vercel/agent-readability`)
|
package/README.md
CHANGED
|
@@ -88,7 +88,12 @@ With --dir: agentic-seo 92/100 (A)
|
|
|
88
88
|
Fix now? [y/N]
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Say `y` and
|
|
91
|
+
Say `y` and aeo-ready analyzes failures across all 5 benchmarks, deduplicates overlapping issues, and fixes what it can:
|
|
92
|
+
|
|
93
|
+
- **Auto-fixes** (with `--dir`): patches robots.txt for AI bots, creates agents.txt, generates sitemap.md, scaffolds llms.txt/AGENTS.md, adds missing pages to llms.txt
|
|
94
|
+
- **Manual instructions**: prints actionable steps for server config (content negotiation, .md URLs, Vary header) and platform-specific issues
|
|
95
|
+
|
|
96
|
+
Non-interactive in CI (`--json` or non-TTY).
|
|
92
97
|
|
|
93
98
|
## CI Mode
|
|
94
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aeo-ready",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, Fern, Vercel, and AgentGrade in one report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"src/history/",
|
|
40
40
|
"src/index.js",
|
|
41
41
|
"src/scan.js",
|
|
42
|
+
"src/fix.js",
|
|
42
43
|
"skills/",
|
|
43
44
|
"README.md",
|
|
44
45
|
"CHANGELOG.md"
|
package/src/fix.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
const AI_BOTS = [
|
|
7
|
+
"GPTBot",
|
|
8
|
+
"OAI-SearchBot",
|
|
9
|
+
"ClaudeBot",
|
|
10
|
+
"Claude-User",
|
|
11
|
+
"Claude-SearchBot",
|
|
12
|
+
"Google-Extended",
|
|
13
|
+
"CCBot",
|
|
14
|
+
"PerplexityBot",
|
|
15
|
+
"Meta-ExternalAgent",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const FIX_ACTIONS = [
|
|
19
|
+
{
|
|
20
|
+
key: "robots-ai-rules",
|
|
21
|
+
label: "Add AI bot rules to robots.txt",
|
|
22
|
+
auto: true,
|
|
23
|
+
match: (id) => /^robots\.txt$|robots.*blocked/i.test(id),
|
|
24
|
+
apply: patchRobotsTxt,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "llms-txt-bootstrap",
|
|
28
|
+
label: "Scaffold llms.txt and AGENTS.md",
|
|
29
|
+
auto: true,
|
|
30
|
+
match: () => false,
|
|
31
|
+
apply: bootstrapLlmsTxt,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "agents-txt",
|
|
35
|
+
label: "Create agents.txt",
|
|
36
|
+
auto: true,
|
|
37
|
+
match: (id) => /agents\.txt/i.test(id),
|
|
38
|
+
apply: createAgentsTxt,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "sitemap-md",
|
|
42
|
+
label: "Generate sitemap.md from sitemap.xml",
|
|
43
|
+
auto: true,
|
|
44
|
+
match: (id) => /^sitemap\.md$/i.test(id),
|
|
45
|
+
apply: generateSitemapMd,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "llms-txt-coverage",
|
|
49
|
+
label: "Add missing pages to llms.txt",
|
|
50
|
+
auto: true,
|
|
51
|
+
match: (id) => id === "llms-txt-coverage",
|
|
52
|
+
apply: patchLlmsTxtCoverage,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "llms-txt-directive",
|
|
56
|
+
label:
|
|
57
|
+
'Add <link rel="llms-txt" href="/llms.txt"> to your HTML <head> template',
|
|
58
|
+
auto: false,
|
|
59
|
+
match: (id) =>
|
|
60
|
+
/llms-txt-directive|llms.*linked.*html|llms-full.*linked/i.test(id),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "content-negotiation",
|
|
64
|
+
label:
|
|
65
|
+
"Configure server to return markdown for Accept: text/markdown requests",
|
|
66
|
+
auto: false,
|
|
67
|
+
match: (id) =>
|
|
68
|
+
/content.negotiation|agent ua.*markdown|accept.*markdown|accept.*text.*returns|accept.*json.*returns|preferred content.type/i.test(
|
|
69
|
+
id,
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "md-url-support",
|
|
74
|
+
label: "Serve markdown at .md URLs (e.g. /docs/page.md)",
|
|
75
|
+
auto: false,
|
|
76
|
+
match: (id) => /markdown.url.support|\.md.*url.*markdown/i.test(id),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "vary-header",
|
|
80
|
+
label: "Add Vary: Accept response header for content negotiation caching",
|
|
81
|
+
auto: false,
|
|
82
|
+
match: (id) => /^vary/i.test(id),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: "content-start",
|
|
86
|
+
label: "Move nav/chrome below main content so agents find content earlier",
|
|
87
|
+
auto: false,
|
|
88
|
+
match: (id) => /content.start.position/i.test(id),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "markdown-parity",
|
|
92
|
+
label:
|
|
93
|
+
"Ensure markdown versions match HTML content (check tabbed/accordion sections)",
|
|
94
|
+
auto: false,
|
|
95
|
+
match: (id) => /markdown.content.parity/i.test(id),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "redirect",
|
|
99
|
+
label: "Avoid cross-host redirects — agents may not follow them",
|
|
100
|
+
auto: false,
|
|
101
|
+
match: (id) => /redirect behavior/i.test(id),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "json-ld",
|
|
105
|
+
label: "Add Organization JSON-LD to <head> — see schema.org/Organization",
|
|
106
|
+
auto: false,
|
|
107
|
+
match: (id) => /organization.*json.ld|json.ld.*organization/i.test(id),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "identity",
|
|
111
|
+
label:
|
|
112
|
+
"Identity protocols (WebFinger, DID, A2A Agent Card) — see agentgrade.com",
|
|
113
|
+
auto: false,
|
|
114
|
+
match: (id) =>
|
|
115
|
+
/webfinger|did document|nostr|at protocol|agent card|webmcp/i.test(id),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
function collectFailedChecks(benchmarks) {
|
|
120
|
+
const failed = [];
|
|
121
|
+
for (const key of ["cloudflare", "fern", "vercel", "agentgrade"]) {
|
|
122
|
+
const b = benchmarks[key];
|
|
123
|
+
if (!b?.available || !b.checks) continue;
|
|
124
|
+
for (const check of b.checks) {
|
|
125
|
+
if (check.status === "fail" || check.status === "warn") {
|
|
126
|
+
failed.push({ ...check, benchmark: key });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return failed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dedup(failedChecks) {
|
|
134
|
+
const triggered = new Map();
|
|
135
|
+
for (const check of failedChecks) {
|
|
136
|
+
for (const action of FIX_ACTIONS) {
|
|
137
|
+
if (triggered.has(action.key)) continue;
|
|
138
|
+
if (action.match(check.id)) {
|
|
139
|
+
triggered.set(action.key, action);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return triggered;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function runFixes(result, dir) {
|
|
148
|
+
const failed = collectFailedChecks(result.benchmarks);
|
|
149
|
+
if (failed.length === 0) {
|
|
150
|
+
console.log(chalk.green("\n All checks passed!\n"));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const triggered = dedup(failed);
|
|
155
|
+
|
|
156
|
+
if (!triggered.has("llms-txt-bootstrap") && dir) {
|
|
157
|
+
const llms = join(dir, "llms.txt");
|
|
158
|
+
const agents = join(dir, "AGENTS.md");
|
|
159
|
+
if (!existsSync(llms) || !existsSync(agents)) {
|
|
160
|
+
triggered.set("llms-txt-bootstrap", FIX_ACTIONS[1]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fixed = [];
|
|
165
|
+
const skipped = [];
|
|
166
|
+
const manual = [];
|
|
167
|
+
|
|
168
|
+
for (const [, action] of triggered) {
|
|
169
|
+
if (!action.auto) {
|
|
170
|
+
manual.push(action.label);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!dir) {
|
|
174
|
+
skipped.push(action.label);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const r = await action.apply(dir, result);
|
|
178
|
+
if (r) fixed.push(r);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log("");
|
|
182
|
+
if (fixed.length > 0) {
|
|
183
|
+
console.log(
|
|
184
|
+
chalk.bold(
|
|
185
|
+
` Fixed ${fixed.length} issue${fixed.length > 1 ? "s" : ""}:\n`,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
for (const f of fixed) {
|
|
189
|
+
console.log(` ${chalk.green("✓")} ${f}`);
|
|
190
|
+
}
|
|
191
|
+
console.log("");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (skipped.length > 0) {
|
|
195
|
+
console.log(
|
|
196
|
+
chalk.bold(" Skipped") + chalk.dim(" (run with --dir to auto-fix):\n"),
|
|
197
|
+
);
|
|
198
|
+
for (const s of skipped) {
|
|
199
|
+
console.log(` ${chalk.yellow("-")} ${s}`);
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (manual.length > 0) {
|
|
205
|
+
console.log(chalk.bold(" Manual fixes needed:\n"));
|
|
206
|
+
manual.forEach((m, i) => {
|
|
207
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${m}`);
|
|
208
|
+
});
|
|
209
|
+
console.log("");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const rescan = `npx aeo-ready scan ${result.url}${dir ? ` --dir ${dir}` : ""}`;
|
|
213
|
+
if (fixed.length > 0) {
|
|
214
|
+
console.log(chalk.dim(" Deploy, then re-scan: ") + rescan + "\n");
|
|
215
|
+
} else {
|
|
216
|
+
console.log(chalk.dim(" Re-scan to verify: ") + rescan + "\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function patchRobotsTxt(dir) {
|
|
221
|
+
const file = join(dir, "robots.txt");
|
|
222
|
+
let content = "";
|
|
223
|
+
if (existsSync(file)) {
|
|
224
|
+
content = readFileSync(file, "utf8");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const missing = AI_BOTS.filter(
|
|
228
|
+
(bot) => !content.toLowerCase().includes(bot.toLowerCase()),
|
|
229
|
+
);
|
|
230
|
+
if (missing.length === 0) return null;
|
|
231
|
+
|
|
232
|
+
const block = missing
|
|
233
|
+
.map((bot) => `User-agent: ${bot}\nAllow: /`)
|
|
234
|
+
.join("\n\n");
|
|
235
|
+
const sep =
|
|
236
|
+
content.length > 0 && !content.endsWith("\n")
|
|
237
|
+
? "\n\n"
|
|
238
|
+
: content.length > 0
|
|
239
|
+
? "\n"
|
|
240
|
+
: "";
|
|
241
|
+
writeFileSync(file, content + sep + block + "\n");
|
|
242
|
+
return `Added ${missing.length} AI bot rules to robots.txt`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function bootstrapLlmsTxt(dir) {
|
|
246
|
+
const llms = join(dir, "llms.txt");
|
|
247
|
+
const agents = join(dir, "AGENTS.md");
|
|
248
|
+
if (existsSync(llms) && existsSync(agents)) return null;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
execFileSync("npx", ["agentic-seo", "init", dir], {
|
|
252
|
+
timeout: 30000,
|
|
253
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
254
|
+
});
|
|
255
|
+
return "Scaffolded llms.txt and AGENTS.md via agentic-seo";
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.log(
|
|
258
|
+
chalk.red(` agentic-seo init failed: ${err.message?.slice(0, 80)}`),
|
|
259
|
+
);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createAgentsTxt(dir) {
|
|
265
|
+
const file = join(dir, "agents.txt");
|
|
266
|
+
if (existsSync(file)) return null;
|
|
267
|
+
|
|
268
|
+
writeFileSync(file, "# agents.txt\nUser-agent: *\nAllow: /\n");
|
|
269
|
+
return "Created agents.txt with default permissions";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function generateSitemapMd(dir) {
|
|
273
|
+
const file = join(dir, "sitemap.md");
|
|
274
|
+
if (existsSync(file)) return null;
|
|
275
|
+
|
|
276
|
+
const xmlFile = join(dir, "sitemap.xml");
|
|
277
|
+
if (!existsSync(xmlFile)) return null;
|
|
278
|
+
|
|
279
|
+
const xml = readFileSync(xmlFile, "utf8");
|
|
280
|
+
const urls = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
|
|
281
|
+
if (urls.length === 0) return null;
|
|
282
|
+
|
|
283
|
+
const lines = ["# Sitemap\n", ...urls.map((u) => `- [${u}](${u})`)];
|
|
284
|
+
writeFileSync(file, lines.join("\n") + "\n");
|
|
285
|
+
return `Generated sitemap.md with ${urls.length} URLs`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function patchLlmsTxtCoverage(dir, result) {
|
|
289
|
+
const llmsFile = join(dir, "llms.txt");
|
|
290
|
+
const xmlFile = join(dir, "sitemap.xml");
|
|
291
|
+
if (!existsSync(llmsFile) || !existsSync(xmlFile)) return null;
|
|
292
|
+
|
|
293
|
+
const llms = readFileSync(llmsFile, "utf8");
|
|
294
|
+
const xml = readFileSync(xmlFile, "utf8");
|
|
295
|
+
const sitemapUrls = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map(
|
|
296
|
+
(m) => m[1],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const missing = sitemapUrls.filter((u) => !llms.includes(u));
|
|
300
|
+
if (missing.length === 0) return null;
|
|
301
|
+
|
|
302
|
+
const block =
|
|
303
|
+
"\n## Additional Pages\n\n" +
|
|
304
|
+
missing.map((u) => `- [${u}](${u})`).join("\n");
|
|
305
|
+
writeFileSync(llmsFile, llms.trimEnd() + "\n" + block + "\n");
|
|
306
|
+
return `Added ${missing.length} missing pages to llms.txt`;
|
|
307
|
+
}
|
package/src/scan.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { createInterface } from "readline";
|
|
3
|
-
import { execFileSync } from "child_process";
|
|
4
3
|
import { runAllBenchmarks, printBenchmarks } from "./benchmark/index.js";
|
|
5
4
|
import { saveResult } from "./history/index.js";
|
|
5
|
+
import { runFixes } from "./fix.js";
|
|
6
6
|
|
|
7
7
|
export async function scan(opts) {
|
|
8
8
|
const { url, dir, json } = opts;
|
|
@@ -144,37 +144,5 @@ function ask(question) {
|
|
|
144
144
|
async function promptFix(result, dir) {
|
|
145
145
|
const answer = await ask(chalk.bold(" Fix now? ") + chalk.dim("[y/N] "));
|
|
146
146
|
if (answer !== "y" && answer !== "yes") return;
|
|
147
|
-
|
|
148
|
-
console.log("");
|
|
149
|
-
|
|
150
|
-
const targetDir = dir || ".";
|
|
151
|
-
|
|
152
|
-
console.log(chalk.dim(` Running: npx agentic-seo init ${targetDir}\n`));
|
|
153
|
-
try {
|
|
154
|
-
execFileSync("npx", ["agentic-seo", "init", targetDir], {
|
|
155
|
-
stdio: "inherit",
|
|
156
|
-
});
|
|
157
|
-
} catch (err) {
|
|
158
|
-
console.log(chalk.red(`\n agentic-seo init failed: ${err.message}\n`));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const fernFails =
|
|
162
|
-
result.benchmarks.fern?.checks?.filter(
|
|
163
|
-
(c) => c.status === "fail" || c.status === "warn",
|
|
164
|
-
) || [];
|
|
165
|
-
if (fernFails.length > 0) {
|
|
166
|
-
console.log(chalk.dim(`\n Running: npx afdocs check ${result.url}\n`));
|
|
167
|
-
try {
|
|
168
|
-
execFileSync("npx", ["afdocs", "check", result.url], {
|
|
169
|
-
stdio: "inherit",
|
|
170
|
-
});
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.log(chalk.red(`\n afdocs failed: ${err.message}\n`));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.log(
|
|
177
|
-
chalk.dim("\n Re-scan to verify: ") +
|
|
178
|
-
`npx aeo-ready scan ${result.url}${dir ? ` --dir ${dir}` : ""}\n`,
|
|
179
|
-
);
|
|
147
|
+
await runFixes(result, dir);
|
|
180
148
|
}
|