@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-rank",
3
3
  "description": "The most comprehensive SEO/GEO/AEO plugin for Claude Code. Audit, fix, and dominate search.",
4
- "version": "1.7.0",
4
+ "version": "1.7.2",
5
5
  "author": {
6
6
  "name": "Houseofmvps",
7
7
  "email": "houseofmvps2024@gmail.com"
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. 80+ rules. Competitive X-Ray. Auto-fix everything. Dominate search — traditional and AI.
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
  [![npm version](https://img.shields.io/npm/v/%40houseofmvps%2Fclaude-rank?style=for-the-badge&logo=npm&color=CB3837)](https://www.npmjs.com/package/@houseofmvps/claude-rank)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/%40houseofmvps%2Fclaude-rank?style=for-the-badge&logo=npm&color=blue&label=Monthly%20Downloads)](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. 80+ 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.**
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 █████████████░ (25 rules)
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 — 25 Rules
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 (25 rules) |
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) | 25 rules | Basic |
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
- | **260 tests** | Security module, all scanners, competitive X-Ray, CLI, integration tests |
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 (25 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 |
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 # 260 tests, node:test
512
+ npm test # 265 tests, node:test
513
513
  node tools/<tool>.mjs # No build step
514
514
  ```
515
515
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Standalone CLI: npx claude-rank <command> <directory>
3
- // Commands: scan, geo, aeo, schema, fix
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houseofmvps/claude-rank",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "The most comprehensive SEO/GEO/AEO plugin for Claude Code. Audit, fix, and dominate search — traditional and AI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, HARO.
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
 
@@ -1,46 +1,180 @@
1
1
  /**
2
- * formatter.mjs — Pretty terminal output for claude-rank CLI reports.
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
- const BAR_WIDTH = 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('EXCELLENT');
19
- if (score >= 80) return c.green('GOOD');
20
- if (score >= 60) return c.yellow('NEEDS WORK');
21
- return c.red('POOR');
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 filled = Math.round((score / 100) * BAR_WIDTH);
26
- const empty = BAR_WIDTH - filled;
27
- return '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
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
- function severityColor(severity) {
31
- if (severity === 'critical' || severity === 'high') return c.red;
32
- if (severity === 'medium') return c.yellow;
33
- return c.dim;
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 pad(str, len) {
37
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
38
- return str + ' '.repeat(Math.max(0, len - stripped.length));
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
- * Group findings by rule, aggregating affected files and using the first message.
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 += `, +${rest} more`;
203
+ if (rest > 0) out += c.dim(` +${rest} more`);
69
204
  return out;
70
205
  }
71
206
 
72
- const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
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(`Skipped: ${result.reason}`);
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 files_scanned = result.files_scanned ?? result.pages_scanned ?? 1;
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 W = 48;
89
- const hr = '\u2550'.repeat(W);
222
+ const grade = gradeFor(score);
90
223
  const lines = [];
91
224
 
92
- lines.push(`\u2554${hr}\u2557`);
93
- lines.push(`\u2551${pad(c.bold(` ${title}`), W + 9)}\u2551`);
94
- lines.push(`\u2560${hr}\u2563`);
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
- lines.push(`\u2551${pad(` Files scanned: ${files_scanned}`, W)}\u2551`);
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(c.green('No findings — looking great!'));
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
- lines.push(c.bold('Findings:'));
113
- {
114
- for (const g of groups) {
115
- const colorFn = severityColor(g.severity);
116
- const tag = pad(colorFn(g.severity.toUpperCase()), 10 + 9);
117
- const countSuffix = g.files.length > 1 ? ` (${g.files.length} pages)` : '';
118
- lines.push(` ${tag}${c.bold(g.rule)}${c.dim(countSuffix)}`);
119
- lines.push(` ${g.message}`);
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(` ${c.dim('Files: ' + formatFileList(g.files))}`);
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, 'claude-rank SEO Audit', 'seo');
336
+ return formatReport(result, 'SEO Audit', 'seo', 'seo');
132
337
  }
133
338
 
134
339
  export function formatGeoReport(result) {
135
- return formatReport(result, 'claude-rank GEO Audit', 'geo');
340
+ return formatReport(result, 'GEO Audit', 'geo', 'geo');
136
341
  }
137
342
 
138
343
  export function formatAeoReport(result) {
139
- return formatReport(result, 'claude-rank AEO Audit', 'aeo');
344
+ return formatReport(result, 'AEO Audit', 'aeo', 'aeo');
140
345
  }
141
346
 
142
- /**
143
- * Format schema detection results.
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(`Error: ${result.error}`);
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
- const summaryText = ` Score: ${c.green(`You ${youWins}`)} vs ${c.red(`Them ${themWins}`)} (${ties} ties)`;
168
- lines.push(`\u2551${pad(summaryText, W + 31)}\u2551`);
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
- // Headline
173
- lines.push(c.bold(result.headline));
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
- // Verdicts table
177
- lines.push(c.bold('Signal-by-Signal Comparison:'));
178
- lines.push(` ${pad(c.dim('Area'), 26)} ${pad(c.dim('You'), 12)} ${pad(c.dim('Them'), 12)} ${c.dim('Winner')}`);
179
- lines.push(c.dim(' ' + '\u2500'.repeat(58)));
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 winIcon = v.winner === 'you' ? c.green('\u2713') :
183
- v.winner === 'them' ? c.red('\u2717') : c.dim('\u2500');
184
- const winLabel = v.winner === 'you' ? c.green('You') :
185
- v.winner === 'them' ? c.red('Them') : c.dim('Tie');
186
- lines.push(` ${pad(v.area, 24)} ${pad(String(v.you), 10)} ${pad(String(v.them), 10)} ${winIcon} ${winLabel}`);
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 comparison
392
+ // ── Tech stack ──────────────────────────────────────────
191
393
  if (result.competitor.techStack.length > 0 || result.you.techStack.length > 0) {
192
- lines.push(c.bold('Tech Stack:'));
394
+ lines.push('');
395
+ lines.push(` ${c.bold('Tech Stack')}`);
396
+ lines.push('');
193
397
  if (result.you.techStack.length > 0) {
194
- lines.push(` ${c.green('You:')} ${result.you.techStack.map(t => `${t.tech} ${c.dim(`(${t.category})`)}`).join(', ')}`);
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.green('You:')} ${c.dim('None detected')}`);
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
- lines.push(` ${c.red('Them:')} ${result.competitor.techStack.map(t => `${t.tech} ${c.dim(`(${t.category})`)}`).join(', ')}`);
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.red('Them:')} ${c.dim('None detected')}`);
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(c.bold('Conversion Signals:'));
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
- // Quick wins
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, Microdata, RDFa) detected.');
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
- lines.push(`\u2551${pad(` Total schemas found: ${totalSchemas}`, W)}\u2551`);
258
- lines.push(`\u255A${hr}\u255D`);
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(` ${c.cyan(type)} ${c.dim(`(${format})`)}`);
482
+ lines.push(` ${c.green('\u2714')} ${c.cyan(type)} ${c.dim(`(${format})`)}`);
267
483
  }
268
484
  lines.push('');
269
485
  }