recker 1.0.32 → 1.0.33-next.bbc56eb

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/cli/index.js +2354 -39
  2. package/dist/cli/tui/shell-search.js +10 -8
  3. package/dist/cli/tui/shell.d.ts +29 -0
  4. package/dist/cli/tui/shell.js +1733 -9
  5. package/dist/mcp/search/hybrid-search.js +4 -2
  6. package/dist/seo/analyzer.d.ts +7 -0
  7. package/dist/seo/analyzer.js +200 -4
  8. package/dist/seo/rules/ai-search.d.ts +2 -0
  9. package/dist/seo/rules/ai-search.js +423 -0
  10. package/dist/seo/rules/canonical.d.ts +12 -0
  11. package/dist/seo/rules/canonical.js +249 -0
  12. package/dist/seo/rules/crawl.js +113 -0
  13. package/dist/seo/rules/cwv.js +0 -95
  14. package/dist/seo/rules/i18n.js +27 -0
  15. package/dist/seo/rules/images.js +23 -27
  16. package/dist/seo/rules/index.js +14 -0
  17. package/dist/seo/rules/internal-linking.js +6 -6
  18. package/dist/seo/rules/links.js +321 -0
  19. package/dist/seo/rules/meta.js +24 -0
  20. package/dist/seo/rules/mobile.js +0 -20
  21. package/dist/seo/rules/performance.js +124 -0
  22. package/dist/seo/rules/redirects.d.ts +16 -0
  23. package/dist/seo/rules/redirects.js +193 -0
  24. package/dist/seo/rules/resources.d.ts +2 -0
  25. package/dist/seo/rules/resources.js +373 -0
  26. package/dist/seo/rules/security.js +290 -0
  27. package/dist/seo/rules/technical-advanced.d.ts +10 -0
  28. package/dist/seo/rules/technical-advanced.js +283 -0
  29. package/dist/seo/rules/technical.js +74 -18
  30. package/dist/seo/rules/types.d.ts +103 -3
  31. package/dist/seo/seo-spider.d.ts +2 -0
  32. package/dist/seo/seo-spider.js +47 -2
  33. package/dist/seo/types.d.ts +48 -28
  34. package/dist/seo/utils/index.d.ts +1 -0
  35. package/dist/seo/utils/index.js +1 -0
  36. package/dist/seo/utils/similarity.d.ts +47 -0
  37. package/dist/seo/utils/similarity.js +273 -0
  38. package/dist/seo/validators/index.d.ts +3 -0
  39. package/dist/seo/validators/index.js +3 -0
  40. package/dist/seo/validators/llms-txt.d.ts +57 -0
  41. package/dist/seo/validators/llms-txt.js +317 -0
  42. package/dist/seo/validators/robots.d.ts +54 -0
  43. package/dist/seo/validators/robots.js +382 -0
  44. package/dist/seo/validators/sitemap.d.ts +69 -0
  45. package/dist/seo/validators/sitemap.js +424 -0
  46. package/package.json +1 -1
@@ -1,5 +1,33 @@
1
1
  import { createResult } from './types.js';
2
2
  import { SEO_THRESHOLDS } from './thresholds.js';
