recker 1.0.27 → 1.0.28-next.c61382b

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 (58) 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 +727 -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/crawl.d.ts +2 -0
  19. package/dist/seo/rules/crawl.js +307 -0
  20. package/dist/seo/rules/cwv.d.ts +2 -0
  21. package/dist/seo/rules/cwv.js +337 -0
  22. package/dist/seo/rules/ecommerce.d.ts +2 -0
  23. package/dist/seo/rules/ecommerce.js +252 -0
  24. package/dist/seo/rules/i18n.d.ts +2 -0
  25. package/dist/seo/rules/i18n.js +222 -0
  26. package/dist/seo/rules/images.d.ts +2 -0
  27. package/dist/seo/rules/images.js +180 -0
  28. package/dist/seo/rules/index.d.ts +52 -0
  29. package/dist/seo/rules/index.js +135 -0
  30. package/dist/seo/rules/links.d.ts +2 -0
  31. package/dist/seo/rules/links.js +150 -0
  32. package/dist/seo/rules/local.d.ts +2 -0
  33. package/dist/seo/rules/local.js +265 -0
  34. package/dist/seo/rules/meta.d.ts +2 -0
  35. package/dist/seo/rules/meta.js +523 -0
  36. package/dist/seo/rules/mobile.d.ts +2 -0
  37. package/dist/seo/rules/mobile.js +71 -0
  38. package/dist/seo/rules/performance.d.ts +2 -0
  39. package/dist/seo/rules/performance.js +246 -0
  40. package/dist/seo/rules/readability.d.ts +2 -0
  41. package/dist/seo/rules/readability.js +255 -0
  42. package/dist/seo/rules/schema.d.ts +2 -0
  43. package/dist/seo/rules/schema.js +54 -0
  44. package/dist/seo/rules/security.d.ts +2 -0
  45. package/dist/seo/rules/security.js +147 -0
  46. package/dist/seo/rules/structural.d.ts +2 -0
  47. package/dist/seo/rules/structural.js +155 -0
  48. package/dist/seo/rules/technical.d.ts +2 -0
  49. package/dist/seo/rules/technical.js +223 -0
  50. package/dist/seo/rules/thresholds.d.ts +196 -0
  51. package/dist/seo/rules/thresholds.js +118 -0
  52. package/dist/seo/rules/types.d.ts +286 -0
  53. package/dist/seo/rules/types.js +11 -0
  54. package/dist/seo/types.d.ts +160 -0
  55. package/dist/seo/types.js +1 -0
  56. package/dist/utils/columns.d.ts +14 -0
  57. package/dist/utils/columns.js +69 -0
  58. package/package.json +1 -1
