recker 1.0.31 → 1.0.32-next.e0741bf
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/cli/index.js +2350 -43
- package/dist/cli/tui/shell-search.js +10 -8
- package/dist/cli/tui/shell.d.ts +29 -0
- package/dist/cli/tui/shell.js +1733 -9
- package/dist/mcp/search/hybrid-search.js +4 -2
- package/dist/seo/analyzer.d.ts +7 -0
- package/dist/seo/analyzer.js +200 -4
- package/dist/seo/rules/ai-search.d.ts +2 -0
- package/dist/seo/rules/ai-search.js +423 -0
- package/dist/seo/rules/canonical.d.ts +12 -0
- package/dist/seo/rules/canonical.js +249 -0
- package/dist/seo/rules/crawl.js +113 -0
- package/dist/seo/rules/cwv.js +0 -95
- package/dist/seo/rules/i18n.js +27 -0
- package/dist/seo/rules/images.js +23 -27
- package/dist/seo/rules/index.js +14 -0
- package/dist/seo/rules/internal-linking.js +6 -6
- package/dist/seo/rules/links.js +321 -0
- package/dist/seo/rules/meta.js +24 -0
- package/dist/seo/rules/mobile.js +0 -20
- package/dist/seo/rules/performance.js +124 -0
- package/dist/seo/rules/redirects.d.ts +16 -0
- package/dist/seo/rules/redirects.js +193 -0
- package/dist/seo/rules/resources.d.ts +2 -0
- package/dist/seo/rules/resources.js +373 -0
- package/dist/seo/rules/security.js +290 -0
- package/dist/seo/rules/technical-advanced.d.ts +10 -0
- package/dist/seo/rules/technical-advanced.js +283 -0
- package/dist/seo/rules/technical.js +74 -18
- package/dist/seo/rules/types.d.ts +103 -3
- package/dist/seo/seo-spider.d.ts +2 -0
- package/dist/seo/seo-spider.js +47 -2
- package/dist/seo/types.d.ts +48 -28
- package/dist/seo/utils/index.d.ts +1 -0
- package/dist/seo/utils/index.js +1 -0
- package/dist/seo/utils/similarity.d.ts +47 -0
- package/dist/seo/utils/similarity.js +273 -0
- package/dist/seo/validators/index.d.ts +3 -0
- package/dist/seo/validators/index.js +3 -0
- package/dist/seo/validators/llms-txt.d.ts +57 -0
- package/dist/seo/validators/llms-txt.js +317 -0
- package/dist/seo/validators/robots.d.ts +54 -0
- package/dist/seo/validators/robots.js +382 -0
- package/dist/seo/validators/sitemap.d.ts +69 -0
- package/dist/seo/validators/sitemap.js +424 -0
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const redirectRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'redirect-chain-length',
|
|
5
|
+
name: 'Redirect Chain Length',
|
|
6
|
+
category: 'technical',
|
|
7
|
+
severity: 'warning',
|
|
8
|
+
description: 'Redirect chains should not exceed 3 hops',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.redirectChain === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const chainLength = ctx.redirectChain.length;
|
|
13
|
+
if (chainLength === 0) {
|
|
14
|
+
return createResult({ id: 'redirect-chain-length', name: 'Redirect Chain Length', category: 'technical', severity: 'warning' }, 'pass', 'No redirects detected');
|
|
15
|
+
}
|
|
16
|
+
if (chainLength > 5) {
|
|
17
|
+
return createResult({ id: 'redirect-chain-length', name: 'Redirect Chain Length', category: 'technical', severity: 'warning' }, 'fail', `Redirect chain has ${chainLength} hops (excessive)`, {
|
|
18
|
+
value: chainLength,
|
|
19
|
+
recommendation: 'Consolidate redirects to point directly to final URL',
|
|
20
|
+
evidence: {
|
|
21
|
+
found: ctx.redirectChain.map(r => `${r.status} ${r.from} → ${r.to}`),
|
|
22
|
+
expected: 'Direct link to final destination',
|
|
23
|
+
impact: 'Long redirect chains waste crawl budget and increase latency'
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (chainLength > 3) {
|
|
28
|
+
return createResult({ id: 'redirect-chain-length', name: 'Redirect Chain Length', category: 'technical', severity: 'warning' }, 'warn', `Redirect chain has ${chainLength} hops`, {
|
|
29
|
+
value: chainLength,
|
|
30
|
+
recommendation: 'Reduce redirect chain by updating original links',
|
|
31
|
+
evidence: {
|
|
32
|
+
found: ctx.redirectChain.map(r => `${r.status} ${r.from} → ${r.to}`),
|
|
33
|
+
expected: 'Maximum 3 redirects',
|
|
34
|
+
impact: 'Excessive redirects slow page load and lose link equity'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return createResult({ id: 'redirect-chain-length', name: 'Redirect Chain Length', category: 'technical', severity: 'warning' }, 'info', `Page reached after ${chainLength} redirect(s)`, {
|
|
39
|
+
value: chainLength,
|
|
40
|
+
evidence: {
|
|
41
|
+
found: ctx.redirectChain.map(r => `${r.status} ${r.from} → ${r.to}`)
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'redirect-loop',
|
|
48
|
+
name: 'Redirect Loop',
|
|
49
|
+
category: 'technical',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
description: 'Pages should not create redirect loops',
|
|
52
|
+
check: (ctx) => {
|
|
53
|
+
if (!ctx.redirectChain || ctx.redirectChain.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const urls = ctx.redirectChain.map(r => r.from);
|
|
56
|
+
urls.push(ctx.redirectChain[ctx.redirectChain.length - 1].to);
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let loopUrl;
|
|
59
|
+
for (const url of urls) {
|
|
60
|
+
const normalized = normalizeUrl(url);
|
|
61
|
+
if (seen.has(normalized)) {
|
|
62
|
+
loopUrl = url;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
seen.add(normalized);
|
|
66
|
+
}
|
|
67
|
+
if (loopUrl) {
|
|
68
|
+
return createResult({ id: 'redirect-loop', name: 'Redirect Loop', category: 'technical', severity: 'error' }, 'fail', 'Redirect loop detected', {
|
|
69
|
+
recommendation: 'Fix server configuration to break the redirect loop',
|
|
70
|
+
evidence: {
|
|
71
|
+
found: ctx.redirectChain.map(r => `${r.status} ${r.from} → ${r.to}`),
|
|
72
|
+
issue: `Loop at: ${loopUrl}`,
|
|
73
|
+
impact: 'Redirect loops prevent page from being crawled and indexed'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'redirect-type',
|
|
82
|
+
name: 'Redirect Type',
|
|
83
|
+
category: 'technical',
|
|
84
|
+
severity: 'info',
|
|
85
|
+
description: 'Permanent content moves should use 301 redirects',
|
|
86
|
+
check: (ctx) => {
|
|
87
|
+
if (!ctx.redirectChain || ctx.redirectChain.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const temporary = ctx.redirectChain.filter(r => r.status === 302 || r.status === 307);
|
|
90
|
+
const permanent = ctx.redirectChain.filter(r => r.status === 301 || r.status === 308);
|
|
91
|
+
if (temporary.length > 0 && permanent.length === 0) {
|
|
92
|
+
return createResult({ id: 'redirect-type', name: 'Redirect Type', category: 'technical', severity: 'info' }, 'warn', `Using temporary redirect (${temporary[0].status})`, {
|
|
93
|
+
recommendation: 'Use 301 redirect for permanent URL changes to pass link equity',
|
|
94
|
+
evidence: {
|
|
95
|
+
found: `${temporary[0].status} ${temporary[0].from} → ${temporary[0].to}`,
|
|
96
|
+
expected: '301 for permanent moves, 302 only for temporary changes',
|
|
97
|
+
impact: 'Temporary redirects may not pass full link equity'
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (permanent.length > 0) {
|
|
102
|
+
return createResult({ id: 'redirect-type', name: 'Redirect Type', category: 'technical', severity: 'info' }, 'pass', 'Using permanent redirect (301)');
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'www-consistency',
|
|
109
|
+
name: 'WWW vs Non-WWW',
|
|
110
|
+
category: 'canonicalization',
|
|
111
|
+
severity: 'warning',
|
|
112
|
+
description: 'Site should redirect consistently to either WWW or non-WWW version',
|
|
113
|
+
check: (ctx) => {
|
|
114
|
+
if (ctx.wwwConsistency === undefined)
|
|
115
|
+
return null;
|
|
116
|
+
const { canonicalHasWww, urlHasWww, redirectsToCanonical } = ctx.wwwConsistency;
|
|
117
|
+
if (canonicalHasWww !== urlHasWww && !redirectsToCanonical) {
|
|
118
|
+
return createResult({ id: 'www-consistency', name: 'WWW vs Non-WWW', category: 'canonicalization', severity: 'warning' }, 'warn', 'WWW/non-WWW version accessible without redirect', {
|
|
119
|
+
recommendation: 'Set up 301 redirect from non-canonical version to canonical',
|
|
120
|
+
evidence: {
|
|
121
|
+
found: urlHasWww ? 'www version' : 'non-www version',
|
|
122
|
+
expected: `Redirect to ${canonicalHasWww ? 'www' : 'non-www'} version`,
|
|
123
|
+
impact: 'Duplicate content issues when both versions are accessible'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return createResult({ id: 'www-consistency', name: 'WWW vs Non-WWW', category: 'canonicalization', severity: 'warning' }, 'pass', 'WWW handling is consistent');
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'http-to-https-redirect',
|
|
132
|
+
name: 'HTTP to HTTPS Redirect',
|
|
133
|
+
category: 'security',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
description: 'HTTP version should redirect to HTTPS',
|
|
136
|
+
check: (ctx) => {
|
|
137
|
+
if (ctx.httpRedirectsToHttps === undefined)
|
|
138
|
+
return null;
|
|
139
|
+
if (!ctx.httpRedirectsToHttps) {
|
|
140
|
+
return createResult({ id: 'http-to-https-redirect', name: 'HTTP to HTTPS Redirect', category: 'security', severity: 'warning' }, 'warn', 'HTTP version does not redirect to HTTPS', {
|
|
141
|
+
recommendation: 'Configure server to redirect HTTP traffic to HTTPS',
|
|
142
|
+
evidence: {
|
|
143
|
+
found: 'HTTP accessible without redirect',
|
|
144
|
+
expected: '301 redirect from HTTP to HTTPS',
|
|
145
|
+
impact: 'Users may access insecure version; duplicate content issues',
|
|
146
|
+
learnMore: 'https://web.dev/why-https-matters/'
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return createResult({ id: 'http-to-https-redirect', name: 'HTTP to HTTPS Redirect', category: 'security', severity: 'warning' }, 'pass', 'HTTP properly redirects to HTTPS');
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'cross-domain-redirect',
|
|
155
|
+
name: 'Cross-Domain Redirect',
|
|
156
|
+
category: 'technical',
|
|
157
|
+
severity: 'info',
|
|
158
|
+
description: 'Detects redirects to different domains',
|
|
159
|
+
check: (ctx) => {
|
|
160
|
+
if (!ctx.redirectChain || ctx.redirectChain.length === 0)
|
|
161
|
+
return null;
|
|
162
|
+
const crossDomainRedirects = ctx.redirectChain.filter(r => {
|
|
163
|
+
try {
|
|
164
|
+
const fromHost = new URL(r.from).hostname;
|
|
165
|
+
const toHost = new URL(r.to).hostname;
|
|
166
|
+
return fromHost !== toHost;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (crossDomainRedirects.length > 0) {
|
|
173
|
+
return createResult({ id: 'cross-domain-redirect', name: 'Cross-Domain Redirect', category: 'technical', severity: 'info' }, 'info', `${crossDomainRedirects.length} cross-domain redirect(s) detected`, {
|
|
174
|
+
value: crossDomainRedirects.length,
|
|
175
|
+
evidence: {
|
|
176
|
+
found: crossDomainRedirects.map(r => `${r.from} → ${r.to}`),
|
|
177
|
+
impact: 'Cross-domain redirects may indicate site migration or tracking'
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
function normalizeUrl(url) {
|
|
186
|
+
try {
|
|
187
|
+
const u = new URL(url);
|
|
188
|
+
return `${u.protocol}//${u.hostname}${u.pathname.replace(/\/$/, '')}${u.search}`;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return url.toLowerCase();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const resourceRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'resources-js-files-count',
|
|
5
|
+
name: 'JavaScript File Count',
|
|
6
|
+
category: 'resources',
|
|
7
|
+
severity: 'warning',
|
|
8
|
+
description: 'Too many JavaScript files increase HTTP requests and slow page load',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.jsFilesCount === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const max = 15;
|
|
13
|
+
if (ctx.jsFilesCount > max) {
|
|
14
|
+
return createResult({ id: 'resources-js-files-count', name: 'JavaScript File Count', category: 'resources', severity: 'warning' }, 'warn', `Too many JS files (${ctx.jsFilesCount})`, {
|
|
15
|
+
value: ctx.jsFilesCount,
|
|
16
|
+
recommendation: `Bundle JavaScript files to reduce HTTP requests (max ${max})`,
|
|
17
|
+
evidence: {
|
|
18
|
+
found: ctx.jsFilesCount,
|
|
19
|
+
expected: `${max} or fewer`,
|
|
20
|
+
impact: 'Each HTTP request adds latency and slows page load',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'resources-js-total-size',
|
|
29
|
+
name: 'JavaScript Total Size',
|
|
30
|
+
category: 'resources',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
description: 'Large JavaScript bundles slow page load and increase bandwidth',
|
|
33
|
+
check: (ctx) => {
|
|
34
|
+
if (ctx.jsTotalSize === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const maxKB = 500;
|
|
37
|
+
const sizeKB = Math.round(ctx.jsTotalSize / 1024);
|
|
38
|
+
if (sizeKB > maxKB) {
|
|
39
|
+
return createResult({ id: 'resources-js-total-size', name: 'JavaScript Total Size', category: 'resources', severity: 'warning' }, 'warn', `Large JS bundle (${sizeKB}KB)`, {
|
|
40
|
+
value: sizeKB,
|
|
41
|
+
recommendation: `Reduce JavaScript size to under ${maxKB}KB`,
|
|
42
|
+
evidence: {
|
|
43
|
+
found: `${sizeKB}KB`,
|
|
44
|
+
expected: `${maxKB}KB or less`,
|
|
45
|
+
impact: 'Large JS bundles delay page interactivity',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'resources-js-render-blocking',
|
|
54
|
+
name: 'Render-Blocking JavaScript',
|
|
55
|
+
category: 'resources',
|
|
56
|
+
severity: 'warning',
|
|
57
|
+
description: 'Render-blocking JS in <head> delays page rendering',
|
|
58
|
+
check: (ctx) => {
|
|
59
|
+
if (ctx.renderBlockingJs === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
if (ctx.renderBlockingJs > 0) {
|
|
62
|
+
return createResult({ id: 'resources-js-render-blocking', name: 'Render-Blocking JavaScript', category: 'resources', severity: 'warning' }, 'warn', `${ctx.renderBlockingJs} render-blocking JS file(s)`, {
|
|
63
|
+
value: ctx.renderBlockingJs,
|
|
64
|
+
recommendation: 'Add async or defer attribute to non-critical scripts',
|
|
65
|
+
evidence: {
|
|
66
|
+
found: `${ctx.renderBlockingJs} scripts without async/defer`,
|
|
67
|
+
expected: 'All scripts with async, defer, or in body',
|
|
68
|
+
example: '<script src="app.js" defer></script>',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return createResult({ id: 'resources-js-render-blocking', name: 'Render-Blocking JavaScript', category: 'resources', severity: 'warning' }, 'pass', 'No render-blocking JavaScript');
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'resources-css-files-count',
|
|
77
|
+
name: 'CSS File Count',
|
|
78
|
+
category: 'resources',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
description: 'Too many CSS files increase HTTP requests',
|
|
81
|
+
check: (ctx) => {
|
|
82
|
+
if (ctx.cssFilesCount === undefined)
|
|
83
|
+
return null;
|
|
84
|
+
const max = 10;
|
|
85
|
+
if (ctx.cssFilesCount > max) {
|
|
86
|
+
return createResult({ id: 'resources-css-files-count', name: 'CSS File Count', category: 'resources', severity: 'warning' }, 'warn', `Too many CSS files (${ctx.cssFilesCount})`, {
|
|
87
|
+
value: ctx.cssFilesCount,
|
|
88
|
+
recommendation: `Bundle CSS files to reduce HTTP requests (max ${max})`,
|
|
89
|
+
evidence: {
|
|
90
|
+
found: ctx.cssFilesCount,
|
|
91
|
+
expected: `${max} or fewer`,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'resources-css-total-size',
|
|
100
|
+
name: 'CSS Total Size',
|
|
101
|
+
category: 'resources',
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
description: 'Large CSS files delay page rendering',
|
|
104
|
+
check: (ctx) => {
|
|
105
|
+
if (ctx.cssTotalSize === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const maxKB = 200;
|
|
108
|
+
const sizeKB = Math.round(ctx.cssTotalSize / 1024);
|
|
109
|
+
if (sizeKB > maxKB) {
|
|
110
|
+
return createResult({ id: 'resources-css-total-size', name: 'CSS Total Size', category: 'resources', severity: 'warning' }, 'warn', `Large CSS bundle (${sizeKB}KB)`, {
|
|
111
|
+
value: sizeKB,
|
|
112
|
+
recommendation: `Reduce CSS size to under ${maxKB}KB`,
|
|
113
|
+
evidence: {
|
|
114
|
+
found: `${sizeKB}KB`,
|
|
115
|
+
expected: `${maxKB}KB or less`,
|
|
116
|
+
impact: 'Large CSS delays first contentful paint',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'resources-css-render-blocking',
|
|
125
|
+
name: 'Render-Blocking CSS',
|
|
126
|
+
category: 'resources',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
description: 'Critical CSS should be inlined, non-critical deferred',
|
|
129
|
+
check: (ctx) => {
|
|
130
|
+
if (ctx.cssFilesCount === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
if (ctx.cssFilesCount > 3 && !ctx.hasCriticalCss) {
|
|
133
|
+
return createResult({ id: 'resources-css-render-blocking', name: 'Render-Blocking CSS', category: 'resources', severity: 'info' }, 'info', `${ctx.cssFilesCount} CSS files blocking render`, {
|
|
134
|
+
value: ctx.cssFilesCount,
|
|
135
|
+
recommendation: 'Consider inlining critical CSS and lazy-loading the rest',
|
|
136
|
+
evidence: {
|
|
137
|
+
impact: 'CSS blocks rendering until fully loaded',
|
|
138
|
+
example: '<link rel="preload" href="style.css" as="style" onload="this.rel=\'stylesheet\'">',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'resources-image-size-large',
|
|
147
|
+
name: 'Large Image Files',
|
|
148
|
+
category: 'resources',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
description: 'Images over 200KB should be optimized',
|
|
151
|
+
check: (ctx) => {
|
|
152
|
+
if (!ctx.largeImages || ctx.largeImages.length === 0)
|
|
153
|
+
return null;
|
|
154
|
+
const maxKB = 200;
|
|
155
|
+
const largeCount = ctx.largeImages.length;
|
|
156
|
+
return createResult({ id: 'resources-image-size-large', name: 'Large Image Files', category: 'resources', severity: 'warning' }, 'warn', `${largeCount} image(s) over ${maxKB}KB`, {
|
|
157
|
+
value: largeCount,
|
|
158
|
+
recommendation: 'Compress images or use modern formats (WebP, AVIF)',
|
|
159
|
+
evidence: {
|
|
160
|
+
found: ctx.largeImages.slice(0, 5),
|
|
161
|
+
impact: 'Large images significantly slow page load',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'resources-image-format',
|
|
168
|
+
name: 'Modern Image Formats',
|
|
169
|
+
category: 'resources',
|
|
170
|
+
severity: 'info',
|
|
171
|
+
description: 'Use WebP or AVIF for better compression',
|
|
172
|
+
check: (ctx) => {
|
|
173
|
+
if (ctx.imagesTotal === undefined || ctx.imagesTotal === 0)
|
|
174
|
+
return null;
|
|
175
|
+
if (ctx.modernFormatImages === undefined)
|
|
176
|
+
return null;
|
|
177
|
+
const modernPercent = (ctx.modernFormatImages / ctx.imagesTotal) * 100;
|
|
178
|
+
if (modernPercent < 50) {
|
|
179
|
+
return createResult({ id: 'resources-image-format', name: 'Modern Image Formats', category: 'resources', severity: 'info' }, 'info', `Only ${Math.round(modernPercent)}% of images use modern formats`, {
|
|
180
|
+
value: ctx.modernFormatImages,
|
|
181
|
+
recommendation: 'Convert images to WebP or AVIF for 25-50% smaller files',
|
|
182
|
+
evidence: {
|
|
183
|
+
found: `${ctx.modernFormatImages}/${ctx.imagesTotal} modern format images`,
|
|
184
|
+
expected: 'WebP or AVIF for all images',
|
|
185
|
+
impact: 'Modern formats reduce image size by 25-50%',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return createResult({ id: 'resources-image-format', name: 'Modern Image Formats', category: 'resources', severity: 'info' }, 'pass', `${Math.round(modernPercent)}% of images use modern formats`);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'resources-image-dimensions',
|
|
194
|
+
name: 'Image Dimensions',
|
|
195
|
+
category: 'resources',
|
|
196
|
+
severity: 'warning',
|
|
197
|
+
description: 'Images should have width and height attributes',
|
|
198
|
+
check: (ctx) => {
|
|
199
|
+
if (ctx.imagesMissingDimensions === undefined)
|
|
200
|
+
return null;
|
|
201
|
+
if (ctx.imagesMissingDimensions > 0) {
|
|
202
|
+
return createResult({ id: 'resources-image-dimensions', name: 'Image Dimensions', category: 'resources', severity: 'warning' }, 'warn', `${ctx.imagesMissingDimensions} image(s) missing width/height`, {
|
|
203
|
+
value: ctx.imagesMissingDimensions,
|
|
204
|
+
recommendation: 'Add width and height attributes to prevent layout shift',
|
|
205
|
+
evidence: {
|
|
206
|
+
impact: 'Missing dimensions cause Cumulative Layout Shift (CLS)',
|
|
207
|
+
example: '<img src="photo.jpg" width="800" height="600" alt="Photo">',
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return createResult({ id: 'resources-image-dimensions', name: 'Image Dimensions', category: 'resources', severity: 'warning' }, 'pass', 'All images have width/height attributes');
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'resources-font-files',
|
|
216
|
+
name: 'Web Font Files',
|
|
217
|
+
category: 'resources',
|
|
218
|
+
severity: 'info',
|
|
219
|
+
description: 'Too many font files can slow page load',
|
|
220
|
+
check: (ctx) => {
|
|
221
|
+
if (ctx.fontFilesCount === undefined)
|
|
222
|
+
return null;
|
|
223
|
+
const max = 4;
|
|
224
|
+
if (ctx.fontFilesCount > max) {
|
|
225
|
+
return createResult({ id: 'resources-font-files', name: 'Web Font Files', category: 'resources', severity: 'info' }, 'info', `${ctx.fontFilesCount} font files loaded`, {
|
|
226
|
+
value: ctx.fontFilesCount,
|
|
227
|
+
recommendation: `Limit to ${max} font files or use variable fonts`,
|
|
228
|
+
evidence: {
|
|
229
|
+
found: ctx.fontFilesCount,
|
|
230
|
+
expected: `${max} or fewer`,
|
|
231
|
+
impact: 'Each font file adds to page weight and load time',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: 'resources-font-display',
|
|
240
|
+
name: 'Font Display Strategy',
|
|
241
|
+
category: 'resources',
|
|
242
|
+
severity: 'info',
|
|
243
|
+
description: 'Use font-display: swap to prevent invisible text during load',
|
|
244
|
+
check: (ctx) => {
|
|
245
|
+
if (ctx.hasFontDisplaySwap === undefined)
|
|
246
|
+
return null;
|
|
247
|
+
if (!ctx.hasFontDisplaySwap && ctx.fontFilesCount && ctx.fontFilesCount > 0) {
|
|
248
|
+
return createResult({ id: 'resources-font-display', name: 'Font Display Strategy', category: 'resources', severity: 'info' }, 'info', 'font-display: swap not detected', {
|
|
249
|
+
recommendation: 'Add font-display: swap to @font-face rules',
|
|
250
|
+
evidence: {
|
|
251
|
+
expected: '@font-face { font-display: swap; }',
|
|
252
|
+
impact: 'Without swap, text may be invisible during font load (FOIT)',
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'resources-total-requests',
|
|
261
|
+
name: 'Total HTTP Requests',
|
|
262
|
+
category: 'resources',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
description: 'Too many HTTP requests slow page load',
|
|
265
|
+
check: (ctx) => {
|
|
266
|
+
if (ctx.totalRequests === undefined)
|
|
267
|
+
return null;
|
|
268
|
+
const max = 50;
|
|
269
|
+
if (ctx.totalRequests > max) {
|
|
270
|
+
return createResult({ id: 'resources-total-requests', name: 'Total HTTP Requests', category: 'resources', severity: 'warning' }, 'warn', `Too many HTTP requests (${ctx.totalRequests})`, {
|
|
271
|
+
value: ctx.totalRequests,
|
|
272
|
+
recommendation: `Reduce to under ${max} requests by bundling and optimizing`,
|
|
273
|
+
evidence: {
|
|
274
|
+
found: ctx.totalRequests,
|
|
275
|
+
expected: `${max} or fewer`,
|
|
276
|
+
impact: 'Each HTTP request adds latency',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return createResult({ id: 'resources-total-requests', name: 'Total HTTP Requests', category: 'resources', severity: 'warning' }, 'pass', `Good HTTP request count (${ctx.totalRequests})`);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'resources-total-size',
|
|
285
|
+
name: 'Total Page Size',
|
|
286
|
+
category: 'resources',
|
|
287
|
+
severity: 'warning',
|
|
288
|
+
description: 'Total page weight affects load time',
|
|
289
|
+
check: (ctx) => {
|
|
290
|
+
if (ctx.totalPageSize === undefined)
|
|
291
|
+
return null;
|
|
292
|
+
const maxMB = 3;
|
|
293
|
+
const sizeMB = ctx.totalPageSize / (1024 * 1024);
|
|
294
|
+
if (sizeMB > maxMB) {
|
|
295
|
+
return createResult({ id: 'resources-total-size', name: 'Total Page Size', category: 'resources', severity: 'warning' }, 'warn', `Large page size (${sizeMB.toFixed(1)}MB)`, {
|
|
296
|
+
value: Math.round(ctx.totalPageSize / 1024),
|
|
297
|
+
recommendation: `Reduce total page size to under ${maxMB}MB`,
|
|
298
|
+
evidence: {
|
|
299
|
+
found: `${sizeMB.toFixed(1)}MB`,
|
|
300
|
+
expected: `${maxMB}MB or less`,
|
|
301
|
+
impact: 'Large pages are slow on mobile and low-bandwidth connections',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return createResult({ id: 'resources-total-size', name: 'Total Page Size', category: 'resources', severity: 'warning' }, 'pass', `Good page size (${sizeMB.toFixed(1)}MB)`);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'resources-compression',
|
|
310
|
+
name: 'Resource Compression',
|
|
311
|
+
category: 'resources',
|
|
312
|
+
severity: 'warning',
|
|
313
|
+
description: 'Text resources should be compressed with gzip or brotli',
|
|
314
|
+
check: (ctx) => {
|
|
315
|
+
if (ctx.uncompressedResources === undefined)
|
|
316
|
+
return null;
|
|
317
|
+
if (ctx.uncompressedResources > 0) {
|
|
318
|
+
return createResult({ id: 'resources-compression', name: 'Resource Compression', category: 'resources', severity: 'warning' }, 'warn', `${ctx.uncompressedResources} uncompressed resource(s)`, {
|
|
319
|
+
value: ctx.uncompressedResources,
|
|
320
|
+
recommendation: 'Enable gzip or Brotli compression on your server',
|
|
321
|
+
evidence: {
|
|
322
|
+
impact: 'Compression typically reduces file size by 60-80%',
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return createResult({ id: 'resources-compression', name: 'Resource Compression', category: 'resources', severity: 'warning' }, 'pass', 'All text resources are compressed');
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'resources-caching',
|
|
331
|
+
name: 'Browser Caching',
|
|
332
|
+
category: 'resources',
|
|
333
|
+
severity: 'info',
|
|
334
|
+
description: 'Static resources should have long cache lifetimes',
|
|
335
|
+
check: (ctx) => {
|
|
336
|
+
if (ctx.resourcesWithoutCaching === undefined)
|
|
337
|
+
return null;
|
|
338
|
+
if (ctx.resourcesWithoutCaching > 0) {
|
|
339
|
+
return createResult({ id: 'resources-caching', name: 'Browser Caching', category: 'resources', severity: 'info' }, 'info', `${ctx.resourcesWithoutCaching} resource(s) without proper caching`, {
|
|
340
|
+
value: ctx.resourcesWithoutCaching,
|
|
341
|
+
recommendation: 'Set Cache-Control headers for static resources',
|
|
342
|
+
evidence: {
|
|
343
|
+
expected: 'Cache-Control: max-age=31536000 for static assets',
|
|
344
|
+
impact: 'Proper caching improves repeat visit performance',
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return createResult({ id: 'resources-caching', name: 'Browser Caching', category: 'resources', severity: 'info' }, 'pass', 'Static resources have proper caching');
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'broken-external-resources',
|
|
353
|
+
name: 'Broken External Resources',
|
|
354
|
+
category: 'resources',
|
|
355
|
+
severity: 'warning',
|
|
356
|
+
description: 'External JS/CSS files should be accessible',
|
|
357
|
+
check: (ctx) => {
|
|
358
|
+
if (ctx.brokenExternalResources === undefined)
|
|
359
|
+
return null;
|
|
360
|
+
if (ctx.brokenExternalResources > 0) {
|
|
361
|
+
return createResult({ id: 'broken-external-resources', name: 'Broken External Resources', category: 'resources', severity: 'warning' }, 'warn', `${ctx.brokenExternalResources} broken external JS/CSS files`, {
|
|
362
|
+
value: ctx.brokenExternalResources,
|
|
363
|
+
recommendation: 'Fix or remove references to broken external resources',
|
|
364
|
+
evidence: {
|
|
365
|
+
found: ctx.brokenExternalResourceUrls?.slice(0, 5) || [],
|
|
366
|
+
impact: 'Broken resources may cause rendering issues and affect user experience'
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
];
|