recker 1.0.27 → 1.0.28-next.4354f8c
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/dist/browser/scrape/extractors.js +2 -1
- package/dist/browser/scrape/types.d.ts +2 -1
- package/dist/cli/index.js +142 -3
- package/dist/cli/tui/shell.d.ts +2 -0
- package/dist/cli/tui/shell.js +269 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scrape/extractors.js +2 -1
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/spider.d.ts +59 -0
- package/dist/scrape/spider.js +209 -0
- package/dist/scrape/types.d.ts +2 -1
- package/dist/seo/analyzer.d.ts +42 -0
- package/dist/seo/analyzer.js +727 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.js +2 -0
- package/dist/seo/rules/accessibility.d.ts +2 -0
- package/dist/seo/rules/accessibility.js +694 -0
- package/dist/seo/rules/best-practices.d.ts +2 -0
- package/dist/seo/rules/best-practices.js +188 -0
- package/dist/seo/rules/content.d.ts +2 -0
- package/dist/seo/rules/content.js +236 -0
- package/dist/seo/rules/crawl.d.ts +2 -0
- package/dist/seo/rules/crawl.js +307 -0
- package/dist/seo/rules/cwv.d.ts +2 -0
- package/dist/seo/rules/cwv.js +337 -0
- package/dist/seo/rules/ecommerce.d.ts +2 -0
- package/dist/seo/rules/ecommerce.js +252 -0
- package/dist/seo/rules/i18n.d.ts +2 -0
- package/dist/seo/rules/i18n.js +222 -0
- package/dist/seo/rules/images.d.ts +2 -0
- package/dist/seo/rules/images.js +180 -0
- package/dist/seo/rules/index.d.ts +52 -0
- package/dist/seo/rules/index.js +143 -0
- package/dist/seo/rules/internal-linking.d.ts +2 -0
- package/dist/seo/rules/internal-linking.js +375 -0
- package/dist/seo/rules/links.d.ts +2 -0
- package/dist/seo/rules/links.js +150 -0
- package/dist/seo/rules/local.d.ts +2 -0
- package/dist/seo/rules/local.js +265 -0
- package/dist/seo/rules/meta.d.ts +2 -0
- package/dist/seo/rules/meta.js +523 -0
- package/dist/seo/rules/mobile.d.ts +2 -0
- package/dist/seo/rules/mobile.js +71 -0
- package/dist/seo/rules/performance.d.ts +2 -0
- package/dist/seo/rules/performance.js +246 -0
- package/dist/seo/rules/pwa.d.ts +2 -0
- package/dist/seo/rules/pwa.js +302 -0
- package/dist/seo/rules/readability.d.ts +2 -0
- package/dist/seo/rules/readability.js +255 -0
- package/dist/seo/rules/schema.d.ts +2 -0
- package/dist/seo/rules/schema.js +54 -0
- package/dist/seo/rules/security.d.ts +2 -0
- package/dist/seo/rules/security.js +525 -0
- package/dist/seo/rules/social.d.ts +2 -0
- package/dist/seo/rules/social.js +373 -0
- package/dist/seo/rules/structural.d.ts +2 -0
- package/dist/seo/rules/structural.js +155 -0
- package/dist/seo/rules/technical.d.ts +2 -0
- package/dist/seo/rules/technical.js +223 -0
- package/dist/seo/rules/thresholds.d.ts +196 -0
- package/dist/seo/rules/thresholds.js +118 -0
- package/dist/seo/rules/types.d.ts +346 -0
- package/dist/seo/rules/types.js +11 -0
- package/dist/seo/types.d.ts +160 -0
- package/dist/seo/types.js +1 -0
- package/dist/utils/columns.d.ts +14 -0
- package/dist/utils/columns.js +69 -0
- package/package.json +1 -1
|
@@ -76,6 +76,7 @@ export function extractImages($, options) {
|
|
|
76
76
|
height: height ? parseInt(height, 10) : undefined,
|
|
77
77
|
srcset: $el.attr('srcset'),
|
|
78
78
|
loading: $el.attr('loading'),
|
|
79
|
+
decoding: $el.attr('decoding'),
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
return images;
|
|
@@ -117,7 +118,7 @@ export function extractMeta($) {
|
|
|
117
118
|
meta.author = content;
|
|
118
119
|
break;
|
|
119
120
|
case 'robots':
|
|
120
|
-
meta.robots = content;
|
|
121
|
+
meta.robots = content.split(',').map((r) => r.trim().toLowerCase());
|
|
121
122
|
break;
|
|
122
123
|
case 'viewport':
|
|
123
124
|
meta.viewport = content;
|
|
@@ -14,13 +14,14 @@ export interface ExtractedImage {
|
|
|
14
14
|
height?: number;
|
|
15
15
|
srcset?: string;
|
|
16
16
|
loading?: 'lazy' | 'eager';
|
|
17
|
+
decoding?: 'async' | 'auto' | 'sync';
|
|
17
18
|
}
|
|
18
19
|
export interface ExtractedMeta {
|
|
19
20
|
title?: string;
|
|
20
21
|
description?: string;
|
|
21
22
|
keywords?: string[];
|
|
22
23
|
author?: string;
|
|
23
|
-
robots?: string;
|
|
24
|
+
robots?: string[];
|
|
24
25
|
canonical?: string;
|
|
25
26
|
viewport?: string;
|
|
26
27
|
charset?: string;
|
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { program } from 'commander';
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import colors from '../utils/colors.js';
|
|
6
|
+
import { formatColumns } from '../utils/columns.js';
|
|
6
7
|
async function readStdin() {
|
|
7
8
|
if (process.stdin.isTTY) {
|
|
8
9
|
return null;
|
|
@@ -120,7 +121,13 @@ async function main() {
|
|
|
120
121
|
}
|
|
121
122
|
return { method, url, headers, data };
|
|
122
123
|
}
|
|
123
|
-
const
|
|
124
|
+
const utilityFunctions = [
|
|
125
|
+
'registry', 'presetRegistry', 'detectPreset', 'getPreset',
|
|
126
|
+
'listPresets', 'listAIPresets', 'listCloudPresets', 'listSaaSPresets', 'listDevToolsPresets'
|
|
127
|
+
];
|
|
128
|
+
const PRESET_NAMES = Object.keys(presets)
|
|
129
|
+
.filter(k => !utilityFunctions.includes(k) && !k.startsWith('_') && typeof presets[k] === 'function')
|
|
130
|
+
.sort();
|
|
124
131
|
program
|
|
125
132
|
.name('rek')
|
|
126
133
|
.description('The HTTP Client for Humans (and Robots)')
|
|
@@ -131,7 +138,7 @@ async function main() {
|
|
|
131
138
|
.option('-o, --output <file>', 'Write response body to file')
|
|
132
139
|
.option('-j, --json', 'Force JSON content-type')
|
|
133
140
|
.option('-e, --env [path]', 'Load .env file from current directory or specified path')
|
|
134
|
-
.addHelpText('after', `
|
|
141
|
+
.addHelpText('after', () => `
|
|
135
142
|
${colors.bold(colors.yellow('Examples:'))}
|
|
136
143
|
${colors.green('$ rek httpbin.org/json')}
|
|
137
144
|
${colors.green('$ rek post api.com/users name="Cyber" role="Admin"')}
|
|
@@ -139,7 +146,7 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
139
146
|
${colors.green('$ rek @openai/v1/chat/completions model="gpt-5.1"')}
|
|
140
147
|
|
|
141
148
|
${colors.bold(colors.yellow('Available Presets:'))}
|
|
142
|
-
|
|
149
|
+
${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform: colors.cyan })}
|
|
143
150
|
`)
|
|
144
151
|
.action(async (args, options) => {
|
|
145
152
|
if (args.length === 0) {
|
|
@@ -381,6 +388,138 @@ ${colors.bold('Details:')}`);
|
|
|
381
388
|
process.exit(1);
|
|
382
389
|
}
|
|
383
390
|
});
|
|
391
|
+
program
|
|
392
|
+
.command('seo')
|
|
393
|
+
.description('Analyze page SEO (title, meta, headings, links, images, structured data)')
|
|
394
|
+
.argument('<url>', 'URL to analyze')
|
|
395
|
+
.option('-a, --all', 'Show all checks including passed ones')
|
|
396
|
+
.option('--format <format>', 'Output format: text (default) or json', 'text')
|
|
397
|
+
.addHelpText('after', `
|
|
398
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
399
|
+
${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
|
|
400
|
+
${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
|
|
401
|
+
${colors.green('$ rek seo example.com --format json')} ${colors.gray('Output as JSON')}
|
|
402
|
+
${colors.green('$ rek seo example.com --format json | jq')} ${colors.gray('Pipe to jq for processing')}
|
|
403
|
+
|
|
404
|
+
${colors.bold(colors.yellow('Checks:'))}
|
|
405
|
+
${colors.cyan('Title Tag')} Length and presence
|
|
406
|
+
${colors.cyan('Meta Description')} Length and presence
|
|
407
|
+
${colors.cyan('Headings')} H1 presence and hierarchy
|
|
408
|
+
${colors.cyan('Images')} Alt text coverage
|
|
409
|
+
${colors.cyan('Links')} Internal/external distribution
|
|
410
|
+
${colors.cyan('OpenGraph')} Social sharing meta tags
|
|
411
|
+
${colors.cyan('Twitter Card')} Twitter sharing meta tags
|
|
412
|
+
${colors.cyan('Structured Data')} JSON-LD presence
|
|
413
|
+
${colors.cyan('Technical')} Canonical, viewport, lang
|
|
414
|
+
`)
|
|
415
|
+
.action(async (url, options) => {
|
|
416
|
+
if (!url.startsWith('http'))
|
|
417
|
+
url = `https://${url}`;
|
|
418
|
+
const isJson = options.format === 'json';
|
|
419
|
+
const { createClient } = await import('../core/client.js');
|
|
420
|
+
const { analyzeSeo } = await import('../seo/analyzer.js');
|
|
421
|
+
if (!isJson) {
|
|
422
|
+
console.log(colors.gray(`Analyzing SEO for ${url}...`));
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const client = createClient({ timeout: 30000 });
|
|
426
|
+
const res = await client.get(url);
|
|
427
|
+
const html = await res.text();
|
|
428
|
+
const report = await analyzeSeo(html, { baseUrl: url });
|
|
429
|
+
if (isJson) {
|
|
430
|
+
const jsonOutput = {
|
|
431
|
+
url,
|
|
432
|
+
analyzedAt: new Date().toISOString(),
|
|
433
|
+
score: report.score,
|
|
434
|
+
grade: report.grade,
|
|
435
|
+
title: report.title,
|
|
436
|
+
metaDescription: report.metaDescription,
|
|
437
|
+
content: report.content,
|
|
438
|
+
headings: report.headings,
|
|
439
|
+
links: report.links,
|
|
440
|
+
images: report.images,
|
|
441
|
+
openGraph: report.social.openGraph,
|
|
442
|
+
twitterCard: report.social.twitterCard,
|
|
443
|
+
jsonLd: report.jsonLd,
|
|
444
|
+
technical: report.technical,
|
|
445
|
+
checks: report.checks,
|
|
446
|
+
summary: {
|
|
447
|
+
total: report.checks.length,
|
|
448
|
+
passed: report.checks.filter(c => c.status === 'pass').length,
|
|
449
|
+
warnings: report.checks.filter(c => c.status === 'warn').length,
|
|
450
|
+
errors: report.checks.filter(c => c.status === 'fail').length,
|
|
451
|
+
info: report.checks.filter(c => c.status === 'info').length,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let gradeColor = colors.red;
|
|
458
|
+
if (report.grade === 'A')
|
|
459
|
+
gradeColor = colors.green;
|
|
460
|
+
else if (report.grade === 'B')
|
|
461
|
+
gradeColor = colors.blue;
|
|
462
|
+
else if (report.grade === 'C')
|
|
463
|
+
gradeColor = colors.yellow;
|
|
464
|
+
console.log(`
|
|
465
|
+
${colors.bold(colors.cyan('🔍 SEO Analysis Report'))}
|
|
466
|
+
${colors.gray('URL:')} ${url}
|
|
467
|
+
${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray('Score:')} ${report.score}/100
|
|
468
|
+
`);
|
|
469
|
+
if (report.title) {
|
|
470
|
+
console.log(`${colors.bold('Title:')} ${colors.gray(report.title.text.slice(0, 60))}${report.title.text.length > 60 ? '...' : ''} ${colors.gray(`(${report.title.length} chars)`)}`);
|
|
471
|
+
}
|
|
472
|
+
if (report.metaDescription) {
|
|
473
|
+
console.log(`${colors.bold('Description:')} ${colors.gray(report.metaDescription.text.slice(0, 80))}${report.metaDescription.text.length > 80 ? '...' : ''}`);
|
|
474
|
+
}
|
|
475
|
+
console.log('');
|
|
476
|
+
console.log(`${colors.bold('Content Metrics:')}`);
|
|
477
|
+
console.log(` ${colors.gray('Words:')} ${report.content.wordCount} ${colors.gray('Reading time:')} ~${report.content.readingTimeMinutes} min`);
|
|
478
|
+
console.log(` ${colors.gray('Headings:')} H1×${report.headings.h1Count}, total ${report.headings.structure.length}`);
|
|
479
|
+
console.log(` ${colors.gray('Links:')} ${report.links.total} (${report.links.internal} internal, ${report.links.external} external)`);
|
|
480
|
+
console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log(`${colors.bold('Checks:')}`);
|
|
483
|
+
const checksToShow = options.all
|
|
484
|
+
? report.checks
|
|
485
|
+
: report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
|
|
486
|
+
if (checksToShow.length === 0 && !options.all) {
|
|
487
|
+
console.log(colors.green(' All checks passed! Use -a to see details.'));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
for (const check of checksToShow) {
|
|
491
|
+
const icon = check.status === 'pass' ? colors.green('✔')
|
|
492
|
+
: check.status === 'warn' ? colors.yellow('⚠')
|
|
493
|
+
: check.status === 'fail' ? colors.red('✖')
|
|
494
|
+
: colors.gray('ℹ');
|
|
495
|
+
const name = colors.bold(check.name.padEnd(18));
|
|
496
|
+
console.log(` ${icon} ${name} ${check.message}`);
|
|
497
|
+
if (check.recommendation && check.status !== 'pass') {
|
|
498
|
+
console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
|
|
499
|
+
}
|
|
500
|
+
const evidence = check.evidence;
|
|
501
|
+
if (evidence && check.status !== 'pass') {
|
|
502
|
+
if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
|
|
503
|
+
const items = evidence.found.slice(0, 3);
|
|
504
|
+
console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
|
|
505
|
+
}
|
|
506
|
+
if (evidence.example) {
|
|
507
|
+
console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
console.log('');
|
|
513
|
+
if (report.jsonLd.count > 0) {
|
|
514
|
+
console.log(`${colors.bold('Structured Data:')} ${report.jsonLd.types.join(', ') || 'Present'}`);
|
|
515
|
+
console.log('');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.error(colors.red(`SEO analysis failed: ${error.message}`));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
384
523
|
program
|
|
385
524
|
.command('ip')
|
|
386
525
|
.description('Get IP address intelligence using local GeoLite2 database')
|
package/dist/cli/tui/shell.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare class RekShell {
|
|
|
43
43
|
private runWhois;
|
|
44
44
|
private runTLS;
|
|
45
45
|
private runSecurityGrader;
|
|
46
|
+
private runSeo;
|
|
46
47
|
private runIpIntelligence;
|
|
47
48
|
private runDNS;
|
|
48
49
|
private runDNSPropagation;
|
|
@@ -50,6 +51,7 @@ export declare class RekShell {
|
|
|
50
51
|
private runRDAP;
|
|
51
52
|
private runPing;
|
|
52
53
|
private runScrap;
|
|
54
|
+
private runSpider;
|
|
53
55
|
private runSelect;
|
|
54
56
|
private runSelectText;
|
|
55
57
|
private runSelectAttr;
|
package/dist/cli/tui/shell.js
CHANGED
|
@@ -10,10 +10,12 @@ import { inspectTLS } from '../../utils/tls-inspector.js';
|
|
|
10
10
|
import { getSecurityRecords } from '../../utils/dns-toolkit.js';
|
|
11
11
|
import { rdap } from '../../utils/rdap.js';
|
|
12
12
|
import { ScrapeDocument } from '../../scrape/document.js';
|
|
13
|
+
import { Spider } from '../../scrape/spider.js';
|
|
13
14
|
import colors from '../../utils/colors.js';
|
|
14
15
|
import { getShellSearch } from './shell-search.js';
|
|
15
16
|
import { openSearchPanel } from './search-panel.js';
|
|
16
17
|
import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
|
|
18
|
+
import { analyzeSeo } from '../../seo/index.js';
|
|
17
19
|
let highlight;
|
|
18
20
|
async function initDependencies() {
|
|
19
21
|
if (!highlight) {
|
|
@@ -93,7 +95,7 @@ export class RekShell {
|
|
|
93
95
|
'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
|
|
94
96
|
'ws', 'udp', 'load', 'chat', 'ai',
|
|
95
97
|
'whois', 'tls', 'ssl', 'security', 'ip', 'dns', 'dns:propagate', 'dns:email', 'rdap', 'ping',
|
|
96
|
-
'scrap', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
|
|
98
|
+
'scrap', 'spider', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
|
|
97
99
|
'?', 'search', 'suggest', 'example',
|
|
98
100
|
'help', 'clear', 'exit', 'set', 'url', 'vars', 'env'
|
|
99
101
|
];
|
|
@@ -343,6 +345,9 @@ export class RekShell {
|
|
|
343
345
|
case 'security':
|
|
344
346
|
await this.runSecurityGrader(parts[1]);
|
|
345
347
|
return;
|
|
348
|
+
case 'seo':
|
|
349
|
+
await this.runSeo(parts[1], parts.includes('-a') || parts.includes('--all'), parts.includes('--format') && parts[parts.indexOf('--format') + 1] === 'json');
|
|
350
|
+
return;
|
|
346
351
|
case 'ip':
|
|
347
352
|
await this.runIpIntelligence(parts[1]);
|
|
348
353
|
return;
|
|
@@ -364,6 +369,9 @@ export class RekShell {
|
|
|
364
369
|
case 'scrap':
|
|
365
370
|
await this.runScrap(parts[1]);
|
|
366
371
|
return;
|
|
372
|
+
case 'spider':
|
|
373
|
+
await this.runSpider(parts.slice(1));
|
|
374
|
+
return;
|
|
367
375
|
case '$':
|
|
368
376
|
await this.runSelect(parts.slice(1).join(' '));
|
|
369
377
|
return;
|
|
@@ -944,6 +952,156 @@ ${colors.bold('Details:')}`);
|
|
|
944
952
|
}
|
|
945
953
|
console.log('');
|
|
946
954
|
}
|
|
955
|
+
async runSeo(url, showAll = false, jsonOutput = false) {
|
|
956
|
+
if (!url) {
|
|
957
|
+
url = this.currentDocUrl || this.baseUrl || '';
|
|
958
|
+
if (!url) {
|
|
959
|
+
console.log(colors.yellow('Usage: seo <url> [-a] [--format json]'));
|
|
960
|
+
console.log(colors.gray(' Examples: seo google.com | seo https://example.com -a'));
|
|
961
|
+
console.log(colors.gray(' -a, --all Show all checks (including passed)'));
|
|
962
|
+
console.log(colors.gray(' --format json Output raw JSON for programmatic use'));
|
|
963
|
+
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else if (!url.startsWith('http') && !url.startsWith('-')) {
|
|
968
|
+
url = `https://${url}`;
|
|
969
|
+
}
|
|
970
|
+
if (!jsonOutput) {
|
|
971
|
+
console.log(colors.gray(`Analyzing SEO for ${url}...`));
|
|
972
|
+
}
|
|
973
|
+
const startTime = performance.now();
|
|
974
|
+
try {
|
|
975
|
+
const res = await this.client.get(url);
|
|
976
|
+
const html = await res.text();
|
|
977
|
+
const duration = Math.round(performance.now() - startTime);
|
|
978
|
+
const report = await analyzeSeo(html, { baseUrl: url });
|
|
979
|
+
if (jsonOutput) {
|
|
980
|
+
const jsonResult = {
|
|
981
|
+
url,
|
|
982
|
+
analyzedAt: new Date().toISOString(),
|
|
983
|
+
durationMs: duration,
|
|
984
|
+
score: report.score,
|
|
985
|
+
grade: report.grade,
|
|
986
|
+
title: report.title,
|
|
987
|
+
metaDescription: report.metaDescription,
|
|
988
|
+
content: report.content,
|
|
989
|
+
headings: report.headings,
|
|
990
|
+
links: report.links,
|
|
991
|
+
images: report.images,
|
|
992
|
+
openGraph: report.social.openGraph,
|
|
993
|
+
twitterCard: report.social.twitterCard,
|
|
994
|
+
jsonLd: report.jsonLd,
|
|
995
|
+
technical: report.technical,
|
|
996
|
+
checks: report.checks,
|
|
997
|
+
summary: {
|
|
998
|
+
total: report.checks.length,
|
|
999
|
+
passed: report.checks.filter(c => c.status === 'pass').length,
|
|
1000
|
+
warnings: report.checks.filter(c => c.status === 'warn').length,
|
|
1001
|
+
errors: report.checks.filter(c => c.status === 'fail').length,
|
|
1002
|
+
info: report.checks.filter(c => c.status === 'info').length,
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
1006
|
+
this.lastResponse = jsonResult;
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
let gradeColor = colors.red;
|
|
1010
|
+
if (report.grade === 'A')
|
|
1011
|
+
gradeColor = colors.green;
|
|
1012
|
+
else if (report.grade === 'B')
|
|
1013
|
+
gradeColor = colors.blue;
|
|
1014
|
+
else if (report.grade === 'C')
|
|
1015
|
+
gradeColor = colors.yellow;
|
|
1016
|
+
else if (report.grade === 'D')
|
|
1017
|
+
gradeColor = colors.magenta;
|
|
1018
|
+
console.log(`
|
|
1019
|
+
${colors.bold(colors.cyan('🔍 SEO Analysis Report'))} ${colors.gray(`(${duration}ms)`)}
|
|
1020
|
+
Grade: ${gradeColor(colors.bold(report.grade))} (${report.score}/100)
|
|
1021
|
+
`);
|
|
1022
|
+
if (report.title) {
|
|
1023
|
+
console.log(colors.bold('Title:') + ` ${report.title.text} ` + colors.gray(`(${report.title.length} chars)`));
|
|
1024
|
+
}
|
|
1025
|
+
if (report.metaDescription) {
|
|
1026
|
+
const desc = report.metaDescription.text.length > 80
|
|
1027
|
+
? report.metaDescription.text.slice(0, 77) + '...'
|
|
1028
|
+
: report.metaDescription.text;
|
|
1029
|
+
console.log(colors.bold('Description:') + ` ${desc} ` + colors.gray(`(${report.metaDescription.length} chars)`));
|
|
1030
|
+
}
|
|
1031
|
+
if (report.content) {
|
|
1032
|
+
console.log(colors.bold('Content:') + ` ${report.content.wordCount} words, ${report.content.paragraphCount} paragraphs, ~${report.content.readingTimeMinutes} min read`);
|
|
1033
|
+
}
|
|
1034
|
+
console.log('');
|
|
1035
|
+
console.log(colors.bold('Checks:'));
|
|
1036
|
+
const checksToShow = showAll
|
|
1037
|
+
? report.checks
|
|
1038
|
+
: report.checks.filter(c => c.status !== 'pass');
|
|
1039
|
+
const failed = checksToShow.filter(c => c.status === 'fail');
|
|
1040
|
+
const warnings = checksToShow.filter(c => c.status === 'warn');
|
|
1041
|
+
const info = checksToShow.filter(c => c.status === 'info');
|
|
1042
|
+
const passed = showAll ? checksToShow.filter(c => c.status === 'pass') : [];
|
|
1043
|
+
const displayCheck = (check) => {
|
|
1044
|
+
let icon;
|
|
1045
|
+
let nameColor;
|
|
1046
|
+
switch (check.status) {
|
|
1047
|
+
case 'pass':
|
|
1048
|
+
icon = colors.green('✔');
|
|
1049
|
+
nameColor = colors.green;
|
|
1050
|
+
break;
|
|
1051
|
+
case 'warn':
|
|
1052
|
+
icon = colors.yellow('⚠');
|
|
1053
|
+
nameColor = colors.yellow;
|
|
1054
|
+
break;
|
|
1055
|
+
case 'fail':
|
|
1056
|
+
icon = colors.red('✖');
|
|
1057
|
+
nameColor = colors.red;
|
|
1058
|
+
break;
|
|
1059
|
+
default:
|
|
1060
|
+
icon = colors.blue('ℹ');
|
|
1061
|
+
nameColor = colors.blue;
|
|
1062
|
+
}
|
|
1063
|
+
console.log(` ${icon} ${nameColor(check.name.padEnd(22))} ${check.message}`);
|
|
1064
|
+
if (check.recommendation && check.status !== 'pass') {
|
|
1065
|
+
console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
|
|
1066
|
+
}
|
|
1067
|
+
const evidence = check.evidence;
|
|
1068
|
+
if (evidence && check.status !== 'pass') {
|
|
1069
|
+
if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
|
|
1070
|
+
const items = evidence.found.slice(0, 3);
|
|
1071
|
+
console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (evidence.example) {
|
|
1074
|
+
console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
if (failed.length > 0) {
|
|
1079
|
+
console.log(colors.red(`\n Errors (${failed.length}):`));
|
|
1080
|
+
failed.forEach(displayCheck);
|
|
1081
|
+
}
|
|
1082
|
+
if (warnings.length > 0) {
|
|
1083
|
+
console.log(colors.yellow(`\n Warnings (${warnings.length}):`));
|
|
1084
|
+
warnings.forEach(displayCheck);
|
|
1085
|
+
}
|
|
1086
|
+
if (info.length > 0) {
|
|
1087
|
+
console.log(colors.blue(`\n Info (${info.length}):`));
|
|
1088
|
+
info.forEach(displayCheck);
|
|
1089
|
+
}
|
|
1090
|
+
if (passed.length > 0) {
|
|
1091
|
+
console.log(colors.green(`\n Passed (${passed.length}):`));
|
|
1092
|
+
passed.forEach(displayCheck);
|
|
1093
|
+
}
|
|
1094
|
+
if (!showAll && report.checks.filter(c => c.status === 'pass').length > 0) {
|
|
1095
|
+
console.log(colors.gray(`\n ${report.checks.filter(c => c.status === 'pass').length} checks passed. Use -a to show all.`));
|
|
1096
|
+
}
|
|
1097
|
+
console.log('');
|
|
1098
|
+
this.lastResponse = report;
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
console.error(colors.red(`SEO analysis failed: ${error.message}`));
|
|
1102
|
+
}
|
|
1103
|
+
console.log('');
|
|
1104
|
+
}
|
|
947
1105
|
async runIpIntelligence(address) {
|
|
948
1106
|
if (!address) {
|
|
949
1107
|
console.log(colors.yellow('Usage: ip <address>'));
|
|
@@ -1280,6 +1438,105 @@ ${colors.bold('Network:')}
|
|
|
1280
1438
|
}
|
|
1281
1439
|
console.log('');
|
|
1282
1440
|
}
|
|
1441
|
+
async runSpider(args) {
|
|
1442
|
+
let url = '';
|
|
1443
|
+
let maxDepth = 3;
|
|
1444
|
+
let maxPages = 100;
|
|
1445
|
+
let concurrency = 5;
|
|
1446
|
+
for (let i = 0; i < args.length; i++) {
|
|
1447
|
+
const arg = args[i];
|
|
1448
|
+
if (arg.startsWith('--depth=') || arg.startsWith('-d=')) {
|
|
1449
|
+
maxDepth = parseInt(arg.split('=')[1]) || 3;
|
|
1450
|
+
}
|
|
1451
|
+
else if (arg.startsWith('--limit=') || arg.startsWith('-l=')) {
|
|
1452
|
+
maxPages = parseInt(arg.split('=')[1]) || 100;
|
|
1453
|
+
}
|
|
1454
|
+
else if (arg.startsWith('--concurrency=') || arg.startsWith('-c=')) {
|
|
1455
|
+
concurrency = parseInt(arg.split('=')[1]) || 5;
|
|
1456
|
+
}
|
|
1457
|
+
else if (!arg.startsWith('-')) {
|
|
1458
|
+
url = arg;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (!url) {
|
|
1462
|
+
if (!this.baseUrl) {
|
|
1463
|
+
console.log(colors.yellow('Usage: spider <url> [options]'));
|
|
1464
|
+
console.log(colors.gray(' Options:'));
|
|
1465
|
+
console.log(colors.gray(' --depth=3 Max crawl depth'));
|
|
1466
|
+
console.log(colors.gray(' --limit=100 Max pages to crawl'));
|
|
1467
|
+
console.log(colors.gray(' --concurrency=5 Concurrent requests'));
|
|
1468
|
+
console.log(colors.gray(' Examples:'));
|
|
1469
|
+
console.log(colors.gray(' spider https://example.com'));
|
|
1470
|
+
console.log(colors.gray(' spider https://example.com --depth=2 --limit=50'));
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
url = this.baseUrl;
|
|
1474
|
+
}
|
|
1475
|
+
else if (!url.startsWith('http')) {
|
|
1476
|
+
url = `https://${url}`;
|
|
1477
|
+
}
|
|
1478
|
+
console.log(colors.cyan(`\nSpider starting: ${url}`));
|
|
1479
|
+
console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}`));
|
|
1480
|
+
console.log('');
|
|
1481
|
+
const spider = new Spider({
|
|
1482
|
+
maxDepth,
|
|
1483
|
+
maxPages,
|
|
1484
|
+
concurrency,
|
|
1485
|
+
sameDomain: true,
|
|
1486
|
+
delay: 100,
|
|
1487
|
+
onProgress: (progress) => {
|
|
1488
|
+
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
1489
|
+
},
|
|
1490
|
+
});
|
|
1491
|
+
try {
|
|
1492
|
+
const result = await spider.crawl(url);
|
|
1493
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
1494
|
+
console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
1495
|
+
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
1496
|
+
console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
|
|
1497
|
+
console.log(` ${colors.cyan('Errors')}: ${result.errors.length}`);
|
|
1498
|
+
const byDepth = new Map();
|
|
1499
|
+
for (const page of result.pages) {
|
|
1500
|
+
byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
|
|
1501
|
+
}
|
|
1502
|
+
console.log(colors.bold('\n Pages by depth:'));
|
|
1503
|
+
for (const [depth, count] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
|
|
1504
|
+
const bar = '█'.repeat(Math.min(count, 40));
|
|
1505
|
+
console.log(` ${colors.gray(`d${depth}:`)} ${bar} ${count}`);
|
|
1506
|
+
}
|
|
1507
|
+
const topPages = [...result.pages]
|
|
1508
|
+
.filter(p => !p.error)
|
|
1509
|
+
.sort((a, b) => b.links.length - a.links.length)
|
|
1510
|
+
.slice(0, 10);
|
|
1511
|
+
if (topPages.length > 0) {
|
|
1512
|
+
console.log(colors.bold('\n Top pages by outgoing links:'));
|
|
1513
|
+
for (const page of topPages) {
|
|
1514
|
+
const title = page.title.slice(0, 40) || new URL(page.url).pathname;
|
|
1515
|
+
console.log(` ${colors.cyan(page.links.length.toString().padStart(3))} ${title}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (result.errors.length > 0 && result.errors.length <= 10) {
|
|
1519
|
+
console.log(colors.bold('\n Errors:'));
|
|
1520
|
+
for (const err of result.errors) {
|
|
1521
|
+
const path = new URL(err.url).pathname;
|
|
1522
|
+
console.log(` ${colors.red('✗')} ${path.slice(0, 40)} ${colors.gray('→')} ${err.error.slice(0, 30)}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
else if (result.errors.length > 10) {
|
|
1526
|
+
console.log(colors.yellow(`\n ${result.errors.length} errors (showing first 10):`));
|
|
1527
|
+
for (const err of result.errors.slice(0, 10)) {
|
|
1528
|
+
const path = new URL(err.url).pathname;
|
|
1529
|
+
console.log(` ${colors.red('✗')} ${path.slice(0, 40)} ${colors.gray('→')} ${err.error.slice(0, 30)}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
this.lastResponse = result;
|
|
1533
|
+
console.log(colors.gray('\n Result stored in lastResponse. Use $links to explore.'));
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
console.error(colors.red(`Spider failed: ${error.message}`));
|
|
1537
|
+
}
|
|
1538
|
+
console.log('');
|
|
1539
|
+
}
|
|
1283
1540
|
async runSelect(selector) {
|
|
1284
1541
|
if (!this.currentDoc) {
|
|
1285
1542
|
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
@@ -2182,6 +2439,9 @@ ${colors.bold('Network:')}
|
|
|
2182
2439
|
${colors.green('dns <domain>')} Full DNS lookup (A, AAAA, MX, NS, SPF, DMARC).
|
|
2183
2440
|
${colors.green('rdap <domain>')} RDAP lookup (modern WHOIS).
|
|
2184
2441
|
${colors.green('ping <host>')} Quick TCP connectivity check.
|
|
2442
|
+
${colors.green('seo <url> [-a] [--format json]')} SEO analysis (70+ rules).
|
|
2443
|
+
${colors.gray('-a, --all Show all checks including passed')}
|
|
2444
|
+
${colors.gray('--format json Output raw JSON for programmatic use')}
|
|
2185
2445
|
|
|
2186
2446
|
${colors.bold('Web Scraping:')}
|
|
2187
2447
|
${colors.green('scrap <url>')} Fetch and parse HTML document.
|
|
@@ -2201,6 +2461,13 @@ ${colors.bold('Network:')}
|
|
|
2201
2461
|
${colors.green('$beautify:save [f]')} Save beautified code to file.
|
|
2202
2462
|
${colors.green('$table <selector>')} Extract table as data.
|
|
2203
2463
|
|
|
2464
|
+
${colors.bold('Web Crawler:')}
|
|
2465
|
+
${colors.green('spider <url>')} Crawl website following internal links.
|
|
2466
|
+
${colors.gray('Options:')}
|
|
2467
|
+
${colors.white('--depth=3')} ${colors.gray('Maximum depth to crawl')}
|
|
2468
|
+
${colors.white('--limit=100')} ${colors.gray('Maximum pages to crawl')}
|
|
2469
|
+
${colors.white('--concurrency=5')} ${colors.gray('Parallel requests')}
|
|
2470
|
+
|
|
2204
2471
|
${colors.bold('Documentation:')}
|
|
2205
2472
|
${colors.green('? <query>')} Search Recker documentation.
|
|
2206
2473
|
${colors.green('search <query>')} Alias for ? (hybrid fuzzy+semantic search).
|
|
@@ -2218,6 +2485,7 @@ ${colors.bold('Network:')}
|
|
|
2218
2485
|
› post /post name="Neo" active:=true role:Admin
|
|
2219
2486
|
› load /heavy-endpoint users=100 mode=stress
|
|
2220
2487
|
› chat openai gpt-5.1
|
|
2488
|
+
› spider https://example.com --depth=2 --limit=50
|
|
2221
2489
|
`);
|
|
2222
2490
|
}
|
|
2223
2491
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
|
|
|
42
42
|
export * from './plugins/xml.js';
|
|
43
43
|
export * from './plugins/scrape.js';
|
|
44
44
|
export * from './scrape/index.js';
|
|
45
|
+
export * from './seo/index.js';
|
|
45
46
|
export * from './plugins/server-timing.js';
|
|
46
47
|
export * from './plugins/auth.js';
|
|
47
48
|
export * from './plugins/proxy-rotator.js';
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
|
|
|
42
42
|
export * from './plugins/xml.js';
|
|
43
43
|
export * from './plugins/scrape.js';
|
|
44
44
|
export * from './scrape/index.js';
|
|
45
|
+
export * from './seo/index.js';
|
|
45
46
|
export * from './plugins/server-timing.js';
|
|
46
47
|
export * from './plugins/auth.js';
|
|
47
48
|
export * from './plugins/proxy-rotator.js';
|
|
@@ -76,6 +76,7 @@ export function extractImages($, options) {
|
|
|
76
76
|
height: height ? parseInt(height, 10) : undefined,
|
|
77
77
|
srcset: $el.attr('srcset'),
|
|
78
78
|
loading: $el.attr('loading'),
|
|
79
|
+
decoding: $el.attr('decoding'),
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
return images;
|
|
@@ -117,7 +118,7 @@ export function extractMeta($) {
|
|
|
117
118
|
meta.author = content;
|
|
118
119
|
break;
|
|
119
120
|
case 'robots':
|
|
120
|
-
meta.robots = content;
|
|
121
|
+
meta.robots = content.split(',').map((r) => r.trim().toLowerCase());
|
|
121
122
|
break;
|
|
122
123
|
case 'viewport':
|
|
123
124
|
meta.viewport = content;
|
package/dist/scrape/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { ScrapeDocument } from './document.js';
|
|
2
2
|
export { ScrapeElement } from './element.js';
|
|
3
|
+
export { Spider, spider } from './spider.js';
|
|
4
|
+
export type { SpiderOptions, SpiderPageResult, SpiderProgress, SpiderResult, } from './spider.js';
|
|
3
5
|
export { extractLinks, extractImages, extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractForms, extractTables, extractScripts, extractStyles, } from './extractors.js';
|
|
4
6
|
export type { ExtractedLink, ExtractedImage, ExtractedMeta, OpenGraphData, TwitterCardData, JsonLdData, ExtractedForm, ExtractedFormField, ExtractedTable, ExtractedScript, ExtractedStyle, ExtractionSchema, ExtractionSchemaField, ScrapeOptions, LinkExtractionOptions, ImageExtractionOptions, } from './types.js';
|
package/dist/scrape/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ScrapeDocument } from './document.js';
|
|
2
2
|
export { ScrapeElement } from './element.js';
|
|
3
|
+
export { Spider, spider } from './spider.js';
|
|
3
4
|
export { extractLinks, extractImages, extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractForms, extractTables, extractScripts, extractStyles, } from './extractors.js';
|