recker 1.0.27 → 1.0.28-next.32fe8ef
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 +1 -0
- package/dist/cli/tui/shell.js +157 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scrape/extractors.js +2 -1
- package/dist/scrape/types.d.ts +2 -1
- package/dist/seo/analyzer.d.ts +42 -0
- package/dist/seo/analyzer.js +715 -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 +128 -0
- package/dist/seo/rules/content.d.ts +2 -0
- package/dist/seo/rules/content.js +236 -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 +20 -0
- package/dist/seo/rules/index.js +72 -0
- package/dist/seo/rules/links.d.ts +2 -0
- package/dist/seo/rules/links.js +150 -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/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 +147 -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 +191 -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
package/dist/cli/tui/shell.js
CHANGED
|
@@ -14,6 +14,7 @@ import colors from '../../utils/colors.js';
|
|
|
14
14
|
import { getShellSearch } from './shell-search.js';
|
|
15
15
|
import { openSearchPanel } from './search-panel.js';
|
|
16
16
|
import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
|
|
17
|
+
import { analyzeSeo } from '../../seo/index.js';
|
|
17
18
|
let highlight;
|
|
18
19
|
async function initDependencies() {
|
|
19
20
|
if (!highlight) {
|
|
@@ -343,6 +344,9 @@ export class RekShell {
|
|
|
343
344
|
case 'security':
|
|
344
345
|
await this.runSecurityGrader(parts[1]);
|
|
345
346
|
return;
|
|
347
|
+
case 'seo':
|
|
348
|
+
await this.runSeo(parts[1], parts.includes('-a') || parts.includes('--all'), parts.includes('--format') && parts[parts.indexOf('--format') + 1] === 'json');
|
|
349
|
+
return;
|
|
346
350
|
case 'ip':
|
|
347
351
|
await this.runIpIntelligence(parts[1]);
|
|
348
352
|
return;
|
|
@@ -944,6 +948,156 @@ ${colors.bold('Details:')}`);
|
|
|
944
948
|
}
|
|
945
949
|
console.log('');
|
|
946
950
|
}
|
|
951
|
+
async runSeo(url, showAll = false, jsonOutput = false) {
|
|
952
|
+
if (!url) {
|
|
953
|
+
url = this.currentDocUrl || this.baseUrl || '';
|
|
954
|
+
if (!url) {
|
|
955
|
+
console.log(colors.yellow('Usage: seo <url> [-a] [--format json]'));
|
|
956
|
+
console.log(colors.gray(' Examples: seo google.com | seo https://example.com -a'));
|
|
957
|
+
console.log(colors.gray(' -a, --all Show all checks (including passed)'));
|
|
958
|
+
console.log(colors.gray(' --format json Output raw JSON for programmatic use'));
|
|
959
|
+
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if (!url.startsWith('http') && !url.startsWith('-')) {
|
|
964
|
+
url = `https://${url}`;
|
|
965
|
+
}
|
|
966
|
+
if (!jsonOutput) {
|
|
967
|
+
console.log(colors.gray(`Analyzing SEO for ${url}...`));
|
|
968
|
+
}
|
|
969
|
+
const startTime = performance.now();
|
|
970
|
+
try {
|
|
971
|
+
const res = await this.client.get(url);
|
|
972
|
+
const html = await res.text();
|
|
973
|
+
const duration = Math.round(performance.now() - startTime);
|
|
974
|
+
const report = await analyzeSeo(html, { baseUrl: url });
|
|
975
|
+
if (jsonOutput) {
|
|
976
|
+
const jsonResult = {
|
|
977
|
+
url,
|
|
978
|
+
analyzedAt: new Date().toISOString(),
|
|
979
|
+
durationMs: duration,
|
|
980
|
+
score: report.score,
|
|
981
|
+
grade: report.grade,
|
|
982
|
+
title: report.title,
|
|
983
|
+
metaDescription: report.metaDescription,
|
|
984
|
+
content: report.content,
|
|
985
|
+
headings: report.headings,
|
|
986
|
+
links: report.links,
|
|
987
|
+
images: report.images,
|
|
988
|
+
openGraph: report.social.openGraph,
|
|
989
|
+
twitterCard: report.social.twitterCard,
|
|
990
|
+
jsonLd: report.jsonLd,
|
|
991
|
+
technical: report.technical,
|
|
992
|
+
checks: report.checks,
|
|
993
|
+
summary: {
|
|
994
|
+
total: report.checks.length,
|
|
995
|
+
passed: report.checks.filter(c => c.status === 'pass').length,
|
|
996
|
+
warnings: report.checks.filter(c => c.status === 'warn').length,
|
|
997
|
+
errors: report.checks.filter(c => c.status === 'fail').length,
|
|
998
|
+
info: report.checks.filter(c => c.status === 'info').length,
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
1002
|
+
this.lastResponse = jsonResult;
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
let gradeColor = colors.red;
|
|
1006
|
+
if (report.grade === 'A')
|
|
1007
|
+
gradeColor = colors.green;
|
|
1008
|
+
else if (report.grade === 'B')
|
|
1009
|
+
gradeColor = colors.blue;
|
|
1010
|
+
else if (report.grade === 'C')
|
|
1011
|
+
gradeColor = colors.yellow;
|
|
1012
|
+
else if (report.grade === 'D')
|
|
1013
|
+
gradeColor = colors.magenta;
|
|
1014
|
+
console.log(`
|
|
1015
|
+
${colors.bold(colors.cyan('🔍 SEO Analysis Report'))} ${colors.gray(`(${duration}ms)`)}
|
|
1016
|
+
Grade: ${gradeColor(colors.bold(report.grade))} (${report.score}/100)
|
|
1017
|
+
`);
|
|
1018
|
+
if (report.title) {
|
|
1019
|
+
console.log(colors.bold('Title:') + ` ${report.title.text} ` + colors.gray(`(${report.title.length} chars)`));
|
|
1020
|
+
}
|
|
1021
|
+
if (report.metaDescription) {
|
|
1022
|
+
const desc = report.metaDescription.text.length > 80
|
|
1023
|
+
? report.metaDescription.text.slice(0, 77) + '...'
|
|
1024
|
+
: report.metaDescription.text;
|
|
1025
|
+
console.log(colors.bold('Description:') + ` ${desc} ` + colors.gray(`(${report.metaDescription.length} chars)`));
|
|
1026
|
+
}
|
|
1027
|
+
if (report.content) {
|
|
1028
|
+
console.log(colors.bold('Content:') + ` ${report.content.wordCount} words, ${report.content.paragraphCount} paragraphs, ~${report.content.readingTimeMinutes} min read`);
|
|
1029
|
+
}
|
|
1030
|
+
console.log('');
|
|
1031
|
+
console.log(colors.bold('Checks:'));
|
|
1032
|
+
const checksToShow = showAll
|
|
1033
|
+
? report.checks
|
|
1034
|
+
: report.checks.filter(c => c.status !== 'pass');
|
|
1035
|
+
const failed = checksToShow.filter(c => c.status === 'fail');
|
|
1036
|
+
const warnings = checksToShow.filter(c => c.status === 'warn');
|
|
1037
|
+
const info = checksToShow.filter(c => c.status === 'info');
|
|
1038
|
+
const passed = showAll ? checksToShow.filter(c => c.status === 'pass') : [];
|
|
1039
|
+
const displayCheck = (check) => {
|
|
1040
|
+
let icon;
|
|
1041
|
+
let nameColor;
|
|
1042
|
+
switch (check.status) {
|
|
1043
|
+
case 'pass':
|
|
1044
|
+
icon = colors.green('✔');
|
|
1045
|
+
nameColor = colors.green;
|
|
1046
|
+
break;
|
|
1047
|
+
case 'warn':
|
|
1048
|
+
icon = colors.yellow('⚠');
|
|
1049
|
+
nameColor = colors.yellow;
|
|
1050
|
+
break;
|
|
1051
|
+
case 'fail':
|
|
1052
|
+
icon = colors.red('✖');
|
|
1053
|
+
nameColor = colors.red;
|
|
1054
|
+
break;
|
|
1055
|
+
default:
|
|
1056
|
+
icon = colors.blue('ℹ');
|
|
1057
|
+
nameColor = colors.blue;
|
|
1058
|
+
}
|
|
1059
|
+
console.log(` ${icon} ${nameColor(check.name.padEnd(22))} ${check.message}`);
|
|
1060
|
+
if (check.recommendation && check.status !== 'pass') {
|
|
1061
|
+
console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
|
|
1062
|
+
}
|
|
1063
|
+
const evidence = check.evidence;
|
|
1064
|
+
if (evidence && check.status !== 'pass') {
|
|
1065
|
+
if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
|
|
1066
|
+
const items = evidence.found.slice(0, 3);
|
|
1067
|
+
console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
|
|
1068
|
+
}
|
|
1069
|
+
if (evidence.example) {
|
|
1070
|
+
console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
if (failed.length > 0) {
|
|
1075
|
+
console.log(colors.red(`\n Errors (${failed.length}):`));
|
|
1076
|
+
failed.forEach(displayCheck);
|
|
1077
|
+
}
|
|
1078
|
+
if (warnings.length > 0) {
|
|
1079
|
+
console.log(colors.yellow(`\n Warnings (${warnings.length}):`));
|
|
1080
|
+
warnings.forEach(displayCheck);
|
|
1081
|
+
}
|
|
1082
|
+
if (info.length > 0) {
|
|
1083
|
+
console.log(colors.blue(`\n Info (${info.length}):`));
|
|
1084
|
+
info.forEach(displayCheck);
|
|
1085
|
+
}
|
|
1086
|
+
if (passed.length > 0) {
|
|
1087
|
+
console.log(colors.green(`\n Passed (${passed.length}):`));
|
|
1088
|
+
passed.forEach(displayCheck);
|
|
1089
|
+
}
|
|
1090
|
+
if (!showAll && report.checks.filter(c => c.status === 'pass').length > 0) {
|
|
1091
|
+
console.log(colors.gray(`\n ${report.checks.filter(c => c.status === 'pass').length} checks passed. Use -a to show all.`));
|
|
1092
|
+
}
|
|
1093
|
+
console.log('');
|
|
1094
|
+
this.lastResponse = report;
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
console.error(colors.red(`SEO analysis failed: ${error.message}`));
|
|
1098
|
+
}
|
|
1099
|
+
console.log('');
|
|
1100
|
+
}
|
|
947
1101
|
async runIpIntelligence(address) {
|
|
948
1102
|
if (!address) {
|
|
949
1103
|
console.log(colors.yellow('Usage: ip <address>'));
|
|
@@ -2182,6 +2336,9 @@ ${colors.bold('Network:')}
|
|
|
2182
2336
|
${colors.green('dns <domain>')} Full DNS lookup (A, AAAA, MX, NS, SPF, DMARC).
|
|
2183
2337
|
${colors.green('rdap <domain>')} RDAP lookup (modern WHOIS).
|
|
2184
2338
|
${colors.green('ping <host>')} Quick TCP connectivity check.
|
|
2339
|
+
${colors.green('seo <url> [-a] [--format json]')} SEO analysis (70+ rules).
|
|
2340
|
+
${colors.gray('-a, --all Show all checks including passed')}
|
|
2341
|
+
${colors.gray('--format json Output raw JSON for programmatic use')}
|
|
2185
2342
|
|
|
2186
2343
|
${colors.bold('Web Scraping:')}
|
|
2187
2344
|
${colors.green('scrap <url>')} Fetch and parse HTML document.
|
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/types.d.ts
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CheerioAPI } from 'cheerio';
|
|
2
|
+
import type { SeoReport, SeoAnalyzerOptions } from './types.js';
|
|
3
|
+
import { type RulesEngineOptions } from './rules/index.js';
|
|
4
|
+
export interface SeoAnalyzerFullOptions extends SeoAnalyzerOptions {
|
|
5
|
+
rules?: RulesEngineOptions;
|
|
6
|
+
}
|
|
7
|
+
export declare class SeoAnalyzer {
|
|
8
|
+
private $;
|
|
9
|
+
private options;
|
|
10
|
+
private rulesEngine;
|
|
11
|
+
constructor($: CheerioAPI, options?: SeoAnalyzerFullOptions);
|
|
12
|
+
static fromHtml(html: string, options?: SeoAnalyzerFullOptions): Promise<SeoAnalyzer>;
|
|
13
|
+
analyze(): SeoReport;
|
|
14
|
+
private buildRuleContext;
|
|
15
|
+
private analyzeUrlQuality;
|
|
16
|
+
private analyzeJsRendering;
|
|
17
|
+
private checkMixedContent;
|
|
18
|
+
private analyzeLinkSecurity;
|
|
19
|
+
private detectFavicon;
|
|
20
|
+
private analyzePerformanceHints;
|
|
21
|
+
private analyzeCWVHints;
|
|
22
|
+
private analyzeAccessibility;
|
|
23
|
+
private analyzeStructuralHtml;
|
|
24
|
+
private analyzeBreadcrumbs;
|
|
25
|
+
private analyzeMultimedia;
|
|
26
|
+
private analyzeTrustSignals;
|
|
27
|
+
private calculateTextHtmlRatio;
|
|
28
|
+
private convertToCheckResults;
|
|
29
|
+
private analyzeHeadings;
|
|
30
|
+
private analyzeContent;
|
|
31
|
+
private buildLinkAnalysis;
|
|
32
|
+
private buildImageAnalysis;
|
|
33
|
+
private buildSocialAnalysis;
|
|
34
|
+
private buildTechnicalAnalysis;
|
|
35
|
+
private calculateScore;
|
|
36
|
+
getRules(): import("./rules/types.js").SeoRule[];
|
|
37
|
+
getRulesByCategory(category: string): import("./rules/types.js").SeoRule[];
|
|
38
|
+
getCategories(): import("./rules/types.js").RuleCategory[];
|
|
39
|
+
}
|
|
40
|
+
export declare function analyzeSeo(html: string, options?: SeoAnalyzerFullOptions): Promise<SeoReport>;
|
|
41
|
+
export { SEO_THRESHOLDS, createRulesEngine, SeoRulesEngine } from './rules/index.js';
|
|
42
|
+
export type { RuleContext, RuleResult, RulesEngineOptions, RuleCategory, RuleSeverity, SeoRule } from './rules/index.js';
|