3
+ const GENERIC_ANCHOR_TEXTS = new Set([
4
+ 'click here', 'click', 'here', 'read more', 'learn more', 'more', 'link',
5
+ 'this', 'go', 'continue', 'next', 'previous', 'back', 'see more',
6
+ 'find out more', 'discover more', 'view more', 'show more', 'details',
7
+ 'clique aqui', 'clique', 'aqui', 'saiba mais', 'leia mais', 'veja mais',
8
+ 'mais', 'confira', 'ver mais', 'continuar', 'próximo', 'anterior', 'voltar',
9
+ 'haga clic aquí', 'clic aquí', 'aquí', 'leer más', 'ver más', 'más',
10
+ 'saber más', 'descubrir más', 'continuar',
11
+ 'cliquez ici', 'ici', 'en savoir plus', 'lire la suite', 'voir plus',
12
+ 'plus', 'continuer', 'suivant', 'précédent',
13
+ 'hier klicken', 'hier', 'mehr erfahren', 'weiterlesen', 'mehr',
14
+ 'weiter', 'zurück',
15
+ 'clicca qui', 'qui', 'scopri di più', 'leggi di più', 'continua',
16
+ ]);
17
+ function isGenericAnchorText(text) {
18
+ const normalized = text.trim().toLowerCase();
19
+ if (normalized.length === 0)
20
+ return true;
21
+ if (normalized.length < 3)
22
+ return true;
23
+ if (GENERIC_ANCHOR_TEXTS.has(normalized))
24
+ return true;
25
+ if (/^[\d\s\-_.,!?#@%&*()[\]{}|\\/<>:;"'`~+=]+$/.test(normalized))
26
+ return true;
27
+ if (['link', 'url', 'page', 'site', 'website', 'info'].includes(normalized))
28
+ return true;
29
+ return false;
30
+ }
3
31
  export const linkRules = [
4
32
  {
5
33
  id: 'links-descriptive-text',
@@ -147,4 +175,297 @@ export const linkRules = [
147
175
  return null;
148
176
  },
149
177
  },
178
+ {
179
+ id: 'links-anchor-text-non-descriptive',
180
+ name: 'Non-Descriptive Anchor Text',
181
+ category: 'links',
182
+ severity: 'warning',
183
+ description: 'Anchor text should describe the link destination',
184
+ check: (ctx) => {
185
+ if (!ctx.allLinks || ctx.allLinks.length === 0)
186
+ return null;
187
+ const nonDescriptive = ctx.allLinks.filter((link) => link.text && isGenericAnchorText(link.text));
188
+ if (nonDescriptive.length > 0) {
189
+ return createResult({ id: 'links-anchor-text-non-descriptive', name: 'Non-Descriptive Anchor Text', category: 'links', severity: 'warning' }, 'warn', `${nonDescriptive.length} link(s) with non-descriptive anchor text`, {
190
+ value: nonDescriptive.length,
191
+ recommendation: 'Replace generic text with descriptive anchors',
192
+ evidence: {
193
+ found: nonDescriptive.slice(0, 5).map((l) => `"${l.text}" → ${l.href}`),
194
+ issue: 'Generic anchor text like "click here" provides no context',
195
+ example: 'Instead of <a href="/pricing">Click here</a>, use <a href="/pricing">View our pricing plans</a>',
196
+ },
197
+ });
198
+ }
199
+ return null;
200
+ },
201
+ },
202
+ {
203
+ id: 'links-self-referencing',
204
+ name: 'Self-Referencing Links',
205
+ category: 'links',
206
+ severity: 'info',
207
+ description: 'Pages should not link to themselves excessively',
208
+ check: (ctx) => {
209
+ if (!ctx.selfReferencingLinks)
210
+ return null;
211
+ if (ctx.selfReferencingLinks > 3) {
212
+ return createResult({ id: 'links-self-referencing', name: 'Self-Referencing Links', category: 'links', severity: 'info' }, 'info', `${ctx.selfReferencingLinks} self-referencing link(s)`, {
213
+ value: ctx.selfReferencingLinks,
214
+ recommendation: 'Remove unnecessary self-referencing links',
215
+ evidence: {
216
+ impact: 'Self-links can confuse users and dilute link equity',
217
+ },
218
+ });
219
+ }
220
+ return null;
221
+ },
222
+ },
223
+ {
224
+ id: 'links-broken-internal',
225
+ name: 'Broken Internal Links',
226
+ category: 'links',
227
+ severity: 'error',
228
+ description: 'Internal links should not return 4xx errors',
229
+ check: (ctx) => {
230
+ if (!ctx.brokenInternalLinks || ctx.brokenInternalLinks.length === 0)
231
+ return null;
232
+ return createResult({ id: 'links-broken-internal', name: 'Broken Internal Links', category: 'links', severity: 'error' }, 'fail', `${ctx.brokenInternalLinks.length} broken internal link(s)`, {
233
+ value: ctx.brokenInternalLinks.length,
234
+ recommendation: 'Fix or remove broken internal links',
235
+ evidence: {
236
+ found: ctx.brokenInternalLinks.slice(0, 10),
237
+ impact: 'Broken links harm user experience and waste crawl budget',
238
+ },
239
+ });
240
+ },
241
+ },
242
+ {
243
+ id: 'links-broken-external',
244
+ name: 'Broken External Links',
245
+ category: 'links',
246
+ severity: 'warning',
247
+ description: 'External links should not return 4xx errors',
248
+ check: (ctx) => {
249
+ if (!ctx.brokenExternalLinks || ctx.brokenExternalLinks.length === 0)
250
+ return null;
251
+ return createResult({ id: 'links-broken-external', name: 'Broken External Links', category: 'links', severity: 'warning' }, 'warn', `${ctx.brokenExternalLinks.length} broken external link(s)`, {
252
+ value: ctx.brokenExternalLinks.length,
253
+ recommendation: 'Update or remove broken external links',
254
+ evidence: {
255
+ found: ctx.brokenExternalLinks.slice(0, 10),
256
+ impact: 'Broken external links reduce trust and user experience',
257
+ },
258
+ });
259
+ },
260
+ },
261
+ {
262
+ id: 'links-redirect-chains',
263
+ name: 'Redirect Chain Links',
264
+ category: 'links',
265
+ severity: 'warning',
266
+ description: 'Internal links should not point to redirect chains',
267
+ check: (ctx) => {
268
+ if (!ctx.redirectChainLinks || ctx.redirectChainLinks.length === 0)
269
+ return null;
270
+ return createResult({ id: 'links-redirect-chains', name: 'Redirect Chain Links', category: 'links', severity: 'warning' }, 'warn', `${ctx.redirectChainLinks.length} link(s) with redirect chains`, {
271
+ value: ctx.redirectChainLinks.length,
272
+ recommendation: 'Update links to point directly to final destination',
273
+ evidence: {
274
+ found: ctx.redirectChainLinks.slice(0, 5).map((l) => `${l.from} → ${l.to} (${l.hops} hops)`),
275
+ impact: 'Redirect chains slow page load and waste crawl budget',
276
+ },
277
+ });
278
+ },
279
+ },
280
+ {
281
+ id: 'links-few-internal',
282
+ name: 'Pages with Few Internal Links',
283
+ category: 'links',
284
+ severity: 'warning',
285
+ description: 'Pages should have at least 3 internal links',
286
+ check: (ctx) => {
287
+ if (ctx.internalLinks === undefined)
288
+ return null;
289
+ if (ctx.internalLinks < 3) {
290
+ return createResult({ id: 'links-few-internal', name: 'Pages with Few Internal Links', category: 'links', severity: 'warning' }, 'warn', `Only ${ctx.internalLinks} internal link(s)`, {
291
+ value: ctx.internalLinks,
292
+ recommendation: 'Add more internal links to improve site navigation',
293
+ evidence: {
294
+ found: ctx.internalLinks,
295
+ expected: '3 or more internal links',
296
+ impact: 'Few internal links isolate pages and hurt discoverability',
297
+ },
298
+ });
299
+ }
300
+ return null;
301
+ },
302
+ },
303
+ {
304
+ id: 'links-orphan-page',
305
+ name: 'Orphan Page Detection',
306
+ category: 'links',
307
+ severity: 'warning',
308
+ description: 'Pages should have incoming internal links',
309
+ check: (ctx) => {
310
+ if (ctx.incomingInternalLinks === undefined)
311
+ return null;
312
+ if (ctx.isStartPage)
313
+ return null;
314
+ if (ctx.incomingInternalLinks === 0) {
315
+ return createResult({ id: 'links-orphan-page', name: 'Orphan Page Detection', category: 'links', severity: 'warning' }, 'warn', 'Page has no incoming internal links (orphan)', {
316
+ value: 0,
317
+ recommendation: 'Add internal links pointing to this page',
318
+ evidence: {
319
+ impact: 'Orphan pages are hard to discover and may not be indexed',
320
+ },
321
+ });
322
+ }
323
+ return null;
324
+ },
325
+ },
326
+ {
327
+ id: 'links-click-depth',
328
+ name: 'Page Click Depth',
329
+ category: 'links',
330
+ severity: 'warning',
331
+ description: 'Important pages should be within 3 clicks from homepage',
332
+ check: (ctx) => {
333
+ if (ctx.clickDepth === undefined)
334
+ return null;
335
+ if (ctx.clickDepth > 3) {
336
+ return createResult({ id: 'links-click-depth', name: 'Page Click Depth', category: 'links', severity: 'warning' }, 'warn', `Page is ${ctx.clickDepth} clicks from homepage`, {
337
+ value: ctx.clickDepth,
338
+ recommendation: 'Improve site structure to keep pages within 3 clicks',
339
+ evidence: {
340
+ found: `${ctx.clickDepth} clicks`,
341
+ expected: '3 clicks or less',
342
+ impact: 'Deep pages are harder to discover and get less link equity',
343
+ },
344
+ });
345
+ }
346
+ return createResult({ id: 'links-click-depth', name: 'Page Click Depth', category: 'links', severity: 'warning' }, 'pass', `Page is ${ctx.clickDepth} click(s) from homepage`, { value: ctx.clickDepth });
347
+ },
348
+ },
349
+ {
350
+ id: 'links-external-ratio',
351
+ name: 'External to Internal Link Ratio',
352
+ category: 'links',
353
+ severity: 'info',
354
+ description: 'Pages should have more internal than external links',
355
+ check: (ctx) => {
356
+ if (ctx.internalLinks === undefined || ctx.externalLinks === undefined)
357
+ return null;
358
+ if (ctx.totalLinks === undefined || ctx.totalLinks === 0)
359
+ return null;
360
+ const externalRatio = (ctx.externalLinks / ctx.totalLinks) * 100;
361
+ if (externalRatio > 70) {
362
+ return createResult({ id: 'links-external-ratio', name: 'External to Internal Link Ratio', category: 'links', severity: 'info' }, 'info', `High external link ratio (${Math.round(externalRatio)}%)`, {
363
+ value: Math.round(externalRatio),
364
+ recommendation: 'Add more internal links to balance link distribution',
365
+ evidence: {
366
+ found: `${ctx.externalLinks} external / ${ctx.internalLinks} internal`,
367
+ expected: 'More internal than external links',
368
+ impact: 'High external ratio may leak PageRank',
369
+ },
370
+ });
371
+ }
372
+ return null;
373
+ },
374
+ },
375
+ {
376
+ id: 'links-nofollow-internal',
377
+ name: 'Internal Nofollow Links',
378
+ category: 'links',
379
+ severity: 'info',
380
+ description: 'Internal links with nofollow prevent PageRank flow',
381
+ check: (ctx) => {
382
+ if (!ctx.nofollowInternalLinks || ctx.nofollowInternalLinks === 0)
383
+ return null;
384
+ return createResult({ id: 'links-nofollow-internal', name: 'Internal Nofollow Links', category: 'links', severity: 'info' }, 'info', `${ctx.nofollowInternalLinks} internal link(s) with nofollow`, {
385
+ value: ctx.nofollowInternalLinks,
386
+ recommendation: 'Remove nofollow from internal links unless intentional',
387
+ evidence: {
388
+ impact: 'Nofollow internal links waste PageRank',
389
+ },
390
+ });
391
+ },
392
+ },
393
+ {
394
+ id: 'excessive-links',
395
+ name: 'Excessive Links on Page',
396
+ category: 'links',
397
+ severity: 'warning',
398
+ description: 'Pages should not have more than 3,000 links',
399
+ check: (ctx) => {
400
+ if (ctx.totalLinks === undefined)
401
+ return null;
402
+ if (ctx.totalLinks > 3000) {
403
+ return createResult({ id: 'excessive-links', name: 'Excessive Links on Page', category: 'links', severity: 'warning' }, 'fail', `Page has ${ctx.totalLinks} links (exceeds 3,000 limit)`, {
404
+ value: ctx.totalLinks,
405
+ recommendation: 'Reduce the number of links to improve crawlability and page quality',
406
+ evidence: {
407
+ found: ctx.totalLinks,
408
+ expected: '<3,000 links',
409
+ impact: 'Search engines may consider pages with too many links as low-quality or spammy'
410
+ }
411
+ });
412
+ }
413
+ if (ctx.totalLinks > 1000) {
414
+ return createResult({ id: 'excessive-links', name: 'Excessive Links on Page', category: 'links', severity: 'warning' }, 'warn', `Page has ${ctx.totalLinks} links (consider reducing)`, {
415
+ value: ctx.totalLinks,
416
+ recommendation: 'Review and remove unnecessary links',
417
+ evidence: {
418
+ found: ctx.totalLinks,
419
+ expected: '<1,000 links recommended',
420
+ impact: 'Many links dilute page authority and can slow page load'
421
+ }
422
+ });
423
+ }
424
+ return null;
425
+ },
426
+ },
427
+ {
428
+ id: 'links-to-resources',
429
+ name: 'Links to Resources',
430
+ category: 'links',
431
+ severity: 'info',
432
+ description: 'Links should point to pages, not raw resources like images',
433
+ check: (ctx) => {
434
+ if (ctx.linksToResources === undefined)
435
+ return null;
436
+ if (ctx.linksToResources > 0) {
437
+ return createResult({ id: 'links-to-resources', name: 'Links to Resources', category: 'links', severity: 'info' }, 'info', `${ctx.linksToResources} links point directly to resources (images, PDFs)`, {
438
+ value: ctx.linksToResources,
439
+ recommendation: 'Use appropriate tags (<img>, <embed>) instead of <a> for resources',
440
+ evidence: {
441
+ found: ctx.resourceLinkUrls?.slice(0, 5) || [],
442
+ impact: 'Links to raw resources may confuse crawlers about site architecture'
443
+ }
444
+ });
445
+ }
446
+ return null;
447
+ },
448
+ },
449
+ {
450
+ id: 'links-403-forbidden',
451
+ name: '403 Forbidden Links',
452
+ category: 'links',
453
+ severity: 'warning',
454
+ description: 'External links should not return 403 Forbidden',
455
+ check: (ctx) => {
456
+ if (ctx.forbidden403Links === undefined)
457
+ return null;
458
+ if (ctx.forbidden403Links > 0) {
459
+ return createResult({ id: 'links-403-forbidden', name: '403 Forbidden Links', category: 'links', severity: 'warning' }, 'warn', `${ctx.forbidden403Links} external links return 403 Forbidden`, {
460
+ value: ctx.forbidden403Links,
461
+ recommendation: 'Replace or remove links that return 403 errors',
462
+ evidence: {
463
+ found: ctx.forbidden403LinkUrls?.slice(0, 5) || [],
464
+ impact: 'Forbidden links negatively affect user experience'
465
+ }
466
+ });
467
+ }
468
+ return null;
469
+ },
470
+ },
150
471
  ];
@@ -520,4 +520,28 @@ export const metaRules = [
520
520
  return null;
521
521
  },
522
522
  },
523
+ {
524
+ id: 'title-too-short',
525
+ name: 'Title Too Short',
526
+ category: 'title',
527
+ severity: 'warning',
528
+ description: 'Title should have at least 10 characters for SEO value',
529
+ check: (ctx) => {
530
+ if (!ctx.title)
531
+ return null;
532
+ const len = ctx.titleLength ?? ctx.title.length;
533
+ if (len <= 10) {
534
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'warn', `Title is very short (${len} chars)`, {
535
+ value: len,
536
+ recommendation: 'Add more descriptive text to your title (50-60 chars ideal)',
537
+ evidence: {
538
+ found: `${len} characters`,
539
+ expected: 'At least 10 characters, ideally 50-60',
540
+ impact: 'Short titles do not provide enough information about the page and limit keyword potential'
541
+ }
542
+ });
543
+ }
544
+ return null;
545
+ },
546
+ },
523
547
  ];
