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.
package/README.md CHANGED
@@ -1,115 +1,91 @@
1
- # agent-web
1
+ # aeo-ready
2
2
 
3
- Is your site AI-ready? One scan, two scorecards, one score.
3
+ AEO benchmark aggregator. One scan, every score.
4
4
 
5
5
  ```bash
6
- npx agent-web scan --url https://your-site.com
6
+ npx aeo-ready scan yoursite.com
7
7
  ```
8
8
 
9
- ## What it measures
9
+ ## What it does
10
10
 
11
- **Agent Readiness** (0-50) Can AI agents discover, parse, and act on your site?
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
- **AI Visibility** (0-50) — Does your content get cited in AI-generated responses?
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
- **Overall: X/100** with letter grade (A-F).
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
- # Audit a live URL
29
- npx agent-web scan --url https://example.com
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
- # Audit current directory (repo mode)
32
- npx agent-web scan
30
+ ### Why `--dir`?
33
31
 
34
- # JSON output for CI pipelines
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
- # Skip benchmark comparison
38
- npx agent-web scan --no-benchmark
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
- agent-webAI readiness audit
45
-
46
- Mode: url | Type: saas
42
+ aeo-readyAEO benchmark aggregator
47
43
 
48
- Overall: C 50/100
44
+ ─── Benchmarks ────────────────────────────────────
49
45
 
50
- Agent Readiness: 22/50
51
- discovery: 3/12
52
- + AI-friendly meta tags [3]
53
- - llms.txt [0/4]
54
- fix: No llms.txt found.
55
- - robots.txt AI crawlers [0/3]
56
- fix: robots.txt exists but doesn't mention AI crawlers.
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
- AI Visibility: 28/50
59
- structured Data: 6/12
60
- + FAQ markup [3]
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
- ## CI Mode
58
+ █████████████░░░ Fern 83/100 (B)
59
+ + llms-txt-exists, + rendering-strategy, - content-negotiation...
60
+ compare: Stripe 85 · Supabase 78 · Anthropic 72
69
61
 
70
- Exit with code 1 if below threshold:
62
+ Average across all sources: 85/100
71
63
 
72
- ```bash
73
- npx agent-web scan --json --threshold 60
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
- Add to GitHub Actions:
69
+ ## CI Mode
77
70
 
78
71
  ```yaml
79
- - run: npx agent-web scan --threshold 50
72
+ - run: npx aeo-ready scan yoursite.com --dir ./public --threshold 50
80
73
  ```
81
74
 
82
- ## Benchmark
75
+ ## Dashboard
83
76
 
84
- Runs `npx agentic-seo --json` as an independent second opinion. Shows their score alongside yours. If agentic-seo isn't installed, the benchmark line is skipped gracefully.
85
-
86
- ## History
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
- Each scan saves to `.agent-web/history.json`. Gitignored by default.
84
+ Auto-opens in browser after each scan.
89
85
 
90
- ## Site Type Detection
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
- Then use `/agent-web` or `/agent-web https://example.com` inside Claude Code for an interactive audit that also generates missing files.
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
@@ -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,44 @@ const program = new Command();
21
15
 
22
16
  program
23
17
  .name("aeo-ready")
24
- .description("Is your site AEO ready? Two scorecards, one score.")
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 a full audit (Agent Readiness + AI Visibility)")
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(
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
+ )
34
30
  .option("--json", "Output results as JSON")
35
31
  .option(
36
32
  "--threshold <number>",
37
- "Minimum score to pass (exit 1 if below)",
33
+ "Minimum average score to pass (exit 1 if below)",
38
34
  parseInt,
39
35
  )
40
- .option("--no-benchmark", "Skip agentic-seo benchmark")
41
36
  .action(async (url, opts) => {
42
37
  try {
43
- const isUrl = url && url.startsWith("http");
44
- const dir = isUrl ? null : process.cwd();
45
- const quiet = opts.fix && dir;
46
- const result = await scan({
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
- }
38
+ if (url && !url.startsWith("http")) url = `https://${url}`;
39
+ if (!url) {
40
+ console.error(" Usage: npx aeo-ready scan <url>");
41
+ console.error(" npx aeo-ready scan <url> --dir ./public");
42
+ process.exit(1);
67
43
  }
68
44
 
69
- if (opts.fix && dir) {
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
- }
45
+ const result = await scan({
46
+ url,
47
+ dir: opts.dir || null,
48
+ json: opts.json || false,
49
+ });
78
50
 
79
51
  if (opts.json) {
80
52
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
81
53
  }
82
54
 
83
- if (opts.threshold && result.score < opts.threshold) {
55
+ if (opts.threshold && result.averageScore < opts.threshold) {
84
56
  process.exit(1);
85
57
  }
86
58
  } catch (err) {
@@ -89,18 +61,4 @@ program
89
61
  }
90
62
  });
91
63
 
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
64
  program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aeo-ready",
3
- "version": "1.1.0",
4
- "description": "Is your site AEO ready? Two scorecards: Agent Readiness + AI Visibility. One scan, one score.",
3
+ "version": "1.3.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"
@@ -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
- const args =
6
- target && target.startsWith("http")
7
- ? `--url ${target} --json`
8
- : `${target || "."} --json`;
96
+ let scanDir;
9
97
 
10
- const output = execSync(`npx agentic-seo ${args}`, {
11
- timeout: 30000,
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
  }
@@ -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
  ]);
@@ -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 { renderRecommendations } from "./sections/recommendations.js";
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, opts.beforeResult || null),
23
- "agent-readiness-scorecard": renderAgentReadiness(scanResult),
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
- recommendations: renderRecommendations(scanResult),
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 — AI Readiness Dashboard</title>
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">Overall Score</a>
73
- <a href="#agent-readiness" class="section-head">Agent Readiness</a>
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>AI Readiness Dashboard</h1>
83
- <p class="subtitle">${target} · Updated ${timestamp}</p>
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:agent-readiness-scorecard -->
90
- ${sections["agent-readiness-scorecard"]}
91
- <!-- /SECTION:agent-readiness-scorecard -->
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
- .score-hero { text-align: center; padding: 32px 0; }
127
- .score-hero .grade { font-size: 64px; font-weight: 700; }
128
- .score-hero .number { font-size: 24px; color: #8b949e; margin-top: 4px; }
129
- .score-hero .bar { margin: 16px auto; width: 300px; height: 8px; background: #21262d; border-radius: 4px; overflow: hidden; }
130
- .score-hero .bar-fill { height: 100%; border-radius: 4px; }
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
- .scorecard { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; margin: 16px 0; }
134
- .scorecard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
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
- .check .fix { display: block; color: #8b949e; font-size: 11px; padding-left: 16px; margin-top: 2px; }
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, "&amp;")
77
+ .replace(/</g, "&lt;")
78
+ .replace(/>/g, "&gt;");
79
+ }