@houseofmvps/claude-rank 1.7.0 → 1.7.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -9
- package/bin/claude-rank.mjs +4 -1
- package/package.json +1 -1
- package/skills/rank-audit/SKILL.md +1 -1
- package/tools/lib/formatter.mjs +339 -123
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<img src="assets/hero-banner.png" alt="claude-rank — SEO/GEO/AEO Plugin for Claude Code" width="100%"/>
|
|
4
4
|
|
|
5
|
-
### The most comprehensive SEO/GEO/AEO plugin for Claude Code.
|
|
5
|
+
### The most comprehensive SEO/GEO/AEO plugin for Claude Code. 85+ rules. Competitive X-Ray. Auto-fix everything. Dominate search — traditional and AI.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@houseofmvps/claude-rank)
|
|
8
8
|
[](https://www.npmjs.com/package/@houseofmvps/claude-rank)
|
|
@@ -188,11 +188,11 @@ That's not an SEO problem. That's a visibility problem across every search surfa
|
|
|
188
188
|
/claude-rank:rank-audit
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
One command. Three scanners run in parallel — SEO, GEO, and AEO.
|
|
191
|
+
One command. Three scanners run in parallel — SEO, GEO, and AEO. 85+ rules checked. Every finding gets an automated fix. Score tracked over time. **Then it tells you exactly what to do in Google Search Console and Bing Webmaster Tools.**
|
|
192
192
|
|
|
193
193
|
```
|
|
194
194
|
SEO Score: 87/100 ████████████░░ (39 rules)
|
|
195
|
-
GEO Score: 92/100 █████████████░ (
|
|
195
|
+
GEO Score: 92/100 █████████████░ (34 rules)
|
|
196
196
|
AEO Score: 78/100 ██████████░░░░ (12 rules)
|
|
197
197
|
Overall: 86/100 READY TO RANK
|
|
198
198
|
```
|
|
@@ -215,7 +215,7 @@ Traditional search optimization. The foundation.
|
|
|
215
215
|
| **Structured Data** | JSON-LD presence, schema validation against Google's required fields (14 schema types) |
|
|
216
216
|
| **Cross-Page** | Duplicate titles across pages, duplicate descriptions, canonical conflicts, orphan pages |
|
|
217
217
|
|
|
218
|
-
### GEO Scanner —
|
|
218
|
+
### GEO Scanner — 34 Rules
|
|
219
219
|
|
|
220
220
|
Generative Engine Optimization. For AI search engines: ChatGPT, Perplexity, Gemini, Google AI Overviews.
|
|
221
221
|
|
|
@@ -382,7 +382,7 @@ Each audit produces separate SEO, GEO, and AEO scores plus a composite. Same rul
|
|
|
382
382
|
|---------|-------------|
|
|
383
383
|
| `claude-rank scan ./project` | Full SEO scan (39 rules) |
|
|
384
384
|
| `claude-rank scan https://example.com` | Crawl and scan a live site (up to 50 pages) |
|
|
385
|
-
| `claude-rank geo ./project` | GEO scan — AI search optimization (
|
|
385
|
+
| `claude-rank geo ./project` | GEO scan — AI search optimization (34 rules) |
|
|
386
386
|
| `claude-rank aeo ./project` | AEO scan — answer engine optimization (12 rules) |
|
|
387
387
|
| `claude-rank compete https://competitor.com .` | Competitive X-Ray — side-by-side comparison |
|
|
388
388
|
| `claude-rank cwv https://example.com` | Core Web Vitals via Lighthouse (optional) |
|
|
@@ -411,7 +411,7 @@ Each audit produces separate SEO, GEO, and AEO scores plus a composite. Same rul
|
|
|
411
411
|
| Feature | claude-rank | claude-seo |
|
|
412
412
|
|---------|:-----------:|:----------:|
|
|
413
413
|
| SEO rules | 39 | ~20 |
|
|
414
|
-
| GEO — AI search (Perplexity, ChatGPT, Gemini) |
|
|
414
|
+
| GEO — AI search (Perplexity, ChatGPT, Gemini) | 34 rules | Basic |
|
|
415
415
|
| AEO — featured snippets, voice search | 12 rules | None |
|
|
416
416
|
| Core Web Vitals / Lighthouse | Yes (optional) | No |
|
|
417
417
|
| Redirect chain detection | Yes | No |
|
|
@@ -452,7 +452,7 @@ Two terms that matter and are often confused:
|
|
|
452
452
|
| **SSRF protection** | All HTTP tools block private IPs, cloud metadata, non-HTTP schemes |
|
|
453
453
|
| **No telemetry** | Zero data collection. No phone-home. Ever. |
|
|
454
454
|
| **1 dependency** | `htmlparser2` only (30KB). No native bindings. No `node-gyp`. |
|
|
455
|
-
| **
|
|
455
|
+
| **265 tests** | Security module, all scanners, competitive X-Ray, CLI, integration tests |
|
|
456
456
|
| **File safety** | 10MB read cap. 5MB response cap. Restrictive write permissions. |
|
|
457
457
|
|
|
458
458
|
See [SECURITY.md](SECURITY.md) for the full vulnerability disclosure policy.
|
|
@@ -463,7 +463,7 @@ See [SECURITY.md](SECURITY.md) for the full vulnerability disclosure policy.
|
|
|
463
463
|
|
|
464
464
|
| Category | Count | Highlights |
|
|
465
465
|
|---|---|---|
|
|
466
|
-
| **Tools** | 10 | SEO scanner (39 rules), GEO scanner (
|
|
466
|
+
| **Tools** | 10 | SEO scanner (39 rules), GEO scanner (34 rules), AEO scanner (12 rules), Competitive X-Ray (50+ tech patterns), Lighthouse/CWV scanner, schema engine, robots analyzer, sitemap analyzer, llms.txt generator, audit history |
|
|
467
467
|
| **Skills** | 7 | /claude-rank:rank, /claude-rank:rank-audit, /claude-rank:rank-geo, /claude-rank:rank-aeo, /claude-rank:rank-fix, /claude-rank:rank-schema, /claude-rank:rank-compete |
|
|
468
468
|
| **Agents** | 4 | SEO auditor (project-type-aware), GEO auditor (AI readiness levels), AEO auditor (snippet opportunities), Schema auditor (Google validation) |
|
|
469
469
|
| **Commands** | 7 | All slash commands above |
|
|
@@ -509,7 +509,7 @@ Found a bug? Want a new scanner rule? [Open an issue](https://github.com/Houseof
|
|
|
509
509
|
git clone https://github.com/Houseofmvps/claude-rank.git
|
|
510
510
|
cd claude-rank
|
|
511
511
|
npm install
|
|
512
|
-
npm test #
|
|
512
|
+
npm test # 265 tests, node:test
|
|
513
513
|
node tools/<tool>.mjs # No build step
|
|
514
514
|
```
|
|
515
515
|
|
package/bin/claude-rank.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Standalone CLI: npx claude-rank <command> <directory>
|
|
3
|
-
// Commands: scan, geo, aeo,
|
|
3
|
+
// Commands: scan, geo, aeo, compete, cwv, schema
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const jsonFlag = args.includes('--json');
|
|
@@ -97,6 +97,9 @@ if (command === 'compete') {
|
|
|
97
97
|
const localDir = positional[2] || '.';
|
|
98
98
|
const { resolve: resolvePath } = await import('path');
|
|
99
99
|
|
|
100
|
+
// Clear argv before importing so compete-scanner's inline CLI guard doesn't fire
|
|
101
|
+
process.argv = process.argv.slice(0, 2);
|
|
102
|
+
|
|
100
103
|
const { compete } = await import(new URL('../tools/compete-scanner.mjs', import.meta.url));
|
|
101
104
|
const { formatCompeteReport } = await import(new URL('../tools/lib/formatter.mjs', import.meta.url));
|
|
102
105
|
|
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@ Advise on content optimizations the scanner cannot automate:
|
|
|
71
71
|
|
|
72
72
|
## Phase 7: Backlink Strategy
|
|
73
73
|
|
|
74
|
-
Guide link building: create link-worthy assets, guest posting, broken link building,
|
|
74
|
+
Guide link building: create link-worthy assets, guest posting, broken link building, digital PR, expert roundups.
|
|
75
75
|
|
|
76
76
|
## Phase 8: Search Console Action Plan
|
|
77
77
|
|
package/tools/lib/formatter.mjs
CHANGED
|
@@ -1,46 +1,180 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* formatter.mjs —
|
|
2
|
+
* formatter.mjs — Professional terminal output for claude-rank CLI reports.
|
|
3
3
|
* No external dependencies — uses raw ANSI escape codes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// ANSI helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
6
10
|
const c = {
|
|
7
11
|
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
8
|
-
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
9
12
|
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
13
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
14
|
+
blue: s => `\x1b[34m${s}\x1b[0m`,
|
|
15
|
+
magenta: s => `\x1b[35m${s}\x1b[0m`,
|
|
10
16
|
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
17
|
+
white: s => `\x1b[37m${s}\x1b[0m`,
|
|
11
18
|
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
12
19
|
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
20
|
+
underline: s => `\x1b[4m${s}\x1b[0m`,
|
|
21
|
+
bgRed: s => `\x1b[41m\x1b[37m${s}\x1b[0m`,
|
|
22
|
+
bgYellow: s => `\x1b[43m\x1b[30m${s}\x1b[0m`,
|
|
23
|
+
bgGreen: s => `\x1b[42m\x1b[30m${s}\x1b[0m`,
|
|
24
|
+
bgBlue: s => `\x1b[44m\x1b[37m${s}\x1b[0m`,
|
|
25
|
+
bgCyan: s => `\x1b[46m\x1b[30m${s}\x1b[0m`,
|
|
13
26
|
};
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
/** Strip ANSI codes for accurate length measurement */
|
|
29
|
+
function stripAnsi(str) {
|
|
30
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Pad string to exact visual width (ANSI-aware) */
|
|
34
|
+
function pad(str, len) {
|
|
35
|
+
const visible = stripAnsi(str).length;
|
|
36
|
+
return str + ' '.repeat(Math.max(0, len - visible));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Score display
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function gradeFor(score) {
|
|
45
|
+
if (score >= 90) return { letter: 'A', color: c.bgGreen };
|
|
46
|
+
if (score >= 80) return { letter: 'B', color: c.bgGreen };
|
|
47
|
+
if (score >= 70) return { letter: 'C', color: c.bgYellow };
|
|
48
|
+
if (score >= 60) return { letter: 'D', color: c.bgYellow };
|
|
49
|
+
return { letter: 'F', color: c.bgRed };
|
|
50
|
+
}
|
|
16
51
|
|
|
17
52
|
function scoreLabel(score) {
|
|
18
|
-
if (score >= 90) return c.green('
|
|
19
|
-
if (score >= 80) return c.green('
|
|
20
|
-
if (score >=
|
|
21
|
-
return c.
|
|
53
|
+
if (score >= 90) return c.green('Excellent');
|
|
54
|
+
if (score >= 80) return c.green('Good');
|
|
55
|
+
if (score >= 70) return c.yellow('Needs Work');
|
|
56
|
+
if (score >= 60) return c.yellow('Below Average');
|
|
57
|
+
return c.red('Poor');
|
|
22
58
|
}
|
|
23
59
|
|
|
24
60
|
function scoreBar(score) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
61
|
+
const width = 20;
|
|
62
|
+
const filled = Math.round((score / 100) * width);
|
|
63
|
+
const empty = width - filled;
|
|
64
|
+
const barChar = '\u2501'; // heavy horizontal line
|
|
65
|
+
const emptyChar = '\u2500'; // light horizontal line
|
|
66
|
+
|
|
67
|
+
let bar;
|
|
68
|
+
if (score >= 80) {
|
|
69
|
+
bar = c.green(barChar.repeat(filled)) + c.dim(emptyChar.repeat(empty));
|
|
70
|
+
} else if (score >= 60) {
|
|
71
|
+
bar = c.yellow(barChar.repeat(filled)) + c.dim(emptyChar.repeat(empty));
|
|
72
|
+
} else {
|
|
73
|
+
bar = c.red(barChar.repeat(filled)) + c.dim(emptyChar.repeat(empty));
|
|
74
|
+
}
|
|
75
|
+
return bar;
|
|
28
76
|
}
|
|
29
77
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Severity helpers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
83
|
+
|
|
84
|
+
function severityBadge(severity) {
|
|
85
|
+
switch (severity) {
|
|
86
|
+
case 'critical': return c.bgRed(' CRITICAL ');
|
|
87
|
+
case 'high': return c.bgRed(' HIGH ');
|
|
88
|
+
case 'medium': return c.bgYellow(' MEDIUM ');
|
|
89
|
+
case 'low': return c.dim(' LOW ');
|
|
90
|
+
default: return c.dim(` ${severity.toUpperCase()} `);
|
|
91
|
+
}
|
|
34
92
|
}
|
|
35
93
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
94
|
+
function severityIcon(severity) {
|
|
95
|
+
switch (severity) {
|
|
96
|
+
case 'critical': return c.red('\u2718'); // heavy X
|
|
97
|
+
case 'high': return c.red('\u2716'); // heavy X
|
|
98
|
+
case 'medium': return c.yellow('\u25CB'); // circle
|
|
99
|
+
case 'low': return c.dim('\u2022'); // bullet
|
|
100
|
+
default: return ' ';
|
|
101
|
+
}
|
|
39
102
|
}
|
|
40
103
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Fix suggestions for common rules
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const FIX_HINTS = {
|
|
109
|
+
// SEO
|
|
110
|
+
'missing-title': 'Add <title>Your Page Title</title> in <head>',
|
|
111
|
+
'missing-meta-description': 'Add <meta name="description" content="..."> in <head>',
|
|
112
|
+
'missing-h1': 'Add one <h1> heading per page',
|
|
113
|
+
'thin-content': 'Expand main content to 300+ words',
|
|
114
|
+
'missing-viewport': 'Add <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
115
|
+
'missing-og-title': 'Add <meta property="og:title" content="...">',
|
|
116
|
+
'missing-og-description': 'Add <meta property="og:description" content="...">',
|
|
117
|
+
'missing-og-image': 'Add <meta property="og:image" content="https://...">',
|
|
118
|
+
'missing-og-url': 'Add <meta property="og:url" content="https://...">',
|
|
119
|
+
'missing-canonical': 'Add <link rel="canonical" href="...">',
|
|
120
|
+
'missing-json-ld': 'Add JSON-LD structured data — run /claude-rank:rank-schema',
|
|
121
|
+
'missing-favicon': 'Add <link rel="icon" href="/favicon.ico">',
|
|
122
|
+
'no-analytics': 'Add Google Analytics, Plausible, or PostHog',
|
|
123
|
+
'missing-twitter-card': 'Add <meta name="twitter:card" content="summary_large_image">',
|
|
124
|
+
'missing-twitter-image': 'Add <meta name="twitter:image" content="https://...">',
|
|
125
|
+
'missing-lang': 'Add lang="en" to your <html> tag',
|
|
126
|
+
'missing-charset': 'Add <meta charset="utf-8"> in <head>',
|
|
127
|
+
'no-manifest': 'Add <link rel="manifest" href="/manifest.json">',
|
|
128
|
+
'missing-main-landmark': 'Wrap main content in <main>...</main>',
|
|
129
|
+
'missing-nav-landmark': 'Wrap navigation in <nav>...</nav>',
|
|
130
|
+
'missing-footer-landmark': 'Wrap footer in <footer>...</footer>',
|
|
131
|
+
'images-missing-alt': 'Add descriptive alt="" to all <img> tags',
|
|
132
|
+
'images-missing-dimensions': 'Add width/height to <img> tags (prevents CLS)',
|
|
133
|
+
'viewport-not-responsive': 'Use width=device-width in viewport meta',
|
|
134
|
+
'has-noindex': 'Remove noindex from robots meta (unless intentional)',
|
|
135
|
+
'schema-invalid': 'Fix JSON-LD schema — run /claude-rank:rank-schema',
|
|
136
|
+
'multiple-h1': 'Use only one <h1> per page',
|
|
137
|
+
'title-too-long': 'Shorten title to under 60 characters',
|
|
138
|
+
'title-too-short': 'Expand title to at least 20 characters',
|
|
139
|
+
'all-scripts-blocking': 'Add async or defer to <script> tags',
|
|
140
|
+
'title-content-mismatch': 'Align page content with title keywords',
|
|
141
|
+
'meta-content-mismatch': 'Align page content with meta description keywords',
|
|
142
|
+
'duplicate-title': 'Make each page title unique',
|
|
143
|
+
'duplicate-meta-description':'Make each meta description unique',
|
|
144
|
+
|
|
145
|
+
// GEO
|
|
146
|
+
'missing-robots-txt': 'Create robots.txt allowing AI crawlers',
|
|
147
|
+
'missing-sitemap': 'Create sitemap.xml and reference in robots.txt',
|
|
148
|
+
'missing-llms-txt': 'Create llms.txt for AI discoverability',
|
|
149
|
+
'bot-blocked': 'Unblock AI bots in robots.txt (GPTBot, ClaudeBot, etc.)',
|
|
150
|
+
'no-ai-bot-rules': 'Add explicit Allow rules for AI bots in robots.txt',
|
|
151
|
+
'missing-org-schema': 'Add Organization JSON-LD schema',
|
|
152
|
+
'missing-author-schema': 'Add author attribution to article content',
|
|
153
|
+
'thin-content-ai': 'Expand content to 300+ words per page for AI citation',
|
|
154
|
+
'no-question-headers': 'Add question-format H2 headings (What is...? How to...?)',
|
|
155
|
+
'no-definition-patterns': 'Add clear definition patterns for AI extraction',
|
|
156
|
+
'no-data-tables': 'Add data tables to support AI citation',
|
|
157
|
+
'content-not-citation-ready':'Write 120-167 word passages for AI citation fitness',
|
|
158
|
+
'no-direct-answer': 'Start with a direct answer in the first 40-60 words',
|
|
159
|
+
'no-statistics': 'Add statistics and data points to support claims',
|
|
160
|
+
|
|
161
|
+
// AEO
|
|
162
|
+
'missing-faq-schema': 'Add FAQPage JSON-LD for People Also Ask',
|
|
163
|
+
'missing-howto-schema': 'Add HowTo JSON-LD for step-by-step content',
|
|
164
|
+
'missing-speakable-schema': 'Add speakable schema for voice search',
|
|
165
|
+
'no-snippet-answers': 'Add 40-60 word answer paragraphs after H2 questions',
|
|
166
|
+
'missing-content-schema': 'Add Article or WebPage JSON-LD schema',
|
|
167
|
+
'missing-llms-txt-aeo': 'Create llms.txt for answer engine discovery',
|
|
168
|
+
'answers-too-long': 'Trim answer paragraphs to 40-60 words',
|
|
169
|
+
'no-numbered-steps': 'Add numbered/ordered lists for featured snippets',
|
|
170
|
+
'no-voice-friendly-content': 'Add 20-35 word concise answers for voice search',
|
|
171
|
+
'no-paa-patterns': 'Add "People Also Ask" style Q&A sections',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Grouping and formatting
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
44
178
|
function groupFindings(findings) {
|
|
45
179
|
const groups = new Map();
|
|
46
180
|
for (const f of findings) {
|
|
@@ -62,208 +196,290 @@ function groupFindings(findings) {
|
|
|
62
196
|
|
|
63
197
|
function formatFileList(files, max = 3) {
|
|
64
198
|
if (files.length === 0) return '';
|
|
199
|
+
if (files.length === 1) return files[0];
|
|
65
200
|
const shown = files.slice(0, max);
|
|
66
201
|
const rest = files.length - max;
|
|
67
202
|
let out = shown.join(', ');
|
|
68
|
-
if (rest > 0) out +=
|
|
203
|
+
if (rest > 0) out += c.dim(` +${rest} more`);
|
|
69
204
|
return out;
|
|
70
205
|
}
|
|
71
206
|
|
|
72
|
-
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Main report formatter
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
73
210
|
|
|
74
|
-
|
|
75
|
-
* Format a scanner report (SEO, GEO, or AEO) with a box header and grouped findings.
|
|
76
|
-
*/
|
|
77
|
-
function formatReport(result, title, scoreKey) {
|
|
211
|
+
function formatReport(result, title, scoreKey, scannerType) {
|
|
78
212
|
if (result.skipped) {
|
|
79
|
-
return c.yellow(
|
|
213
|
+
return `\n ${c.yellow('\u26A0')} ${c.bold('Skipped:')} ${result.reason}\n`;
|
|
80
214
|
}
|
|
81
215
|
|
|
82
216
|
const score = result.scores[scoreKey];
|
|
83
217
|
const { findings, summary } = result;
|
|
84
|
-
const
|
|
218
|
+
const filesScanned = result.files_scanned ?? result.pages_scanned ?? 1;
|
|
85
219
|
const groups = groupFindings(findings);
|
|
86
220
|
groups.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
|
|
87
221
|
|
|
88
|
-
const
|
|
89
|
-
const hr = '\u2550'.repeat(W);
|
|
222
|
+
const grade = gradeFor(score);
|
|
90
223
|
const lines = [];
|
|
91
224
|
|
|
92
|
-
|
|
93
|
-
lines.push(
|
|
94
|
-
lines.push(
|
|
95
|
-
|
|
96
|
-
const barStr = ` Score: ${score}/100 ${scoreBar(score)} ${scoreLabel(score)}`;
|
|
97
|
-
lines.push(`\u2551${pad(barStr, W + 22)}\u2551`);
|
|
98
|
-
lines.push(`\u2560${hr}\u2563`);
|
|
225
|
+
// ── Header ──────────────────────────────────────────────
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push(` ${c.bold(c.cyan('claude-rank'))} ${c.dim('/')} ${c.bold(title)}`);
|
|
228
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
99
229
|
|
|
100
|
-
|
|
101
|
-
lines.push(`\u2551${pad(` Findings: ${findings.length}`, W)}\u2551`);
|
|
102
|
-
const countsLine = ` Critical: ${summary.critical} High: ${summary.high} Medium: ${summary.medium} Low: ${summary.low}`;
|
|
103
|
-
lines.push(`\u2551${pad(countsLine, W)}\u2551`);
|
|
104
|
-
lines.push(`\u255A${hr}\u255D`);
|
|
230
|
+
// ── Score ───────────────────────────────────────────────
|
|
105
231
|
lines.push('');
|
|
232
|
+
lines.push(` ${grade.color(` ${score} `)} ${scoreBar(score)} ${scoreLabel(score)}`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push(` ${c.dim('Files scanned:')} ${filesScanned} ${c.dim('Findings:')} ${findings.length} ${summary.critical > 0 ? c.red(`Critical: ${summary.critical}`) : c.dim(`Critical: ${summary.critical}`)} ${summary.high > 0 ? c.red(`High: ${summary.high}`) : c.dim(`High: ${summary.high}`)} ${summary.medium > 0 ? c.yellow(`Medium: ${summary.medium}`) : c.dim(`Medium: ${summary.medium}`)} ${c.dim(`Low: ${summary.low}`)}`);
|
|
106
235
|
|
|
236
|
+
// ── No findings ─────────────────────────────────────────
|
|
107
237
|
if (groups.length === 0) {
|
|
108
|
-
lines.push(
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(` ${c.green('\u2714')} ${c.bold(c.green('All checks passed!'))} No issues found.`);
|
|
240
|
+
lines.push('');
|
|
109
241
|
return lines.join('\n');
|
|
110
242
|
}
|
|
111
243
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
244
|
+
// ── Critical & High (must fix) ──────────────────────────
|
|
245
|
+
const critical = groups.filter(g => g.severity === 'critical' || g.severity === 'high');
|
|
246
|
+
const medium = groups.filter(g => g.severity === 'medium');
|
|
247
|
+
const low = groups.filter(g => g.severity === 'low');
|
|
248
|
+
|
|
249
|
+
if (critical.length > 0) {
|
|
250
|
+
lines.push('');
|
|
251
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
252
|
+
lines.push(` ${c.bold(c.red('\u2718 Must Fix'))} ${c.dim(`(${critical.length} issues)`)}`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
for (const g of critical) {
|
|
255
|
+
const badge = severityBadge(g.severity);
|
|
256
|
+
const hint = FIX_HINTS[g.rule];
|
|
257
|
+
const pageCount = g.files.length > 1 ? c.dim(` (${g.files.length} pages)`) : '';
|
|
258
|
+
lines.push(` ${badge} ${c.bold(g.rule)}${pageCount}`);
|
|
259
|
+
lines.push(` ${g.message}`);
|
|
260
|
+
if (hint) {
|
|
261
|
+
lines.push(` ${c.cyan('\u2192')} ${c.cyan(hint)}`);
|
|
262
|
+
}
|
|
120
263
|
if (g.files.length > 0) {
|
|
121
|
-
lines.push(`
|
|
264
|
+
lines.push(` ${c.dim(formatFileList(g.files))}`);
|
|
265
|
+
}
|
|
266
|
+
lines.push('');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Medium (should fix) ─────────────────────────────────
|
|
271
|
+
if (medium.length > 0) {
|
|
272
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
273
|
+
lines.push(` ${c.bold(c.yellow('\u25CB Should Fix'))} ${c.dim(`(${medium.length} issues)`)}`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
for (const g of medium) {
|
|
276
|
+
const hint = FIX_HINTS[g.rule];
|
|
277
|
+
const pageCount = g.files.length > 1 ? c.dim(` (${g.files.length} pages)`) : '';
|
|
278
|
+
lines.push(` ${severityIcon(g.severity)} ${c.bold(g.rule)}${pageCount}`);
|
|
279
|
+
lines.push(` ${g.message}`);
|
|
280
|
+
if (hint) {
|
|
281
|
+
lines.push(` ${c.cyan('\u2192')} ${c.cyan(hint)}`);
|
|
282
|
+
}
|
|
283
|
+
if (g.files.length > 1) {
|
|
284
|
+
lines.push(` ${c.dim(formatFileList(g.files))}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push('');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Low (nice to have) ─────────────────────────────────
|
|
291
|
+
if (low.length > 0) {
|
|
292
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
293
|
+
lines.push(` ${c.dim('\u2022 Nice to Have')} ${c.dim(`(${low.length} issues)`)}`);
|
|
294
|
+
lines.push('');
|
|
295
|
+
for (const g of low) {
|
|
296
|
+
const hint = FIX_HINTS[g.rule];
|
|
297
|
+
const pageCount = g.files.length > 1 ? c.dim(` (${g.files.length} pages)`) : '';
|
|
298
|
+
lines.push(` ${severityIcon(g.severity)} ${c.dim(g.rule)}${pageCount}`);
|
|
299
|
+
lines.push(` ${c.dim(g.message)}`);
|
|
300
|
+
if (hint) {
|
|
301
|
+
lines.push(` ${c.dim('\u2192 ' + hint)}`);
|
|
122
302
|
}
|
|
123
303
|
lines.push('');
|
|
124
304
|
}
|
|
125
305
|
}
|
|
126
306
|
|
|
307
|
+
// ── Next steps ──────────────────────────────────────────
|
|
308
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
309
|
+
lines.push(` ${c.bold('Next Steps')}`);
|
|
310
|
+
lines.push('');
|
|
311
|
+
if (critical.length > 0) {
|
|
312
|
+
lines.push(` ${c.red('1.')} Fix ${c.bold(`${critical.length} critical/high`)} issues first — they have the biggest impact`);
|
|
313
|
+
}
|
|
314
|
+
if (medium.length > 0) {
|
|
315
|
+
const step = critical.length > 0 ? '2.' : '1.';
|
|
316
|
+
lines.push(` ${c.yellow(step)} Address ${c.bold(`${medium.length} medium`)} issues for a solid foundation`);
|
|
317
|
+
}
|
|
318
|
+
if (scannerType === 'seo') {
|
|
319
|
+
lines.push(` ${c.cyan('\u2192')} Run ${c.bold('claude-rank geo .')} to check AI search readiness`);
|
|
320
|
+
lines.push(` ${c.cyan('\u2192')} Run ${c.bold('claude-rank compete <url> .')} to compare vs competitors`);
|
|
321
|
+
} else if (scannerType === 'geo') {
|
|
322
|
+
lines.push(` ${c.cyan('\u2192')} Run ${c.bold('claude-rank aeo .')} to optimize for featured snippets`);
|
|
323
|
+
} else if (scannerType === 'aeo') {
|
|
324
|
+
lines.push(` ${c.cyan('\u2192')} Run ${c.bold('/claude-rank:rank-fix')} to auto-fix all findings`);
|
|
325
|
+
}
|
|
326
|
+
lines.push('');
|
|
327
|
+
|
|
127
328
|
return lines.join('\n');
|
|
128
329
|
}
|
|
129
330
|
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Public exports — scanner reports
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
130
335
|
export function formatSeoReport(result) {
|
|
131
|
-
return formatReport(result, '
|
|
336
|
+
return formatReport(result, 'SEO Audit', 'seo', 'seo');
|
|
132
337
|
}
|
|
133
338
|
|
|
134
339
|
export function formatGeoReport(result) {
|
|
135
|
-
return formatReport(result, '
|
|
340
|
+
return formatReport(result, 'GEO Audit', 'geo', 'geo');
|
|
136
341
|
}
|
|
137
342
|
|
|
138
343
|
export function formatAeoReport(result) {
|
|
139
|
-
return formatReport(result, '
|
|
344
|
+
return formatReport(result, 'AEO Audit', 'aeo', 'aeo');
|
|
140
345
|
}
|
|
141
346
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* Format competitive analysis comparison report.
|
|
147
|
-
*/
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Competitive X-Ray report
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
148
351
|
export function formatCompeteReport(result) {
|
|
149
352
|
if (result.error) {
|
|
150
|
-
return c.red(
|
|
353
|
+
return `\n ${c.red('\u2718')} ${result.error}\n`;
|
|
151
354
|
}
|
|
152
355
|
|
|
153
356
|
const lines = [];
|
|
154
|
-
const W = 62;
|
|
155
|
-
const hr = '\u2550'.repeat(W);
|
|
156
|
-
|
|
157
|
-
// Header
|
|
158
|
-
lines.push(`\u2554${hr}\u2557`);
|
|
159
|
-
lines.push(`\u2551${pad(c.bold(' claude-rank Competitive X-Ray'), W + 9)}\u2551`);
|
|
160
|
-
lines.push(`\u2560${hr}\u2563`);
|
|
161
|
-
lines.push(`\u2551${pad(` ${c.bold('You:')} ${result.you.title || result.you.directory}`, W + 9)}\u2551`);
|
|
162
|
-
lines.push(`\u2551${pad(` ${c.bold('Them:')} ${result.competitor.title || result.competitor.url}`, W + 9)}\u2551`);
|
|
163
|
-
lines.push(`\u2560${hr}\u2563`);
|
|
164
|
-
|
|
165
|
-
// Score summary
|
|
166
357
|
const { youWins, themWins, ties } = result.summary;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
lines.push(`\u255A${hr}\u255D`);
|
|
358
|
+
|
|
359
|
+
// ── Header ──────────────────────────────────────────────
|
|
170
360
|
lines.push('');
|
|
361
|
+
lines.push(` ${c.bold(c.cyan('claude-rank'))} ${c.dim('/')} ${c.bold('Competitive X-Ray')}`);
|
|
362
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
363
|
+
lines.push('');
|
|
364
|
+
lines.push(` ${c.bold('You:')} ${result.you.title || result.you.directory}`);
|
|
365
|
+
lines.push(` ${c.bold('Them:')} ${result.competitor.title || result.competitor.url}`);
|
|
171
366
|
|
|
172
|
-
//
|
|
173
|
-
lines.push(
|
|
367
|
+
// ── Score ───────────────────────────────────────────────
|
|
368
|
+
lines.push('');
|
|
369
|
+
const youColor = youWins >= themWins ? c.green : c.red;
|
|
370
|
+
const themColor = themWins >= youWins ? c.red : c.green;
|
|
371
|
+
lines.push(` ${youColor(`You ${youWins}`)} ${c.dim('vs')} ${themColor(`Them ${themWins}`)} ${c.dim(`(${ties} ties)`)}`);
|
|
174
372
|
lines.push('');
|
|
373
|
+
lines.push(` ${c.bold(result.headline)}`);
|
|
175
374
|
|
|
176
|
-
//
|
|
177
|
-
lines.push(
|
|
178
|
-
lines.push(
|
|
179
|
-
lines.push(c.
|
|
375
|
+
// ── Signal comparison table ─────────────────────────────
|
|
376
|
+
lines.push('');
|
|
377
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
378
|
+
lines.push(` ${pad(c.bold('Signal'), 26)} ${pad(c.bold('You'), 12)} ${pad(c.bold('Them'), 12)} ${c.bold('Result')}`);
|
|
379
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
180
380
|
|
|
181
381
|
for (const v of result.verdicts) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
382
|
+
const icon = v.winner === 'you' ? c.green('\u2714') :
|
|
383
|
+
v.winner === 'them' ? c.red('\u2718') :
|
|
384
|
+
c.dim('\u2500');
|
|
385
|
+
const label = v.winner === 'you' ? c.green('You') :
|
|
386
|
+
v.winner === 'them' ? c.red('Them') :
|
|
387
|
+
c.dim('Tie');
|
|
388
|
+
lines.push(` ${pad(v.area, 24)} ${pad(String(v.you), 10)} ${pad(String(v.them), 10)} ${icon} ${label}`);
|
|
187
389
|
}
|
|
188
|
-
lines.push('');
|
|
390
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
189
391
|
|
|
190
|
-
// Tech stack
|
|
392
|
+
// ── Tech stack ──────────────────────────────────────────
|
|
191
393
|
if (result.competitor.techStack.length > 0 || result.you.techStack.length > 0) {
|
|
192
|
-
lines.push(
|
|
394
|
+
lines.push('');
|
|
395
|
+
lines.push(` ${c.bold('Tech Stack')}`);
|
|
396
|
+
lines.push('');
|
|
193
397
|
if (result.you.techStack.length > 0) {
|
|
194
|
-
|
|
398
|
+
for (const t of result.you.techStack) {
|
|
399
|
+
lines.push(` ${c.green('\u2022')} ${t.tech} ${c.dim(`(${t.category})`)}`);
|
|
400
|
+
}
|
|
195
401
|
} else {
|
|
196
|
-
lines.push(` ${c.
|
|
402
|
+
lines.push(` ${c.dim(' No technologies detected in your project')}`);
|
|
197
403
|
}
|
|
404
|
+
lines.push('');
|
|
405
|
+
lines.push(` ${c.bold('Competitor:')}`);
|
|
198
406
|
if (result.competitor.techStack.length > 0) {
|
|
199
|
-
|
|
407
|
+
for (const t of result.competitor.techStack) {
|
|
408
|
+
lines.push(` ${c.red('\u2022')} ${t.tech} ${c.dim(`(${t.category})`)}`);
|
|
409
|
+
}
|
|
200
410
|
} else {
|
|
201
|
-
lines.push(` ${c.
|
|
411
|
+
lines.push(` ${c.dim(' No technologies detected')}`);
|
|
202
412
|
}
|
|
203
|
-
lines.push('');
|
|
204
413
|
}
|
|
205
414
|
|
|
206
|
-
// Conversion signals
|
|
415
|
+
// ── Conversion signals ──────────────────────────────────
|
|
207
416
|
if (result.competitor.conversionSignals.length > 0 || result.you.conversionSignals.length > 0) {
|
|
208
|
-
lines.push(
|
|
417
|
+
lines.push('');
|
|
418
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
419
|
+
lines.push(` ${c.bold('Conversion Signals')}`);
|
|
420
|
+
lines.push('');
|
|
209
421
|
if (result.you.conversionSignals.length > 0) {
|
|
210
|
-
lines.push(` ${c.green('You:')} ${result.you.conversionSignals.join('
|
|
422
|
+
lines.push(` ${c.green('You:')} ${result.you.conversionSignals.join(' \u2022 ')}`);
|
|
211
423
|
} else {
|
|
212
424
|
lines.push(` ${c.green('You:')} ${c.dim('None detected')}`);
|
|
213
425
|
}
|
|
214
426
|
if (result.competitor.conversionSignals.length > 0) {
|
|
215
|
-
lines.push(` ${c.red('Them:')} ${result.competitor.conversionSignals.join('
|
|
427
|
+
lines.push(` ${c.red('Them:')} ${result.competitor.conversionSignals.join(' \u2022 ')}`);
|
|
216
428
|
} else {
|
|
217
429
|
lines.push(` ${c.red('Them:')} ${c.dim('None detected')}`);
|
|
218
430
|
}
|
|
219
|
-
lines.push('');
|
|
220
431
|
}
|
|
221
432
|
|
|
222
|
-
//
|
|
433
|
+
// ── Action items ────────────────────────────────────────
|
|
223
434
|
if (result.summary.theirAdvantages.length > 0) {
|
|
224
|
-
lines.push(c.bold('Quick Wins — Close These Gaps:'));
|
|
225
|
-
for (const gap of result.summary.theirAdvantages.slice(0, 5)) {
|
|
226
|
-
lines.push(` ${c.yellow('\u26A0')} ${gap}`);
|
|
227
|
-
}
|
|
228
435
|
lines.push('');
|
|
436
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
437
|
+
lines.push(` ${c.bold(c.yellow('Gaps to Close'))}`);
|
|
438
|
+
lines.push('');
|
|
439
|
+
for (const gap of result.summary.theirAdvantages) {
|
|
440
|
+
lines.push(` ${c.yellow('\u2192')} ${gap}`);
|
|
441
|
+
}
|
|
229
442
|
}
|
|
230
443
|
|
|
231
|
-
// Your strengths
|
|
232
444
|
if (result.summary.yourAdvantages.length > 0) {
|
|
233
|
-
lines.push(c.bold('Your Strengths — Keep These:'));
|
|
234
|
-
for (const adv of result.summary.yourAdvantages.slice(0, 5)) {
|
|
235
|
-
lines.push(` ${c.green('\u2713')} ${adv}`);
|
|
236
|
-
}
|
|
237
445
|
lines.push('');
|
|
446
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(56)));
|
|
447
|
+
lines.push(` ${c.bold(c.green('Your Advantages'))}`);
|
|
448
|
+
lines.push('');
|
|
449
|
+
for (const adv of result.summary.yourAdvantages) {
|
|
450
|
+
lines.push(` ${c.green('\u2714')} ${adv}`);
|
|
451
|
+
}
|
|
238
452
|
}
|
|
239
453
|
|
|
454
|
+
lines.push('');
|
|
240
455
|
return lines.join('\n');
|
|
241
456
|
}
|
|
242
457
|
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Schema report
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
|
|
243
462
|
export function formatSchemaReport(results) {
|
|
244
463
|
if (!results || results.length === 0) {
|
|
245
|
-
return c.yellow('No structured data (JSON-LD
|
|
464
|
+
return `\n ${c.yellow('\u26A0')} No structured data (JSON-LD) detected.\n ${c.cyan('\u2192')} Run ${c.bold('/claude-rank:rank-schema')} to generate schema.\n`;
|
|
246
465
|
}
|
|
247
466
|
|
|
248
467
|
const lines = [];
|
|
249
|
-
const W = 48;
|
|
250
|
-
const hr = '\u2550'.repeat(W);
|
|
251
|
-
|
|
252
|
-
lines.push(`\u2554${hr}\u2557`);
|
|
253
|
-
lines.push(`\u2551${pad(c.bold(' claude-rank Schema Report'), W + 9)}\u2551`);
|
|
254
|
-
lines.push(`\u2560${hr}\u2563`);
|
|
255
|
-
lines.push(`\u2551${pad(` Files with schemas: ${results.length}`, W)}\u2551`);
|
|
256
468
|
const totalSchemas = results.reduce((n, r) => n + r.schemas.length, 0);
|
|
257
|
-
|
|
258
|
-
lines.push(
|
|
469
|
+
|
|
470
|
+
lines.push('');
|
|
471
|
+
lines.push(` ${c.bold(c.cyan('claude-rank'))} ${c.dim('/')} ${c.bold('Schema Report')}`);
|
|
472
|
+
lines.push(c.dim(' ' + '\u2500'.repeat(50)));
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push(` ${c.dim('Files with schemas:')} ${results.length} ${c.dim('Total schemas:')} ${totalSchemas}`);
|
|
259
475
|
lines.push('');
|
|
260
476
|
|
|
261
477
|
for (const r of results) {
|
|
262
|
-
lines.push(c.bold(r.file));
|
|
478
|
+
lines.push(` ${c.bold(r.file)}`);
|
|
263
479
|
for (const s of r.schemas) {
|
|
264
480
|
const type = s.type || s['@type'] || 'Unknown';
|
|
265
481
|
const format = s.format || 'JSON-LD';
|
|
266
|
-
lines.push(`
|
|
482
|
+
lines.push(` ${c.green('\u2714')} ${c.cyan(type)} ${c.dim(`(${format})`)}`);
|
|
267
483
|
}
|
|
268
484
|
lines.push('');
|
|
269
485
|
}
|