@rivi-software/agentic-seo-check 0.1.0 → 0.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/LICENSE +1 -1
- package/README.md +124 -18
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +22 -0
- package/dist/brand.js.map +1 -0
- package/dist/cli.js +174 -86
- package/dist/cli.js.map +1 -1
- package/dist/core/aeo/audit.d.ts +44 -0
- package/dist/core/aeo/audit.js +310 -0
- package/dist/core/aeo/audit.js.map +1 -0
- package/dist/core/aeo/bots.d.ts +30 -0
- package/dist/core/aeo/bots.js +299 -0
- package/dist/core/aeo/bots.js.map +1 -0
- package/dist/core/aeo/llms-txt.d.ts +8 -0
- package/dist/core/aeo/llms-txt.js +88 -0
- package/dist/core/aeo/llms-txt.js.map +1 -0
- package/dist/core/aeo/page.d.ts +53 -0
- package/dist/core/aeo/page.js +312 -0
- package/dist/core/aeo/page.js.map +1 -0
- package/dist/core/aeo/probe.d.ts +10 -0
- package/dist/core/aeo/probe.js +53 -0
- package/dist/core/aeo/probe.js.map +1 -0
- package/dist/core/aeo/site-rules.d.ts +13 -0
- package/dist/core/aeo/site-rules.js +178 -0
- package/dist/core/aeo/site-rules.js.map +1 -0
- package/dist/core/audit.d.ts +37 -16
- package/dist/core/audit.js +389 -249
- package/dist/core/audit.js.map +1 -1
- package/dist/core/concurrency.d.ts +4 -4
- package/dist/core/concurrency.js +33 -23
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/crawl.d.ts +9 -5
- package/dist/core/crawl.js +271 -123
- package/dist/core/crawl.js.map +1 -1
- package/dist/core/fetch.d.ts +5 -5
- package/dist/core/fetch.js +151 -100
- package/dist/core/fetch.js.map +1 -1
- package/dist/core/html/answer.d.ts +4 -0
- package/dist/core/html/answer.js +198 -0
- package/dist/core/html/answer.js.map +1 -0
- package/dist/core/html/dom.d.ts +17 -0
- package/dist/core/html/dom.js +83 -0
- package/dist/core/html/dom.js.map +1 -0
- package/dist/core/html/extract.d.ts +2 -2
- package/dist/core/html/extract.js +102 -111
- package/dist/core/html/extract.js.map +1 -1
- package/dist/core/html/structured-data.d.ts +16 -6
- package/dist/core/html/structured-data.js +180 -145
- package/dist/core/html/structured-data.js.map +1 -1
- package/dist/core/html/text.d.ts +14 -14
- package/dist/core/html/text.js +110 -107
- package/dist/core/html/text.js.map +1 -1
- package/dist/core/lighthouse.d.ts +26 -25
- package/dist/core/lighthouse.js +181 -105
- package/dist/core/lighthouse.js.map +1 -1
- package/dist/core/performance-rules.d.ts +2 -2
- package/dist/core/performance-rules.js +105 -33
- package/dist/core/performance-rules.js.map +1 -1
- package/dist/core/render-rules.d.ts +5 -4
- package/dist/core/render-rules.js +129 -62
- package/dist/core/render-rules.js.map +1 -1
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +29 -23
- package/dist/core/render.js.map +1 -1
- package/dist/core/report/markdown.d.ts +4 -2
- package/dist/core/report/markdown.js +210 -129
- package/dist/core/report/markdown.js.map +1 -1
- package/dist/core/robots.d.ts +12 -5
- package/dist/core/robots.js +43 -17
- package/dist/core/robots.js.map +1 -1
- package/dist/core/rules/aeo.d.ts +11 -0
- package/dist/core/rules/aeo.js +423 -0
- package/dist/core/rules/aeo.js.map +1 -0
- package/dist/core/rules/canonical.d.ts +1 -1
- package/dist/core/rules/canonical.js +93 -57
- package/dist/core/rules/canonical.js.map +1 -1
- package/dist/core/rules/content.js +40 -26
- package/dist/core/rules/content.js.map +1 -1
- package/dist/core/rules/document.js +64 -41
- package/dist/core/rules/document.js.map +1 -1
- package/dist/core/rules/headings.js +58 -38
- package/dist/core/rules/headings.js.map +1 -1
- package/dist/core/rules/hreflang.js +70 -38
- package/dist/core/rules/hreflang.js.map +1 -1
- package/dist/core/rules/images.js +44 -26
- package/dist/core/rules/images.js.map +1 -1
- package/dist/core/rules/index.d.ts +10 -6
- package/dist/core/rules/index.js +43 -36
- package/dist/core/rules/index.js.map +1 -1
- package/dist/core/rules/indexing.d.ts +2 -2
- package/dist/core/rules/indexing.js +48 -26
- package/dist/core/rules/indexing.js.map +1 -1
- package/dist/core/rules/keyword.js +84 -47
- package/dist/core/rules/keyword.js.map +1 -1
- package/dist/core/rules/links.js +72 -37
- package/dist/core/rules/links.js.map +1 -1
- package/dist/core/rules/meta.js +55 -34
- package/dist/core/rules/meta.js.map +1 -1
- package/dist/core/rules/response.js +75 -41
- package/dist/core/rules/response.js.map +1 -1
- package/dist/core/rules/rule.d.ts +6 -6
- package/dist/core/rules/rule.js +36 -36
- package/dist/core/rules/rule.js.map +1 -1
- package/dist/core/rules/social.js +86 -45
- package/dist/core/rules/social.js.map +1 -1
- package/dist/core/rules/structured-data.d.ts +6 -4
- package/dist/core/rules/structured-data.js +314 -102
- package/dist/core/rules/structured-data.js.map +1 -1
- package/dist/core/rules/title.js +48 -33
- package/dist/core/rules/title.js.map +1 -1
- package/dist/core/score.d.ts +5 -3
- package/dist/core/score.js +11 -9
- package/dist/core/score.js.map +1 -1
- package/dist/core/site-rules.d.ts +3 -3
- package/dist/core/site-rules.js +409 -198
- package/dist/core/site-rules.js.map +1 -1
- package/dist/core/sitemap.d.ts +5 -5
- package/dist/core/sitemap.js +109 -64
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/static.d.ts +2 -2
- package/dist/core/static.js +210 -112
- package/dist/core/static.js.map +1 -1
- package/dist/core/summary.d.ts +2 -2
- package/dist/core/summary.js +11 -6
- package/dist/core/summary.js.map +1 -1
- package/dist/core/types.d.ts +189 -14
- package/dist/core/types.js +15 -8
- package/dist/core/types.js.map +1 -1
- package/dist/core/url.d.ts +11 -11
- package/dist/core/url.js +122 -70
- package/dist/core/url.js.map +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +7 -4
- package/dist/mcp-bin.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +437 -121
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +0 -1
- package/dist/version.js +4 -2
- package/dist/version.js.map +1 -1
- package/package.json +32 -10
- package/skills/seo-audit/SKILL.md +8 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://rivi.software">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rivi-software/agentic-seo-check/main/assets/rivi-agentic-seo-check-on-dark.svg">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/rivi-software/agentic-seo-check/main/assets/rivi-agentic-seo-check.svg" alt="RIVI agentic-seo-check" width="545">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
SEO and AEO analysis built for coding agents: a CLI and an MCP server that audit URLs,<br>
|
|
12
|
+
static build output or raw HTML, and return findings an agent can act on.<br>
|
|
13
|
+
Open source, from <a href="https://rivi.software">RIVI Software</a>.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://github.com/rivi-software/agentic-seo-check/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/rivi-software/agentic-seo-check/ci.yml?branch=main&label=CI" alt="CI status"></a>
|
|
18
|
+
<a href="https://www.npmjs.com/package/@rivi-software/agentic-seo-check"><img src="https://img.shields.io/npm/v/@rivi-software/agentic-seo-check?color=0b6b3a" alt="npm version"></a>
|
|
19
|
+
<a href="https://www.npmjs.com/package/@rivi-software/agentic-seo-check"><img src="https://img.shields.io/npm/dm/@rivi-software/agentic-seo-check?color=0b6b3a" alt="Monthly npm downloads"></a>
|
|
20
|
+
<a href="https://github.com/rivi-software/agentic-seo-check/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-0b6b3a" alt="MIT License"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="#install">Install</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="#cli">CLI</a>
|
|
27
|
+
·
|
|
28
|
+
<a href="#mcp-server">MCP server</a>
|
|
29
|
+
·
|
|
30
|
+
<a href="#agent-skill">Agent skill</a>
|
|
31
|
+
·
|
|
32
|
+
<a href="https://github.com/rivi-software/agentic-seo-check/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
33
|
+
·
|
|
34
|
+
<a href="https://rivi.software">rivi.software</a>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<br>
|
|
38
|
+
|
|
39
|
+
agentic-seo-check is one core with two interfaces:
|
|
6
40
|
|
|
7
41
|
- **CLI** `agentic-seo-check` with JSON (default when piped) or Markdown output and CI-friendly exit codes.
|
|
8
|
-
- **MCP server** exposing
|
|
42
|
+
- **MCP server** exposing six tools for Claude Code, Cursor, Codex and any other MCP client.
|
|
9
43
|
|
|
10
44
|
It audits **URLs** (production, staging, `localhost` dev servers), **static build output** (`dist/`, `out/`, `build/`) and **raw HTML**, and returns findings an agent can act on: a stable rule id, severity, what is wrong, the fix, a CSS selector for where it lives, and evidence with the current values.
|
|
11
45
|
|
|
46
|
+
Two audits share that core: the **SEO audit** (`audit`, `crawl`) for search engines, and the **AEO audit** (`aeo`, answer engine optimization) for AI answer engines such as ChatGPT search, Claude, Perplexity, Google AI Overviews and AI Mode, Copilot and Gemini. Each has its own rules and its own score, so adding one never changes the other.
|
|
47
|
+
|
|
12
48
|
## What it checks
|
|
13
49
|
|
|
14
|
-
|
|
50
|
+
### SEO audit
|
|
51
|
+
|
|
52
|
+
| Area | Examples (123 rules, `agentic-seo-check rules` lists them) |
|
|
15
53
|
| --- | --- |
|
|
16
54
|
| On-page basics | title missing/long/short/placeholder ("React App"), meta description, H1 and heading outline, canonical (missing, conflicting, relative, pointing elsewhere), robots meta and `X-Robots-Tag`, `lang`, viewport, charset, doctype |
|
|
17
55
|
| Social | Open Graph completeness, absolute `og:image`, `og:url` vs canonical, Twitter card |
|
|
@@ -24,6 +62,16 @@ It audits **URLs** (production, staging, `localhost` dev servers), **static buil
|
|
|
24
62
|
| Rendering (`--render`, Playwright) | title/description/canonical/H1/content/links/structured data that only exist after JavaScript runs; noindex injected or removed by JavaScript; console errors |
|
|
25
63
|
| Performance (`--lighthouse`, Chrome) | LCP, CLS, TBT, FCP, Speed Index, TTFB with Core Web Vitals thresholds, Lighthouse opportunities with estimated savings, failed Lighthouse SEO and agentic-browsing audits |
|
|
26
64
|
|
|
65
|
+
### AEO audit
|
|
66
|
+
|
|
67
|
+
| Area | Examples (30 rules, `agentic-seo-check rules --category aeo` lists them) |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| AI crawler access | robots.txt checked per bot: search crawlers whose index AI answers draw on (Googlebot, Bingbot, Applebot; blocked is an error), answer bots (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Google-Extended, DuckAssistBot, MistralAI-User; a warning) and training bots (GPTBot, ClaudeBot, Applebot-Extended, CCBot, meta-externalagent; info, since opting out of training is a legitimate choice). Also an unreachable robots.txt, `Content-Signal` lines (`ai-input=no`, invalid values), the shared training opt-out `User-Agent: __usage_ai-training`, and `/llms.txt` (missing, no H1 title, no links) |
|
|
70
|
+
| Server blocks (`--ai-bots`) | requests the page with each AI bot's User-Agent and reports 403s, 429s and challenge pages that a CDN or firewall serves while robots.txt allows the bot |
|
|
71
|
+
| Snippet and AI opt-outs | noindex, `nosnippet` and `max-snippet` (Google AI Overviews and AI Mode), `noarchive` and `nocache` (Bing's controls for Copilot), `noai`/`noimageai`, `data-nosnippet` over a large share of the text; meta tags per crawler and scoped `X-Robots-Tag` values |
|
|
72
|
+
| Answer-ready content | an empty JavaScript app shell (most AI crawlers do not run JavaScript), question headings without a short direct answer, long text without subheadings, lists or tables, FAQPage/QAPage/HowTo markup whose questions, answers or steps the page does not show |
|
|
73
|
+
| Entity and freshness | article author missing or a bare name without url/sameAs, missing datePublished/dateModified, a structured-data date the page does not show, a home page without Organization or Person markup, entities without sameAs links |
|
|
74
|
+
|
|
27
75
|
## Install
|
|
28
76
|
|
|
29
77
|
Requires Node 22.19 or newer. Published on npm as [`@rivi-software/agentic-seo-check`](https://www.npmjs.com/package/@rivi-software/agentic-seo-check); the commands it installs are `agentic-seo-check` and `agentic-seo-check-mcp`.
|
|
@@ -62,11 +110,20 @@ agentic-seo-check audit https://www.example.com/widgets --keyword "blue widgets"
|
|
|
62
110
|
# CI: exit 1 when errors exist, smaller output
|
|
63
111
|
agentic-seo-check audit https://staging.example.com --crawl --fail-on error --compact --no-info
|
|
64
112
|
|
|
113
|
+
# AEO: AI crawler access, opt-outs and answer-ready content, with its own score
|
|
114
|
+
agentic-seo-check aeo https://www.example.com/guide
|
|
115
|
+
agentic-seo-check aeo https://www.example.com --crawl --max-pages 50
|
|
116
|
+
agentic-seo-check aeo https://www.example.com --ai-bots # also catch CDN/firewall blocks
|
|
117
|
+
agentic-seo-check aeo ./dist --base-url https://www.example.com # reads dist/robots.txt and dist/llms.txt
|
|
118
|
+
|
|
65
119
|
# rule reference
|
|
66
120
|
agentic-seo-check rules --category structured-data
|
|
121
|
+
agentic-seo-check rules --category aeo
|
|
67
122
|
agentic-seo-check rules --rule canonical.multiple
|
|
68
123
|
```
|
|
69
124
|
|
|
125
|
+
`aeo` takes the same targets and output, crawl and request options as `audit`, plus `--ai-bots`; it has no `--keyword`, `--render`, `--lighthouse` or `--check-external`. With `--ai-bots`, the requests come from your machine with each bot's User-Agent, not from the bots' own IP ranges: a CDN that verifies bots may treat the real crawler differently, and the findings say so. Search crawlers and robots.txt-only tokens (Google-Extended, Applebot-Extended) are never impersonated.
|
|
126
|
+
|
|
70
127
|
Exit codes: `0` success, `1` findings at or above `--fail-on`, `2` usage or runtime error. Progress goes to stderr, the report to stdout.
|
|
71
128
|
|
|
72
129
|
## MCP server
|
|
@@ -92,6 +149,7 @@ Or `claude mcp add agentic-seo-check -- npx -y @rivi-software/agentic-seo-check
|
|
|
92
149
|
| `seo_crawl_site` | Crawl and audit a site. Options: `maxPages`, `maxDepth`, `concurrency`, `delayMs`, `respectRobots`, `includeSubdomains`, `checkExternalLinks`, `includePatterns`, `excludePatterns`, `pageDetail`. |
|
|
93
150
|
| `seo_audit_directory` | Audit built HTML files in a directory. Options: `dir`, `baseUrl`, `maxPages`, `pageDetail`. |
|
|
94
151
|
| `seo_lighthouse` | Lighthouse performance + SEO + agentic-browsing for one URL. |
|
|
152
|
+
| `seo_aeo_audit` | AEO audit of one of `url` (with `crawl` for a site), `html` or `dir`. Options: `aiBots`, `baseUrl`, `maxPages`, `maxDepth`, `concurrency`, `respectRobots`, `includeSubdomains`, `pageDetail`, `includeInfo`, `disableRules`, `headers`, `format`, `detail`. |
|
|
95
153
|
| `seo_list_rules` | Rule reference, filter by `category` or `rule`. |
|
|
96
154
|
|
|
97
155
|
All tools are read-only. Results are returned both as text (JSON or Markdown) and as `structuredContent`.
|
|
@@ -130,32 +188,80 @@ All tools are read-only. Results are returned both as text (JSON or Markdown) an
|
|
|
130
188
|
|
|
131
189
|
Site audits add `crawl` stats, `robotsTxt`, `sitemaps`, `siteFindings`, `issues` (page findings rolled up per rule with example URLs) and `pages`.
|
|
132
190
|
|
|
191
|
+
An AEO audit has `kind: "aeo"` and its own score:
|
|
192
|
+
|
|
193
|
+
```jsonc
|
|
194
|
+
{
|
|
195
|
+
"schemaVersion": 1,
|
|
196
|
+
"kind": "aeo",
|
|
197
|
+
"target": "https://www.example.com/guide",
|
|
198
|
+
"url": "https://www.example.com/guide",
|
|
199
|
+
"source": "url", // or "crawl", "directory", "file", "html"
|
|
200
|
+
"score": 89, // average page score minus weighted site-wide findings
|
|
201
|
+
"counts": { "error": 0, "warning": 2, "info": 1 },
|
|
202
|
+
"summary": {
|
|
203
|
+
"pages": 1, "robotsTxt": "found", "llmsTxt": "missing",
|
|
204
|
+
"searchCrawlersBlocked": [], "answerBotsBlocked": ["Google-Extended"], "trainingBotsBlocked": ["GPTBot"],
|
|
205
|
+
"trainingPreference": "none", // the __usage_ai-training group: "allowed", "disallowed" or "none"
|
|
206
|
+
"blockedByServer": null, // with --ai-bots: the bots the server refused
|
|
207
|
+
"page": { "indexable": true, "googleSnippets": "allowed", "copilotAnswers": "allowed", "questions": 4, "...": "…" }
|
|
208
|
+
},
|
|
209
|
+
"findings": [ { "rule": "aeo.robots-blocks-answer-bot", "severity": "warning", "...": "…" } ],
|
|
210
|
+
"crawlers": { "robotsTxt": "https://www.example.com/robots.txt", "status": "found", "contentSignals": [], "bots": [
|
|
211
|
+
{ "name": "OAI-SearchBot", "operator": "OpenAI", "purpose": "answer", "usedFor": "ChatGPT search results", "robotsGroup": "wildcard", "allowed": true, "blockedPages": 0 }
|
|
212
|
+
] },
|
|
213
|
+
"llmsTxt": { "url": "https://www.example.com/llms.txt", "status": "missing", "...": "…" }
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A crawl or directory adds `pages` (each page's score and summary) and `issues`. `findings` holds the site-wide findings plus every page finding, except for a site audited with `pageDetail: "summary"` (the MCP default), where page findings are only rolled up in `issues`.
|
|
218
|
+
|
|
133
219
|
## Library
|
|
134
220
|
|
|
135
221
|
```ts
|
|
136
|
-
import { auditUrl, auditSite,
|
|
222
|
+
import { auditUrl, auditSite, auditAeoUrl, renderMarkdown, renderAeoMarkdown } from '@rivi-software/agentic-seo-check';
|
|
137
223
|
|
|
138
224
|
const page = await auditUrl('https://www.example.com/', { keyword: 'example', render: true });
|
|
139
225
|
const site = await auditSite('https://www.example.com/', { maxPages: 100 });
|
|
140
226
|
console.log(renderMarkdown(site));
|
|
141
|
-
```
|
|
142
227
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
pnpm test # vitest: extractor, rules, crawler against a local fixture server, directory scan, MCP client
|
|
147
|
-
pnpm typecheck
|
|
148
|
-
pnpm dev audit https://example.com # run from source with tsx
|
|
228
|
+
const aeo = await auditAeoUrl('https://www.example.com/guide', { aiBots: true });
|
|
229
|
+
console.log(renderAeoMarkdown(aeo));
|
|
149
230
|
```
|
|
150
231
|
|
|
151
232
|
## Scope of this first version
|
|
152
233
|
|
|
153
234
|
- Crawls are breadth-first, same-site, HTML only, and obey robots.txt by default. `--render`/`--lighthouse` apply to the start page only during a crawl.
|
|
154
235
|
- Scores are indicative. Thresholds (title 30-60 chars, description 70-160, thin content 300 words, slow response 2 s, deep page depth 3) are configurable through the library `thresholds` option.
|
|
155
|
-
-
|
|
236
|
+
- The AEO audit reads what answer engines document: robots.txt tokens, Content-Signal, snippet controls, structured data. Whether an engine cites a page also depends on things no audit can see from outside (its index, its ranking, the question asked), so the AEO score says how ready a site is, not how often it is cited. llms.txt is reported at info level: no major answer engine has said it reads one.
|
|
237
|
+
- Not included yet: gzip sitemaps, rendering every page in a crawl, field (CrUX) data, rank tracking, backlink data, AI-generated copy suggestions, tracking of citations in AI answers.
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Bug reports, wrong findings, new rules and fixes are welcome from anyone. With Node 24 and pnpm 12:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pnpm install
|
|
245
|
+
pnpm run verify
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`verify` runs what CI runs: format, lint, typecheck, tests, build and a check of the packed package. `pnpm run dev audit https://example.com` runs the CLI from source. [CONTRIBUTING.md](https://github.com/rivi-software/agentic-seo-check/blob/main/CONTRIBUTING.md) covers proposing a change, what a pull request needs and how releases reach npm, and [AGENTS.md](https://github.com/rivi-software/agentic-seo-check/blob/main/AGENTS.md) is the brief for coding agents working on the tool itself. Everyone who takes part follows the [code of conduct](https://github.com/rivi-software/agentic-seo-check/blob/main/CODE_OF_CONDUCT.md). [SECURITY.md](https://github.com/rivi-software/agentic-seo-check/blob/main/SECURITY.md) says how to report a vulnerability privately.
|
|
156
249
|
|
|
157
250
|
## License
|
|
158
251
|
|
|
159
|
-
|
|
252
|
+
Licensed under the [MIT License](https://github.com/rivi-software/agentic-seo-check/blob/main/LICENSE). Copyright © 2026 RIVI Software.
|
|
253
|
+
|
|
254
|
+
<br>
|
|
255
|
+
|
|
256
|
+
<p align="center">
|
|
257
|
+
<a href="https://rivi.software">
|
|
258
|
+
<picture>
|
|
259
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rivi-software/agentic-seo-check/main/assets/rivi-mark-on-dark.svg">
|
|
260
|
+
<img src="https://raw.githubusercontent.com/rivi-software/agentic-seo-check/main/assets/rivi-mark.svg" alt="RIVI Software" width="36" height="36">
|
|
261
|
+
</picture>
|
|
262
|
+
</a>
|
|
263
|
+
</p>
|
|
160
264
|
|
|
161
|
-
|
|
265
|
+
<p align="center">
|
|
266
|
+
<sub>RIVI agentic-seo-check is made by <a href="https://rivi.software">RIVI Software</a>, which builds software for radio stations. Created in <a href="https://t3.codes">T3 Code</a>.</sub>
|
|
267
|
+
</p>
|
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RIVI Software's name, site and mark as the CLI, the MCP server and the Markdown report show
|
|
3
|
+
* them. The marks are copies of assets/rivi-mark.svg and assets/rivi-mark-on-dark.svg, inlined
|
|
4
|
+
* because npm ships dist/ without assets/.
|
|
5
|
+
*/
|
|
6
|
+
import type { Icon } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
export declare const MAKER = "RIVI Software";
|
|
8
|
+
export declare const MAKER_URL = "https://rivi.software";
|
|
9
|
+
export declare const REPOSITORY_URL = "https://github.com/rivi-software/agentic-seo-check";
|
|
10
|
+
/** The mark as MCP icons: the ink tile for light themes, the light tile for dark ones. */
|
|
11
|
+
export declare const MCP_ICONS: Icon[];
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const MAKER = "RIVI Software";
|
|
2
|
+
export const MAKER_URL = "https://rivi.software";
|
|
3
|
+
export const REPOSITORY_URL = "https://github.com/rivi-software/agentic-seo-check";
|
|
4
|
+
const MARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="RIVI Software"><rect width="40" height="40" rx="9" fill="#15171a"/><g fill="#f5f6f7"><rect x="8" y="16" width="3" height="8" rx="1.5"/><rect x="13.25" y="11" width="3" height="18" rx="1.5"/><rect x="18.5" y="7" width="3" height="26" rx="1.5" fill="#3be08a"/><rect x="23.75" y="12" width="3" height="16" rx="1.5"/><rect x="29" y="16" width="3" height="8" rx="1.5"/></g></svg>';
|
|
5
|
+
const MARK_ON_DARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="RIVI Software"><rect width="40" height="40" rx="9" fill="#f5f6f7"/><g fill="#15171a"><rect x="8" y="16" width="3" height="8" rx="1.5"/><rect x="13.25" y="11" width="3" height="18" rx="1.5"/><rect x="18.5" y="7" width="3" height="26" rx="1.5" fill="#0e9f55"/><rect x="23.75" y="12" width="3" height="16" rx="1.5"/><rect x="29" y="16" width="3" height="8" rx="1.5"/></g></svg>';
|
|
6
|
+
const dataUri = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, "utf-8").toString("base64")}`;
|
|
7
|
+
/** The mark as MCP icons: the ink tile for light themes, the light tile for dark ones. */
|
|
8
|
+
export const MCP_ICONS = [
|
|
9
|
+
{
|
|
10
|
+
mimeType: "image/svg+xml",
|
|
11
|
+
sizes: ["any"],
|
|
12
|
+
src: dataUri(MARK_SVG),
|
|
13
|
+
theme: "light",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
mimeType: "image/svg+xml",
|
|
17
|
+
sizes: ["any"],
|
|
18
|
+
src: dataUri(MARK_ON_DARK_SVG),
|
|
19
|
+
theme: "dark",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../src/brand.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,KAAK,GAAG,eAAe,CAAC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GACzB,oDAAoD,CAAC;AAEvD,MAAM,QAAQ,GACZ,2cAA2c,CAAC;AAE9c,MAAM,gBAAgB,GACpB,2cAA2c,CAAC;AAE9c,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,EAAE,CAC9B,6BAA6B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AAE9E,0FAA0F;AAC1F,MAAM,CAAC,MAAM,SAAS,GAAW;IAC/B;QACE,QAAQ,EAAE,eAAe;QACzB,KAAK,EAAE,CAAC,KAAK,CAAC;QACd,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC;QACtB,KAAK,EAAE,OAAO;KACf;IACD;QACE,QAAQ,EAAE,eAAe;QACzB,KAAK,EAAE,CAAC,KAAK,CAAC;QACd,GAAG,EAAE,OAAO,CAAC,gBAAgB,CAAC;QAC9B,KAAK,EAAE,MAAM;KACd;CACF,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -1,71 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { promises as fs } from
|
|
3
|
-
import { Command, InvalidArgumentError, Option } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
4
|
+
import { MAKER, MAKER_URL, REPOSITORY_URL } from "./brand.js";
|
|
5
|
+
import { auditAeoTarget, compactAeoAudit } from "./core/aeo/audit.js";
|
|
6
|
+
import { auditTarget, compactAudit } from "./core/audit.js";
|
|
7
|
+
import { renderAeoMarkdown, renderMarkdown } from "./core/report/markdown.js";
|
|
8
|
+
import { ALL_RULES } from "./core/rules/index.js";
|
|
9
|
+
import { SEVERITY_RANK } from "./core/types.js";
|
|
10
|
+
import { startMcpServer } from "./mcp.js";
|
|
11
|
+
import { VERSION } from "./version.js";
|
|
10
12
|
const program = new Command();
|
|
11
13
|
program
|
|
12
|
-
.name(
|
|
13
|
-
.description(
|
|
14
|
+
.name("agentic-seo-check")
|
|
15
|
+
.description("SEO and AEO analysis for coding agents. Audits URLs, static build output or raw HTML for search engines (audit, crawl) and AI answer engines (aeo), and prints machine-readable findings.")
|
|
14
16
|
.version(VERSION)
|
|
17
|
+
.addHelpText("afterAll", `\nMade by ${MAKER} (${MAKER_URL}). Docs and issues: ${REPOSITORY_URL}`)
|
|
15
18
|
.showHelpAfterError();
|
|
16
19
|
const intArg = (value) => {
|
|
20
|
+
// oxlint-disable-next-line unicorn/prefer-number-coercion -- parseInt reads the leading digits ("10.5" is 10), where Number would refuse what users have always passed
|
|
17
21
|
const n = Number.parseInt(value, 10);
|
|
18
|
-
if (!Number.isFinite(n) || n < 0)
|
|
19
|
-
throw new InvalidArgumentError(
|
|
22
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
23
|
+
throw new InvalidArgumentError("Expected a non-negative integer.");
|
|
24
|
+
}
|
|
20
25
|
return n;
|
|
21
26
|
};
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
cmd.option(
|
|
27
|
+
const addAuditOptions = (cmd, mode) => {
|
|
28
|
+
if (mode !== "crawl") {
|
|
29
|
+
cmd.option("--crawl", "Crawl the whole site starting at the URL instead of auditing one page");
|
|
30
|
+
}
|
|
31
|
+
cmd
|
|
32
|
+
.option("--max-pages <n>", "Maximum pages to crawl", intArg, 50)
|
|
33
|
+
.option("--max-depth <n>", "Maximum link depth from the start URL", intArg, 3)
|
|
34
|
+
.option("--concurrency <n>", "Parallel requests while crawling", intArg, 5)
|
|
35
|
+
.option("--delay <ms>", "Delay between requests per worker", intArg, 0);
|
|
36
|
+
if (mode === "aeo") {
|
|
37
|
+
cmd.option("--ai-bots", "Also request the page with each AI bot's User-Agent to catch CDN or firewall blocks");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
cmd
|
|
41
|
+
.option("-k, --keyword <keyword>", "Target keyword for keyword.* checks")
|
|
42
|
+
.option("--render", "Render with headless Chromium (Playwright) and compare with the server HTML")
|
|
43
|
+
.option("--lighthouse", "Run Lighthouse for Core Web Vitals and Lighthouse SEO audits (slow)")
|
|
44
|
+
.addOption(new Option("--form-factor <type>", "Lighthouse device emulation")
|
|
45
|
+
.choices(["mobile", "desktop"])
|
|
46
|
+
.default("mobile"));
|
|
47
|
+
}
|
|
48
|
+
cmd
|
|
49
|
+
.option("--base-url <url>", "Origin used for directories, files and stdin HTML (default http://localhost/)")
|
|
50
|
+
.option("--ignore-robots", "Do not obey robots.txt while crawling")
|
|
51
|
+
.option("--include-subdomains", "Treat subdomains as internal when crawling");
|
|
52
|
+
if (mode !== "aeo") {
|
|
53
|
+
cmd.option("--check-external", "Probe external links for 4xx/5xx while crawling");
|
|
54
|
+
}
|
|
25
55
|
return cmd
|
|
26
|
-
.option(
|
|
27
|
-
.option(
|
|
28
|
-
.
|
|
29
|
-
.option(
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.option(
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.option(
|
|
39
|
-
.option(
|
|
40
|
-
.
|
|
41
|
-
.option(
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
51
|
-
}
|
|
52
|
-
|
|
56
|
+
.option("--include <regex...>", "Only crawl URLs matching these patterns")
|
|
57
|
+
.option("--exclude <regex...>", "Skip URLs matching these patterns")
|
|
58
|
+
.addOption(new Option("-f, --format <format>", "Output format (default: json when piped, md on a terminal)").choices(["json", "md"]))
|
|
59
|
+
.option("-o, --out <file>", "Write the report to a file instead of stdout")
|
|
60
|
+
.addOption(new Option("--fail-on <severity>", "Exit with code 1 when findings at or above this severity exist")
|
|
61
|
+
.choices(["error", "warning", "info", "none"])
|
|
62
|
+
.default("none"))
|
|
63
|
+
.option("--no-info", "Omit info-level findings")
|
|
64
|
+
.option("--compact", "Trim evidence lists and bulky fields (smaller output for LLM consumption)")
|
|
65
|
+
.addOption(new Option("--page-detail <level>", "Per-page detail in site reports")
|
|
66
|
+
.choices(["full", "summary"])
|
|
67
|
+
.default("full"))
|
|
68
|
+
.option("--disable <rule...>", "Rule ids to skip")
|
|
69
|
+
.option("--timeout <ms>", "Per-request timeout", intArg, 15_000)
|
|
70
|
+
.option("--user-agent <ua>", "Custom User-Agent header")
|
|
71
|
+
.option("-H, --header <header...>", 'Extra request header "Name: value" (repeatable)')
|
|
72
|
+
.option("-q, --quiet", "Suppress progress output on stderr");
|
|
73
|
+
};
|
|
74
|
+
/** The report format: as asked, else by the output file's extension, else Markdown on a terminal. */
|
|
75
|
+
const formatOf = (opts) => {
|
|
76
|
+
if (opts.format !== undefined) {
|
|
77
|
+
return opts.format;
|
|
78
|
+
}
|
|
79
|
+
if (opts.out) {
|
|
80
|
+
return opts.out.endsWith(".md") ? "md" : "json";
|
|
81
|
+
}
|
|
82
|
+
return process.stdout.isTTY ? "md" : "json";
|
|
83
|
+
};
|
|
84
|
+
/** `-H "Name: value"` flags as request headers. */
|
|
85
|
+
const parseHeaders = (values = []) => {
|
|
53
86
|
const headers = {};
|
|
54
|
-
for (const h of
|
|
55
|
-
const idx = h.indexOf(
|
|
56
|
-
if (idx === -1)
|
|
87
|
+
for (const h of values) {
|
|
88
|
+
const idx = h.indexOf(":");
|
|
89
|
+
if (idx === -1) {
|
|
57
90
|
throw new InvalidArgumentError(`Header "${h}" must be "Name: value".`);
|
|
91
|
+
}
|
|
58
92
|
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
59
93
|
}
|
|
94
|
+
return headers;
|
|
95
|
+
};
|
|
96
|
+
const progressPrinter = (opts) => {
|
|
60
97
|
const showProgress = !opts.quiet && process.stderr.isTTY;
|
|
61
|
-
|
|
62
|
-
if (!showProgress)
|
|
98
|
+
return (e) => {
|
|
99
|
+
if (!showProgress) {
|
|
63
100
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
}
|
|
102
|
+
const counter = e.crawled === undefined ? "" : ` [${e.crawled}/${e.discovered}]`;
|
|
103
|
+
process.stderr.write(`\u001B[2m${e.phase}${counter}: ${e.message}\u001B[0m\n`);
|
|
66
104
|
};
|
|
105
|
+
};
|
|
106
|
+
/** Runs the audit the command asks for, and renders it in the format asked for. */
|
|
107
|
+
const auditFor = async (target, opts, mode) => {
|
|
108
|
+
const headers = parseHeaders(opts.header);
|
|
67
109
|
const options = {
|
|
68
|
-
crawl:
|
|
110
|
+
crawl: mode === "crawl" || Boolean(opts.crawl),
|
|
69
111
|
maxPages: opts.maxPages,
|
|
70
112
|
maxDepth: opts.maxDepth,
|
|
71
113
|
concurrency: opts.concurrency,
|
|
@@ -83,67 +125,113 @@ async function runAudit(target, opts, forceCrawl) {
|
|
|
83
125
|
includeInfo: opts.info,
|
|
84
126
|
disableRules: opts.disable,
|
|
85
127
|
pageDetail: opts.pageDetail,
|
|
86
|
-
fetch: {
|
|
87
|
-
|
|
128
|
+
fetch: {
|
|
129
|
+
timeoutMs: opts.timeout,
|
|
130
|
+
userAgent: opts.userAgent,
|
|
131
|
+
headers: Object.keys(headers).length ? headers : undefined,
|
|
132
|
+
},
|
|
133
|
+
onProgress: progressPrinter(opts),
|
|
88
134
|
};
|
|
135
|
+
const format = formatOf(opts);
|
|
136
|
+
if (mode === "aeo") {
|
|
137
|
+
let aeo = await auditAeoTarget(target, {
|
|
138
|
+
...options,
|
|
139
|
+
aiBots: opts.aiBots,
|
|
140
|
+
});
|
|
141
|
+
if (opts.compact) {
|
|
142
|
+
aeo = compactAeoAudit(aeo);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
counts: aeo.counts,
|
|
146
|
+
output: format === "md" ? renderAeoMarkdown(aeo) : JSON.stringify(aeo, null, 2),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
89
149
|
let audit = await auditTarget(target, options);
|
|
90
|
-
if (opts.compact)
|
|
150
|
+
if (opts.compact) {
|
|
91
151
|
audit = compactAudit(audit);
|
|
92
|
-
|
|
93
|
-
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
counts: audit.counts,
|
|
155
|
+
output: format === "md" ? renderMarkdown(audit) : JSON.stringify(audit, null, 2),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const runAudit = async (target, opts, mode) => {
|
|
159
|
+
const { counts, output } = await auditFor(target, opts, mode);
|
|
94
160
|
if (opts.out) {
|
|
95
|
-
await fs.writeFile(opts.out, output
|
|
96
|
-
if (!opts.quiet)
|
|
161
|
+
await fs.writeFile(opts.out, `${output}\n`, "utf-8");
|
|
162
|
+
if (!opts.quiet) {
|
|
97
163
|
process.stderr.write(`Report written to ${opts.out}\n`);
|
|
164
|
+
}
|
|
98
165
|
}
|
|
99
166
|
else {
|
|
100
|
-
process.stdout.write(output
|
|
167
|
+
process.stdout.write(`${output}\n`);
|
|
101
168
|
}
|
|
102
|
-
if (opts.failOn !==
|
|
169
|
+
if (opts.failOn !== "none") {
|
|
103
170
|
const threshold = SEVERITY_RANK[opts.failOn];
|
|
104
|
-
const
|
|
105
|
-
|
|
171
|
+
const hits = ["error", "warning", "info"]
|
|
172
|
+
.filter((s) => SEVERITY_RANK[s] >= threshold)
|
|
173
|
+
.reduce((n, s) => n + counts[s], 0);
|
|
106
174
|
if (hits > 0) {
|
|
107
|
-
if (!opts.quiet)
|
|
175
|
+
if (!opts.quiet) {
|
|
108
176
|
process.stderr.write(`${hits} finding(s) at or above "${opts.failOn}" severity.\n`);
|
|
177
|
+
}
|
|
109
178
|
process.exitCode = 1;
|
|
110
179
|
}
|
|
111
180
|
}
|
|
112
|
-
}
|
|
181
|
+
};
|
|
113
182
|
addAuditOptions(program
|
|
114
|
-
.command(
|
|
115
|
-
.argument(
|
|
116
|
-
.description(
|
|
117
|
-
await runAudit(target, opts,
|
|
183
|
+
.command("audit")
|
|
184
|
+
.argument("<target>", 'URL, directory of HTML files, .html file, or "-" for HTML on stdin')
|
|
185
|
+
.description("Audit a page, a site (--crawl), a build directory or raw HTML"), "audit").action(async (target, opts) => {
|
|
186
|
+
await runAudit(target, opts, "audit");
|
|
118
187
|
});
|
|
119
|
-
addAuditOptions(program
|
|
120
|
-
|
|
188
|
+
addAuditOptions(program
|
|
189
|
+
.command("crawl")
|
|
190
|
+
.argument("<url>", "Start URL")
|
|
191
|
+
.description('Crawl a site and audit every page (same as "audit --crawl")'), "crawl").action(async (url, opts) => {
|
|
192
|
+
await runAudit(url, opts, "crawl");
|
|
193
|
+
});
|
|
194
|
+
addAuditOptions(program
|
|
195
|
+
.command("aeo")
|
|
196
|
+
.argument("<target>", 'URL, directory of HTML files, .html file, or "-" for HTML on stdin')
|
|
197
|
+
.description("Answer engine optimization audit: AI crawler access (robots.txt, llms.txt), snippet and AI opt-outs, answer-ready content, entities and freshness, with its own score"), "aeo").action(async (target, opts) => {
|
|
198
|
+
await runAudit(target, opts, "aeo");
|
|
121
199
|
});
|
|
122
200
|
program
|
|
123
|
-
.command(
|
|
124
|
-
.description(
|
|
125
|
-
.option(
|
|
126
|
-
.option(
|
|
127
|
-
.addOption(new Option(
|
|
201
|
+
.command("rules")
|
|
202
|
+
.description("List the rules this tool can report")
|
|
203
|
+
.option("--category <category>", "Filter by category")
|
|
204
|
+
.option("--rule <id>", "Show one rule")
|
|
205
|
+
.addOption(new Option("-f, --format <format>", "Output format")
|
|
206
|
+
.choices(["json", "md"])
|
|
207
|
+
.default(process.stdout.isTTY ? "md" : "json"))
|
|
128
208
|
.action((opts) => {
|
|
129
|
-
const rules = ALL_RULES.filter((r) => (!opts.category || r.category === opts.category) &&
|
|
130
|
-
|
|
131
|
-
|
|
209
|
+
const rules = ALL_RULES.filter((r) => (!opts.category || r.category === opts.category) &&
|
|
210
|
+
(!opts.rule || r.id === opts.rule));
|
|
211
|
+
if (opts.format === "json") {
|
|
212
|
+
process.stdout.write(`${JSON.stringify({ count: rules.length, rules }, null, 2)}\n`);
|
|
132
213
|
return;
|
|
133
214
|
}
|
|
134
|
-
const lines = [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
215
|
+
const lines = [
|
|
216
|
+
"| Rule | Severity | Category | Checks |",
|
|
217
|
+
"| --- | --- | --- | --- |",
|
|
218
|
+
];
|
|
219
|
+
for (const r of rules) {
|
|
220
|
+
lines.push(`| \`${r.id}\` | ${r.severity} | ${r.category} | ${r.description.replaceAll("|", "\\|")}${r.requires ? ` _(requires ${r.requires})_` : ""} |`);
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
138
223
|
});
|
|
139
224
|
program
|
|
140
|
-
.command(
|
|
141
|
-
.description(
|
|
225
|
+
.command("mcp")
|
|
226
|
+
.description("Start the MCP server on stdio (for Claude Code, Cursor, Codex, etc.)")
|
|
142
227
|
.action(async () => {
|
|
143
228
|
await startMcpServer();
|
|
144
229
|
});
|
|
145
|
-
|
|
146
|
-
|
|
230
|
+
try {
|
|
231
|
+
await program.parseAsync(process.argv);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
process.stderr.write(`agentic-seo-check: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
147
235
|
process.exit(2);
|
|
148
|
-
}
|
|
236
|
+
}
|
|
149
237
|
//# sourceMappingURL=cli.js.map
|