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.
Files changed (46) 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 +142 -3
  4. package/dist/cli/tui/shell.d.ts +1 -0
  5. package/dist/cli/tui/shell.js +157 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/scrape/extractors.js +2 -1
  9. package/dist/scrape/types.d.ts +2 -1
  10. package/dist/seo/analyzer.d.ts +42 -0
  11. package/dist/seo/analyzer.js +715 -0
  12. package/dist/seo/index.d.ts +5 -0
  13. package/dist/seo/index.js +2 -0
  14. package/dist/seo/rules/accessibility.d.ts +2 -0
  15. package/dist/seo/rules/accessibility.js +128 -0
  16. package/dist/seo/rules/content.d.ts +2 -0
  17. package/dist/seo/rules/content.js +236 -0
  18. package/dist/seo/rules/images.d.ts +2 -0
  19. package/dist/seo/rules/images.js +180 -0
  20. package/dist/seo/rules/index.d.ts +20 -0
  21. package/dist/seo/rules/index.js +72 -0
  22. package/dist/seo/rules/links.d.ts +2 -0
  23. package/dist/seo/rules/links.js +150 -0
  24. package/dist/seo/rules/meta.d.ts +2 -0
  25. package/dist/seo/rules/meta.js +523 -0
  26. package/dist/seo/rules/mobile.d.ts +2 -0
  27. package/dist/seo/rules/mobile.js +71 -0
  28. package/dist/seo/rules/performance.d.ts +2 -0
  29. package/dist/seo/rules/performance.js +246 -0
  30. package/dist/seo/rules/schema.d.ts +2 -0
  31. package/dist/seo/rules/schema.js +54 -0
  32. package/dist/seo/rules/security.d.ts +2 -0
  33. package/dist/seo/rules/security.js +147 -0
  34. package/dist/seo/rules/structural.d.ts +2 -0
  35. package/dist/seo/rules/structural.js +155 -0
  36. package/dist/seo/rules/technical.d.ts +2 -0
  37. package/dist/seo/rules/technical.js +223 -0
  38. package/dist/seo/rules/thresholds.d.ts +196 -0
  39. package/dist/seo/rules/thresholds.js +118 -0
  40. package/dist/seo/rules/types.d.ts +191 -0
  41. package/dist/seo/rules/types.js +11 -0
  42. package/dist/seo/types.d.ts +160 -0
  43. package/dist/seo/types.js +1 -0
  44. package/dist/utils/columns.d.ts +14 -0
  45. package/dist/utils/columns.js +69 -0
  46. 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 PRESET_NAMES = Object.keys(presets).filter(k => k !== 'registry' && !k.startsWith('_'));
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
- ${colors.cyan(PRESET_NAMES.map(p => '@' + p).join(', '))}
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')
@@ -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.
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;
@@ -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';