@@ -48,24 +48,4 @@ export const mobileRules = [
48
48
  return createResult({ id: 'viewport-scalable', name: 'Viewport Scalable', category: 'mobile', severity: 'warning' }, 'pass', 'Viewport allows user scaling');
49
49
  },
50
50
  },
51
- {
52
- id: 'mobile-font-size',
53
- name: 'Mobile Font Size',
54
- category: 'mobile',
55
- severity: 'info',
56
- description: 'Content should use readable font sizes on mobile (at least 16px base)',
57
- check: (ctx) => {
58
- return createResult({ id: 'mobile-font-size', name: 'Mobile Font Size', category: 'mobile', severity: 'info' }, 'info', 'Font size check requires CSS analysis', { recommendation: 'Use a minimum base font size of 16px for mobile readability' });
59
- },
60
- },
61
- {
62
- id: 'mobile-tap-targets',
63
- name: 'Mobile Tap Targets',
64
- category: 'mobile',
65
- severity: 'info',
66
- description: 'Interactive elements should be at least 48x48px for touch accessibility',
67
- check: (ctx) => {
68
- return createResult({ id: 'mobile-tap-targets', name: 'Mobile Tap Targets', category: 'mobile', severity: 'info' }, 'info', 'Tap target size check requires CSS analysis', { recommendation: 'Ensure buttons and links are at least 48x48px with 8px spacing for mobile usability' });
69
- },
70
- },
71
51
  ];