@@ -0,0 +1,147 @@
1
+ import { createResult } from './types.js';
2
+ export const securityRules = [
3
+ {
4
+ id: 'https-required',
5
+ name: 'HTTPS',
6
+ category: 'security',
7
+ severity: 'error',
8
+ description: 'Page must be served over HTTPS',
9
+ check: (ctx) => {
10
+ if (ctx.isHttps === false) {
11
+ return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'fail', 'Page is not served over HTTPS', { recommendation: 'Enable HTTPS for all pages' });
12
+ }
13
+ if (ctx.isHttps === true) {
14
+ return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'pass', 'Page is served over HTTPS');
15
+ }
16
+ return null;
17
+ },
18
+ },
19
+ {
20
+ id: 'mixed-content',
21
+ name: 'Mixed Content',
22
+ category: 'security',
23
+ severity: 'error',
24
+ description: 'HTTPS pages should not load HTTP resources',
25
+ check: (ctx) => {
26
+ if (ctx.hasMixedContent) {
27
+ return createResult({ id: 'mixed-content', name: 'Mixed Content', category: 'security', severity: 'error' }, 'fail', 'Page has mixed content (HTTP resources on HTTPS page)', { recommendation: 'Update all resources to use HTTPS' });
28
+ }
29
+ return null;
30
+ },
31
+ },
32
+ {
33
+ id: 'security-csp-exists',
34
+ name: 'Content Security Policy (CSP)',
35
+ category: 'security',
36
+ severity: 'warning',
37
+ description: 'Content Security Policy header should be present to mitigate XSS attacks.',
38
+ check: (ctx) => {
39
+ if (!ctx.responseHeaders)
40
+ return null;
41
+ const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
42
+ if (!cspHeader) {
43
+ return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'warn', 'Content-Security-Policy header is missing', { recommendation: 'Implement a strong Content-Security-Policy to prevent XSS attacks.' });
44
+ }
45
+ return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'pass', 'Content-Security-Policy header is present');
46
+ },
47
+ },
48
+ {
49
+ id: 'security-xfo-exists',
50
+ name: 'X-Frame-Options',
51
+ category: 'security',
52
+ severity: 'warning',
53
+ description: 'X-Frame-Options header should be present to prevent clickjacking.',
54
+ check: (ctx) => {
55
+ if (!ctx.responseHeaders)
56
+ return null;
57
+ const xfoHeader = ctx.responseHeaders['x-frame-options'] || ctx.responseHeaders['X-Frame-Options'];
58
+ if (!xfoHeader) {
59
+ return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Frame-Options header is missing', { recommendation: 'Implement X-Frame-Options to prevent clickjacking attacks.' });
60
+ }
61
+ return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'pass', `X-Frame-Options header is present: ${xfoHeader}`);
62
+ },
63
+ },
64
+ {
65
+ id: 'security-cors-config',
66
+ name: 'CORS Configuration',
67
+ category: 'security',
68
+ severity: 'warning',
69
+ description: 'Review Access-Control-Allow-Origin header for proper CORS configuration.',
70
+ check: (ctx) => {
71
+ if (!ctx.responseHeaders)
72
+ return null;
73
+ const acaoHeader = ctx.responseHeaders['access-control-allow-origin'] || ctx.responseHeaders['Access-Control-Allow-Origin'];
74
+ if (acaoHeader === '*') {
75
+ return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'warn', 'Access-Control-Allow-Origin is set to "*"', { recommendation: 'Avoid wildcard (*) in Access-Control-Allow-Origin for sensitive content. Specify allowed origins.' });
76
+ }
77
+ if (!acaoHeader) {
78
+ return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'info', 'Access-Control-Allow-Origin header is missing', { recommendation: 'Consider explicit CORS configuration if resources are consumed cross-origin.' });
79
+ }
80
+ return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'pass', `Access-Control-Allow-Origin: ${acaoHeader}`);
81
+ },
82
+ },
83
+ {
84
+ id: 'security-hsts-exists',
85
+ name: 'Strict-Transport-Security (HSTS)',
86
+ category: 'security',
87
+ severity: 'warning',
88
+ description: 'HSTS header forces secure connections and improves SEO indirectly.',
89
+ check: (ctx) => {
90
+ if (!ctx.responseHeaders)
91
+ return null;
92
+ const hstsHeader = ctx.responseHeaders['strict-transport-security'] || ctx.responseHeaders['Strict-Transport-Security'];
93
+ if (!hstsHeader) {
94
+ return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'warn', 'Strict-Transport-Security header is missing', { recommendation: 'Implement HSTS to force secure connections and benefit SEO.' });
95
+ }
96
+ return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'pass', `Strict-Transport-Security header is present: ${hstsHeader}`);
97
+ },
98
+ },
99
+ {
100
+ id: 'security-xcto-exists',
101
+ name: 'X-Content-Type-Options',
102
+ category: 'security',
103
+ severity: 'warning',
104
+ description: 'X-Content-Type-Options header prevents MIME sniffing attacks.',
105
+ check: (ctx) => {
106
+ if (!ctx.responseHeaders)
107
+ return null;
108
+ const xctoHeader = ctx.responseHeaders['x-content-type-options'] || ctx.responseHeaders['X-Content-Type-Options'];
109
+ if (!xctoHeader) {
110
+ return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Content-Type-Options header is missing', { recommendation: 'Implement X-Content-Type-Options: nosniff to prevent MIME sniffing.' });
111
+ }
112
+ return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'pass', `X-Content-Type-Options header is present: ${xctoHeader}`);
113
+ },
114
+ },
115
+ {
116
+ id: 'security-rp-exists',
117
+ name: 'Referrer-Policy',
118
+ category: 'security',
119
+ severity: 'info',
120
+ description: 'Referrer-Policy controls how much referrer information is sent with requests.',
121
+ check: (ctx) => {
122
+ if (!ctx.responseHeaders)
123
+ return null;
124
+ const rpHeader = ctx.responseHeaders['referrer-policy'] || ctx.responseHeaders['Referrer-Policy'];
125
+ if (!rpHeader) {
126
+ return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'info', 'Referrer-Policy header is missing', { recommendation: 'Consider implementing a Referrer-Policy for better privacy and control (e.g., strict-origin-when-cross-origin).' });
127
+ }
128
+ return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'pass', `Referrer-Policy header is present: ${rpHeader}`);
129
+ },
130
+ },
131
+ {
132
+ id: 'security-pp-exists',
133
+ name: 'Permissions-Policy',
134
+ category: 'security',
135
+ severity: 'info',
136
+ description: 'Permissions-Policy controls browser features available to the page and iframes.',
137
+ check: (ctx) => {
138
+ if (!ctx.responseHeaders)
139
+ return null;
140
+ const ppHeader = ctx.responseHeaders['permissions-policy'] || ctx.responseHeaders['Permissions-Policy'];
141
+ if (!ppHeader) {
142
+ return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'info', 'Permissions-Policy header is missing', { recommendation: 'Consider implementing a Permissions-Policy to disable unused browser features and enhance security (e.g., camera=(), microphone=()).' });
143
+ }
144
+ return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'pass', `Permissions-Policy header is present: ${ppHeader}`);
145
+ },
146
+ },
147
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const structuralRules: SeoRule[];
@@ -0,0 +1,155 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const structuralRules = [
4
+ {
5
+ id: 'h1-exists',
6
+ name: 'H1 Exists',
7
+ category: 'headings',
8
+ severity: 'error',
9
+ description: 'Page must have exactly one H1 heading',
10
+ check: (ctx) => {
11
+ if (ctx.h1Count === undefined)
12
+ return null;
13
+ if (ctx.h1Count === 0) {
14
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'fail', 'Missing H1 heading', { recommendation: 'Add a single H1 heading that describes the page content' });
15
+ }
16
+ if (ctx.h1Count > 1) {
17
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'warn', `Multiple H1 tags (${ctx.h1Count} found)`, { value: ctx.h1Count, recommendation: 'Use only one H1 per page' });
18
+ }
19
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'pass', 'Single H1 tag present');
20
+ },
21
+ },
22
+ {
23
+ id: 'h1-length',
24
+ name: 'H1 Length',
25
+ category: 'headings',
26
+ severity: 'warning',
27
+ description: 'H1 should be 20-70 characters',
28
+ check: (ctx) => {
29
+ if (!ctx.h1Text)
30
+ return null;
31
+ const len = ctx.h1Length ?? ctx.h1Text.length;
32
+ const { minLength, maxLength } = SEO_THRESHOLDS.headings.h1;
33
+ if (len < minLength) {
34
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too short (${len} chars, min: ${minLength})`, { value: len, recommendation: `Expand H1 to at least ${minLength} characters` });
35
+ }
36
+ if (len > maxLength) {
37
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too long (${len} chars, max: ${maxLength})`, { value: len, recommendation: `Shorten H1 to under ${maxLength} characters` });
38
+ }
39
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'pass', `H1 length OK (${len} chars)`, { value: len });
40
+ },
41
+ },
42
+ {
43
+ id: 'heading-hierarchy',
44
+ name: 'Heading Hierarchy',
45
+ category: 'headings',
46
+ severity: 'warning',
47
+ description: 'Headings should follow sequential order (H1 → H2 → H3)',
48
+ check: (ctx) => {
49
+ if (ctx.headingHierarchyValid === undefined)
50
+ return null;
51
+ if (!ctx.headingHierarchyValid) {
52
+ const skipped = ctx.headingSkippedLevels?.join(', ') || 'unknown';
53
+ return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'warn', `Heading levels are skipped (${skipped})`, { recommendation: 'Use sequential heading levels (H1 → H2 → H3)' });
54
+ }
55
+ return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'pass', 'Heading structure is correct');
56
+ },
57
+ },
58
+ {
59
+ id: 'h2-count',
60
+ name: 'H2 Count',
61
+ category: 'headings',
62
+ severity: 'info',
63
+ description: 'Page should have 2-8 H2 headings for good structure',
64
+ check: (ctx) => {
65
+ if (ctx.h2Count === undefined)
66
+ return null;
67
+ const { min, max } = SEO_THRESHOLDS.headings.h2;
68
+ if (ctx.h2Count < min) {
69
+ return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', `Few H2 headings (${ctx.h2Count})`, { value: ctx.h2Count, recommendation: `Consider adding more H2 headings for better structure (${min}-${max} ideal)` });
70
+ }
71
+ if (ctx.h2Count > max) {
72
+ return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', `Many H2 headings (${ctx.h2Count})`, { value: ctx.h2Count, recommendation: 'Consider consolidating sections' });
73
+ }
74
+ return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'pass', `Good H2 count (${ctx.h2Count})`, { value: ctx.h2Count });
75
+ },
76
+ },
77
+ {
78
+ id: 'html5-header-exists',
79
+ name: 'HTML5 Header',
80
+ category: 'technical',
81
+ severity: 'info',
82
+ description: 'Page should use a <header> element',
83
+ check: (ctx) => {
84
+ if (!ctx.hasHeader) {
85
+ return createResult({ id: 'html5-header-exists', name: 'HTML5 Header', category: 'technical', severity: 'info' }, 'info', 'Missing <header> element', { recommendation: 'Use <header> for site-wide or page-specific introductory content' });
86
+ }
87
+ return null;
88
+ },
89
+ },
90
+ {
91
+ id: 'html5-nav-exists',
92
+ name: 'HTML5 Navigation',
93
+ category: 'technical',
94
+ severity: 'info',
95
+ description: 'Page should use a <nav> element for navigation links',
96
+ check: (ctx) => {
97
+ if (!ctx.hasNav) {
98
+ return createResult({ id: 'html5-nav-exists', name: 'HTML5 Navigation', category: 'technical', severity: 'info' }, 'info', 'Missing <nav> element', { recommendation: 'Use <nav> to define a block of navigation links' });
99
+ }
100
+ return null;
101
+ },
102
+ },
103
+ {
104
+ id: 'html5-main-exists',
105
+ name: 'HTML5 Main Content',
106
+ category: 'technical',
107
+ severity: 'info',
108
+ description: 'Page should use a <main> element for the dominant content',
109
+ check: (ctx) => {
110
+ if (!ctx.hasMain) {
111
+ return createResult({ id: 'html5-main-exists', name: 'HTML5 Main Content', category: 'technical', severity: 'info' }, 'info', 'Missing <main> element', { recommendation: 'Use <main> to enclose the dominant content of the <body>' });
112
+ }
113
+ return null;
114
+ },
115
+ },
116
+ {
117
+ id: 'html5-article-exists',
118
+ name: 'HTML5 Article',
119
+ category: 'technical',
120
+ severity: 'info',
121
+ description: 'Consider using <article> for independent, self-contained content',
122
+ check: (ctx) => {
123
+ if (!ctx.hasArticle && ctx.wordCount && ctx.wordCount > 500) {
124
+ return createResult({ id: 'html5-article-exists', name: 'HTML5 Article', category: 'technical', severity: 'info' }, 'info', 'Missing <article> element for substantial content', { recommendation: 'Consider using <article> for blog posts, news articles, etc.' });
125
+ }
126
+ return null;
127
+ },
128
+ },
129
+ {
130
+ id: 'html5-section-exists',
131
+ name: 'HTML5 Section',
132
+ category: 'technical',
133
+ severity: 'info',
134
+ description: 'Consider using <section> for thematic grouping of content',
135
+ check: (ctx) => {
136
+ if (!ctx.hasSection && (ctx.h2Count && ctx.h2Count > 1)) {
137
+ return createResult({ id: 'html5-section-exists', name: 'HTML5 Section', category: 'technical', severity: 'info' }, 'info', 'Missing <section> element for content grouping', { recommendation: 'Consider using <section> to group related content, usually with a heading' });
138
+ }
139
+ return null;
140
+ },
141
+ },
142
+ {
143
+ id: 'html5-footer-exists',
144
+ name: 'HTML5 Footer',
145
+ category: 'technical',
146
+ severity: 'info',
147
+ description: 'Page should use a <footer> element',
148
+ check: (ctx) => {
149
+ if (!ctx.hasFooter) {
150
+ return createResult({ id: 'html5-footer-exists', name: 'HTML5 Footer', category: 'technical', severity: 'info' }, 'info', 'Missing <footer> element', { recommendation: 'Use <footer> for site-wide or page-specific footer content' });
151
+ }
152
+ return null;
153
+ },
154
+ },
155
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const technicalRules: SeoRule[];
@@ -0,0 +1,223 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const technicalRules = [
4
+ {
5
+ id: 'canonical-exists',
6
+ name: 'Canonical URL',
7
+ category: 'technical',
8
+ severity: 'warning',
9
+ description: 'Page should have a canonical URL',
10
+ check: (ctx) => {
11
+ if (!ctx.hasCanonical) {
12
+ return createResult({ id: 'canonical-exists', name: 'Canonical URL', category: 'technical', severity: 'warning' }, 'warn', 'No canonical URL defined', { recommendation: 'Add <link rel="canonical" href="..."> to prevent duplicate content' });
13
+ }
14
+ return createResult({ id: 'canonical-exists', name: 'Canonical URL', category: 'technical', severity: 'warning' }, 'pass', 'Canonical URL is defined', { value: ctx.canonicalUrl });
15
+ },
16
+ },
17
+ {
18
+ id: 'lang-exists',
19
+ name: 'Language',
20
+ category: 'technical',
21
+ severity: 'warning',
22
+ description: 'HTML should have lang attribute',
23
+ check: (ctx) => {
24
+ if (!ctx.hasLang) {
25
+ return createResult({ id: 'lang-exists', name: 'Language', category: 'technical', severity: 'warning' }, 'warn', 'Missing lang attribute on <html>', { recommendation: 'Add lang attribute: <html lang="en">' });
26
+ }
27
+ return createResult({ id: 'lang-exists', name: 'Language', category: 'technical', severity: 'warning' }, 'pass', `Language attribute set (${ctx.langValue})`);
28
+ },
29
+ },
30
+ {
31
+ id: 'charset-exists',
32
+ name: 'Charset',
33
+ category: 'technical',
34
+ severity: 'warning',
35
+ description: 'Page should declare character encoding',
36
+ check: (ctx) => {
37
+ if (!ctx.hasCharset) {
38
+ return createResult({ id: 'charset-exists', name: 'Charset', category: 'technical', severity: 'warning' }, 'warn', 'Missing charset declaration', { recommendation: 'Add <meta charset="UTF-8"> in <head>' });
39
+ }
40
+ if (ctx.charset && ctx.charset.toLowerCase() !== 'utf-8') {
41
+ return createResult({ id: 'charset-exists', name: 'Charset', category: 'technical', severity: 'warning' }, 'warn', `Non-UTF-8 charset: ${ctx.charset}`, { recommendation: 'Use UTF-8 charset for best compatibility' });
42
+ }
43
+ return createResult({ id: 'charset-exists', name: 'Charset', category: 'technical', severity: 'warning' }, 'pass', 'UTF-8 charset declared');
44
+ },
45
+ },
46
+ {
47
+ id: 'robots-noindex',
48
+ name: 'Robots Noindex',
49
+ category: 'technical',
50
+ severity: 'warning',
51
+ description: 'Check if page is set to noindex',
52
+ check: (ctx) => {
53
+ if (!ctx.metaRobots || ctx.metaRobots.length === 0)
54
+ return null;
55
+ if (ctx.metaRobots.includes('noindex')) {
56
+ return createResult({ id: 'robots-noindex', name: 'Robots', category: 'technical', severity: 'warning' }, 'warn', 'Page is set to noindex', { value: ctx.metaRobots.join(', '), recommendation: 'Remove noindex if you want the page to be indexed' });
57
+ }
58
+ return createResult({ id: 'robots-noindex', name: 'Robots', category: 'technical', severity: 'info' }, 'info', `Robots meta: ${ctx.metaRobots.join(', ')}`);
59
+ },
60
+ },
61
+ {
62
+ id: 'favicon-exists',
63
+ name: 'Favicon',
64
+ category: 'technical',
65
+ severity: 'warning',
66
+ description: 'Page should have a favicon defined',
67
+ check: (ctx) => {
68
+ if (!ctx.hasFavicon) {
69
+ return createResult({ id: 'favicon-exists', name: 'Favicon', category: 'technical', severity: 'warning' }, 'warn', 'No favicon defined', { recommendation: 'Add <link rel="icon" href="/favicon.ico"> for browser tab icon' });
70
+ }
71
+ return createResult({ id: 'favicon-exists', name: 'Favicon', category: 'technical', severity: 'warning' }, 'pass', 'Favicon is defined', { value: ctx.faviconUrl });
72
+ },
73
+ },
74
+ {
75
+ id: 'url-length',
76
+ name: 'URL Length',
77
+ category: 'technical',
78
+ severity: 'info',
79
+ description: 'URL should be under 75 characters',
80
+ check: (ctx) => {
81
+ if (!ctx.url)
82
+ return null;
83
+ const len = ctx.urlLength ?? ctx.url.length;
84
+ const max = SEO_THRESHOLDS.url.maxLength;
85
+ if (len > max) {
86
+ return createResult({ id: 'url-length', name: 'URL Length', category: 'technical', severity: 'info' }, 'info', `URL is long (${len} chars)`, { value: len, recommendation: `Keep URLs under ${max} characters when possible` });
87
+ }
88
+ return null;
89
+ },
90
+ },
91
+ {
92
+ id: 'url-lowercase',
93
+ name: 'URL Lowercase',
94
+ category: 'technical',
95
+ severity: 'warning',
96
+ description: 'URLs should be lowercase for consistency',
97
+ check: (ctx) => {
98
+ if (ctx.urlHasUppercase) {
99
+ return createResult({ id: 'url-lowercase', name: 'URL Lowercase', category: 'technical', severity: 'warning' }, 'warn', 'URL contains uppercase characters', { recommendation: 'Use lowercase URLs for better SEO consistency' });
100
+ }
101
+ return null;
102
+ },
103
+ },
104
+ {
105
+ id: 'url-clean',
106
+ name: 'URL Clean',
107
+ category: 'technical',
108
+ severity: 'warning',
109
+ description: 'URLs should not contain special characters or accents',
110
+ check: (ctx) => {
111
+ if (ctx.urlHasAccents || ctx.urlHasSpecialChars) {
112
+ const issues = [];
113
+ if (ctx.urlHasAccents)
114
+ issues.push('accents');
115
+ if (ctx.urlHasSpecialChars)
116
+ issues.push('special characters');
117
+ return createResult({ id: 'url-clean', name: 'URL Clean', category: 'technical', severity: 'warning' }, 'warn', `URL contains ${issues.join(' and ')}`, { recommendation: 'Use clean URLs without accents or special characters' });
118
+ }
119
+ return null;
120
+ },
121
+ },
122
+ {
123
+ id: 'url-no-params',
124
+ name: 'URL Parameters',
125
+ category: 'technical',
126
+ severity: 'warning',
127
+ description: 'URLs should not contain query parameters',
128
+ check: (ctx) => {
129
+ if (!ctx.url)
130
+ return null;
131
+ try {
132
+ const urlObj = new URL(ctx.url);
133
+ if (urlObj.search && urlObj.search.length > 1) {
134
+ return createResult({ id: 'url-no-params', name: 'URL Parameters', category: 'technical', severity: 'warning' }, 'warn', 'URL contains query parameters', { value: urlObj.search, recommendation: 'Use clean URLs (rewritten paths) without query parameters' });
135
+ }
136
+ }
137
+ catch {
138
+ }
139
+ return null;
140
+ },
141
+ },
142
+ {
143
+ id: 'technical-meta-robots-directives',
144
+ name: 'Meta Robots Directives',
145
+ category: 'technical',
146
+ severity: 'warning',
147
+ description: 'Check for restrictive meta robots directives like noindex, nofollow, noarchive etc.',
148
+ check: (ctx) => {
149
+ if (!ctx.metaRobots || ctx.metaRobots.length === 0)
150
+ return null;
151
+ const restrictiveDirectives = ['noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex'];
152
+ const foundRestrictive = ctx.metaRobots.filter(directive => restrictiveDirectives.includes(directive));
153
+ if (foundRestrictive.length > 0) {
154
+ return createResult({ id: 'technical-meta-robots-directives', name: 'Meta Robots Directives', category: 'technical', severity: 'warning' }, 'warn', `Restrictive meta robots directives found: ${foundRestrictive.join(', ')}`, { recommendation: 'Ensure these directives are intentional to prevent unintended blocking of indexing or crawling.' });
155
+ }
156
+ return null;
157
+ },
158
+ },
159
+ {
160
+ id: 'technical-x-robots-tag',
161
+ name: 'X-Robots-Tag Header',
162
+ category: 'technical',
163
+ severity: 'info',
164
+ description: 'X-Robots-Tag header can be used to control indexing, especially for non-HTML content.',
165
+ check: (ctx) => {
166
+ if (!ctx.responseHeaders)
167
+ return null;
168
+ const xRobotsTag = ctx.responseHeaders['x-robots-tag'] || ctx.responseHeaders['X-Robots-Tag'];
169
+ if (xRobotsTag) {
170
+ return createResult({ id: 'technical-x-robots-tag', name: 'X-Robots-Tag Header', category: 'technical', severity: 'info' }, 'info', `X-Robots-Tag header found: ${xRobotsTag}`, { recommendation: 'Ensure X-Robots-Tag directives (e.g., noindex) are intentional.' });
171
+ }
172
+ return null;
173
+ },
174
+ },
175
+ {
176
+ id: 'technical-trust-signals',
177
+ name: 'Trust Signals (Links)',
178
+ category: 'technical',
179
+ severity: 'info',
180
+ description: 'Presence of links to "About", "Contact", "Privacy Policy", "Terms of Service" pages builds trust.',
181
+ check: (ctx) => {
182
+ const missingSignals = [];
183
+ if (!ctx.hasAboutPageLink)
184
+ missingSignals.push('About Page');
185
+ if (!ctx.hasContactPageLink)
186
+ missingSignals.push('Contact Page');
187
+ if (!ctx.hasPrivacyPolicyLink)
188
+ missingSignals.push('Privacy Policy');
189
+ if (!ctx.hasTermsOfServiceLink)
190
+ missingSignals.push('Terms of Service');
191
+ if (missingSignals.length > 0) {
192
+ return createResult({ id: 'technical-trust-signals', name: 'Trust Signals (Links)', category: 'technical', severity: 'info' }, 'info', `Missing links to key trust pages: ${missingSignals.join(', ')}`, { recommendation: 'Add clear links to "About Us", "Contact", "Privacy Policy", and "Terms of Service" pages to build user and search engine trust.' });
193
+ }
194
+ return null;
195
+ },
196
+ },
197
+ {
198
+ id: 'technical-text-html-ratio',
199
+ name: 'Text/HTML Ratio',
200
+ category: 'technical',
201
+ severity: 'info',
202
+ description: 'A higher text to HTML ratio indicates more content relative to code, which is good for SEO.',
203
+ check: (ctx) => {
204
+ if (ctx.textHtmlRatio === undefined)
205
+ return null;
206
+ const threshold = 15;
207
+ if (ctx.textHtmlRatio < threshold) {
208
+ return createResult({ id: 'technical-text-html-ratio', name: 'Text/HTML Ratio', category: 'technical', severity: 'info' }, 'warn', `Low Text/HTML ratio: ${ctx.textHtmlRatio.toFixed(2)}% (target > ${threshold}%)`, { recommendation: 'Increase text content relative to HTML code. Reduce unnecessary markup, or add more textual content.' });
209
+ }
210
+ return null;
211
+ },
212
+ },
213
+ {
214
+ id: 'technical-robots-txt-hint',
215
+ name: 'Robots.txt Hint',
216
+ category: 'technical',
217
+ severity: 'info',
218
+ description: 'Ensure a robots.txt file exists at the root of the domain to guide crawlers.',
219
+ check: (ctx) => {
220
+ return createResult({ id: 'technical-robots-txt-hint', name: 'Robots.txt Hint', category: 'technical', severity: 'info' }, 'info', 'Robots.txt existence cannot be verified from HTML alone.', { recommendation: 'Ensure a valid `robots.txt` file is present at your domain root (e.g., `https://example.com/robots.txt`) to guide search engine crawlers and define your sitemap location.' });
221
+ },
222
+ },
223
+ ];