recker 1.0.27-next.8396df6 → 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.
Files changed (41) hide show
  1. package/dist/browser/scrape/extractors.js +2 -1
  2. package/dist/browser/scrape/types.d.ts +2 -1
  3. package/dist/cli/index.js +47 -3
  4. package/dist/cli/tui/shell.d.ts +1 -0
  5. package/dist/cli/tui/shell.js +157 -0
  6. package/dist/scrape/extractors.js +2 -1
  7. package/dist/scrape/types.d.ts +2 -1
  8. package/dist/seo/analyzer.d.ts +32 -10
  9. package/dist/seo/analyzer.js +557 -386
  10. package/dist/seo/index.d.ts +4 -1
  11. package/dist/seo/index.js +1 -0
  12. package/dist/seo/rules/accessibility.d.ts +2 -0
  13. package/dist/seo/rules/accessibility.js +128 -0
  14. package/dist/seo/rules/content.d.ts +2 -0
  15. package/dist/seo/rules/content.js +236 -0
  16. package/dist/seo/rules/images.d.ts +2 -0
  17. package/dist/seo/rules/images.js +180 -0
  18. package/dist/seo/rules/index.d.ts +20 -0
  19. package/dist/seo/rules/index.js +72 -0
  20. package/dist/seo/rules/links.d.ts +2 -0
  21. package/dist/seo/rules/links.js +150 -0
  22. package/dist/seo/rules/meta.d.ts +2 -0
  23. package/dist/seo/rules/meta.js +523 -0
  24. package/dist/seo/rules/mobile.d.ts +2 -0
  25. package/dist/seo/rules/mobile.js +71 -0
  26. package/dist/seo/rules/performance.d.ts +2 -0
  27. package/dist/seo/rules/performance.js +246 -0
  28. package/dist/seo/rules/schema.d.ts +2 -0
  29. package/dist/seo/rules/schema.js +54 -0
  30. package/dist/seo/rules/security.d.ts +2 -0
  31. package/dist/seo/rules/security.js +147 -0
  32. package/dist/seo/rules/structural.d.ts +2 -0
  33. package/dist/seo/rules/structural.js +155 -0
  34. package/dist/seo/rules/technical.d.ts +2 -0
  35. package/dist/seo/rules/technical.js +223 -0
  36. package/dist/seo/rules/thresholds.d.ts +196 -0
  37. package/dist/seo/rules/thresholds.js +118 -0
  38. package/dist/seo/rules/types.d.ts +191 -0
  39. package/dist/seo/rules/types.js +11 -0
  40. package/dist/seo/types.d.ts +61 -1
  41. 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
@@ -393,10 +393,13 @@ ${colors.bold('Details:')}`);
393
393
  .description('Analyze page SEO (title, meta, headings, links, images, structured data)')
394
394
  .argument('<url>', 'URL to analyze')
395
395
  .option('-a, --all', 'Show all checks including passed ones')
396
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
396
397
  .addHelpText('after', `
397
398
  ${colors.bold(colors.yellow('Examples:'))}
398
- ${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
399
- ${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
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')}
400
403
 
401
404
  ${colors.bold(colors.yellow('Checks:'))}
402
405
  ${colors.cyan('Title Tag')} Length and presence
@@ -412,14 +415,45 @@ ${colors.bold(colors.yellow('Checks:'))}
412
415
  .action(async (url, options) => {
413
416
  if (!url.startsWith('http'))
414
417
  url = `https://${url}`;
418
+ const isJson = options.format === 'json';
415
419
  const { createClient } = await import('../core/client.js');
416
420
  const { analyzeSeo } = await import('../seo/analyzer.js');
417
- console.log(colors.gray(`Analyzing SEO for ${url}...`));
421
+ if (!isJson) {
422
+ console.log(colors.gray(`Analyzing SEO for ${url}...`));
423
+ }
418
424
  try {
419
425
  const client = createClient({ timeout: 30000 });
420
426
  const res = await client.get(url);
421
427
  const html = await res.text();
422
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
+ }
423
457
  let gradeColor = colors.red;
424
458
  if (report.grade === 'A')
425
459
  gradeColor = colors.green;
@@ -463,6 +497,16 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
463
497
  if (check.recommendation && check.status !== 'pass') {
464
498
  console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
465
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
+ }
466
510
  }
467
511
  }
468
512
  console.log('');
@@ -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;
@@ -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.
@@ -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;
@@ -1,20 +1,42 @@
1
1
  import type { CheerioAPI } from 'cheerio';
2
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
+ }
3
7
  export declare class SeoAnalyzer {
4
8
  private $;
5
9
  private options;
6
- constructor($: CheerioAPI, options?: SeoAnalyzerOptions);
7
- static fromHtml(html: string, options?: SeoAnalyzerOptions): Promise<SeoAnalyzer>;
10
+ private rulesEngine;
11
+ constructor($: CheerioAPI, options?: SeoAnalyzerFullOptions);
12
+ static fromHtml(html: string, options?: SeoAnalyzerFullOptions): Promise<SeoAnalyzer>;
8
13
  analyze(): SeoReport;
9
- private analyzeTitle;
10
- private analyzeDescription;
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;
11
29
  private analyzeHeadings;
12
30
  private analyzeContent;
13
- private analyzeLinks;
14
- private analyzeImages;
15
- private analyzeSocialMeta;
16
- private analyzeTechnical;
17
- private analyzeJsonLd;
31
+ private buildLinkAnalysis;
32
+ private buildImageAnalysis;
33
+ private buildSocialAnalysis;
34
+ private buildTechnicalAnalysis;
18
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[];
19
39
  }
20
- export declare function analyzeSeo(html: string, options?: SeoAnalyzerOptions): Promise<SeoReport>;
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';