@@ -243,4 +243,128 @@ export const performanceRules = [
243
243
  return null;
244
244
  },
245
245
  },
246
+ {
247
+ id: 'browser-caching',
248
+ name: 'Browser Caching',
249
+ category: 'performance',
250
+ severity: 'warning',
251
+ description: 'Static resources should have browser caching enabled',
252
+ check: (ctx) => {
253
+ if (ctx.resourcesWithoutCaching === undefined)
254
+ return null;
255
+ if (ctx.resourcesWithoutCaching > 0) {
256
+ return createResult({ id: 'browser-caching', name: 'Browser Caching', category: 'performance', severity: 'warning' }, 'warn', `${ctx.resourcesWithoutCaching} resources without cache headers`, {
257
+ value: ctx.resourcesWithoutCaching,
258
+ recommendation: 'Enable browser caching with Cache-Control or Expires headers',
259
+ evidence: {
260
+ found: `${ctx.resourcesWithoutCaching} uncached resources`,
261
+ expected: 'All static resources should have cache headers',
262
+ impact: 'Without caching, browsers download resources repeatedly, increasing load time',
263
+ learnMore: 'https://web.dev/uses-long-cache-ttl/'
264
+ }
265
+ });
266
+ }
267
+ return createResult({ id: 'browser-caching', name: 'Browser Caching', category: 'performance', severity: 'warning' }, 'pass', 'Resources have proper cache headers');
268
+ },
269
+ },
270
+ {
271
+ id: 'js-css-total-size',
272
+ name: 'JS/CSS Total Size',
273
+ category: 'performance',
274
+ severity: 'warning',
275
+ description: 'Total JS and CSS size should not exceed 2MB',
276
+ check: (ctx) => {
277
+ const jsSize = ctx.jsTotalSize || 0;
278
+ const cssSize = ctx.cssTotalSize || 0;
279
+ const totalSize = jsSize + cssSize;
280
+ if (totalSize === 0)
281
+ return null;
282
+ const sizeMb = totalSize / (1024 * 1024);
283
+ if (sizeMb > 2) {
284
+ return createResult({ id: 'js-css-total-size', name: 'JS/CSS Total Size', category: 'performance', severity: 'warning' }, 'fail', `JS/CSS total ${sizeMb.toFixed(2)}MB exceeds 2MB limit`, {
285
+ value: totalSize,
286
+ recommendation: 'Reduce JS/CSS file sizes through minification and code splitting',
287
+ evidence: {
288
+ found: `${sizeMb.toFixed(2)}MB (JS: ${(jsSize / 1024).toFixed(0)}KB, CSS: ${(cssSize / 1024).toFixed(0)}KB)`,
289
+ expected: '<2MB total',
290
+ impact: 'Large JS/CSS files significantly increase page load time',
291
+ learnMore: 'https://web.dev/total-byte-weight/'
292
+ }
293
+ });
294
+ }
295
+ if (sizeMb > 1) {
296
+ return createResult({ id: 'js-css-total-size', name: 'JS/CSS Total Size', category: 'performance', severity: 'warning' }, 'warn', `JS/CSS total ${sizeMb.toFixed(2)}MB is large`, {
297
+ value: totalSize,
298
+ recommendation: 'Consider reducing JS/CSS bundle sizes',
299
+ evidence: {
300
+ found: `${sizeMb.toFixed(2)}MB`,
301
+ expected: '<1MB recommended',
302
+ impact: 'Large bundles increase load time, especially on mobile'
303
+ }
304
+ });
305
+ }
306
+ return null;
307
+ },
308
+ },
309
+ {
310
+ id: 'excessive-js-css-files',
311
+ name: 'Excessive JS/CSS Files',
312
+ category: 'performance',
313
+ severity: 'warning',
314
+ description: 'Pages should not load more than 100 JS/CSS files',
315
+ check: (ctx) => {
316
+ const jsCount = ctx.jsFilesCount || 0;
317
+ const cssCount = ctx.cssFilesCount || 0;
318
+ const totalFiles = jsCount + cssCount;
319
+ if (totalFiles === 0)
320
+ return null;
321
+ if (totalFiles > 100) {
322
+ return createResult({ id: 'excessive-js-css-files', name: 'Excessive JS/CSS Files', category: 'performance', severity: 'warning' }, 'fail', `Page loads ${totalFiles} JS/CSS files (exceeds 100)`, {
323
+ value: totalFiles,
324
+ recommendation: 'Combine files or use bundling to reduce HTTP requests',
325
+ evidence: {
326
+ found: `${totalFiles} files (JS: ${jsCount}, CSS: ${cssCount})`,
327
+ expected: '<100 files',
328
+ impact: 'Each file requires a separate HTTP request, increasing page load time'
329
+ }
330
+ });
331
+ }
332
+ if (totalFiles > 50) {
333
+ return createResult({ id: 'excessive-js-css-files', name: 'Excessive JS/CSS Files', category: 'performance', severity: 'warning' }, 'warn', `Page loads ${totalFiles} JS/CSS files`, {
334
+ value: totalFiles,
335
+ recommendation: 'Consider bundling resources to reduce requests',
336
+ evidence: {
337
+ found: `${totalFiles} files`,
338
+ expected: '<50 files recommended',
339
+ impact: 'Many files increase connection overhead'
340
+ }
341
+ });
342
+ }
343
+ return null;
344
+ },
345
+ },
346
+ {
347
+ id: 'unminified-resources',
348
+ name: 'Unminified Resources',
349
+ category: 'performance',
350
+ severity: 'warning',
351
+ description: 'JS and CSS files should be minified',
352
+ check: (ctx) => {
353
+ if (ctx.unminifiedResources === undefined)
354
+ return null;
355
+ if (ctx.unminifiedResources > 0) {
356
+ return createResult({ id: 'unminified-resources', name: 'Unminified Resources', category: 'performance', severity: 'warning' }, 'warn', `${ctx.unminifiedResources} unminified JS/CSS files detected`, {
357
+ value: ctx.unminifiedResources,
358
+ recommendation: 'Minify JavaScript and CSS files to reduce file size',
359
+ evidence: {
360
+ found: `${ctx.unminifiedResources} unminified files`,
361
+ expected: 'All JS/CSS files should be minified',
362
+ impact: 'Minification reduces file size by removing whitespace and comments',
363
+ learnMore: 'https://web.dev/minify-css/'
364
+ }
365
+ });
366
+ }
367
+ return null;
368
+ },
369
+ },
246
370
  ];
@@ -0,0 +1,16 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const redirectRules: SeoRule[];
3
+ declare module './types.js' {
4
+ interface RuleContext {
5
+ redirectChain?: Array<{
6
+ status: number;
7
+ from: string;
8
+ to: string;
9
+ }>;
10
+ wwwConsistency?: {
11
+ canonicalHasWww: boolean;
12
+ urlHasWww: boolean;
13
+ redirectsToCanonical: boolean;
14
+ };
15
+ }
16
+ }