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
@@ -522,4 +522,294 @@ export const securityRules = [
522
522
  return createResult({ id: 'security-corp', name: 'Cross-Origin-Resource-Policy', category: 'security', severity: 'info' }, 'pass', `CORP: ${corpHeader}`);
523
523
  },
524
524
  },
525
+ {
526
+ id: 'security-ssl-valid',
527
+ name: 'SSL Certificate Valid',
528
+ category: 'security',
529
+ severity: 'error',
530
+ description: 'SSL certificate must be valid and not expired',
531
+ check: (ctx) => {
532
+ if (ctx.sslCertificate === undefined)
533
+ return null;
534
+ if (!ctx.sslCertificate.valid) {
535
+ return createResult({ id: 'security-ssl-valid', name: 'SSL Certificate Valid', category: 'security', severity: 'error' }, 'fail', 'SSL certificate is invalid', {
536
+ recommendation: 'Renew or replace the SSL certificate immediately',
537
+ evidence: {
538
+ found: ctx.sslCertificate.error || 'Invalid certificate',
539
+ impact: 'Browsers will show security warnings, users may not trust the site',
540
+ },
541
+ });
542
+ }
543
+ return createResult({ id: 'security-ssl-valid', name: 'SSL Certificate Valid', category: 'security', severity: 'error' }, 'pass', 'SSL certificate is valid');
544
+ },
545
+ },
546
+ {
547
+ id: 'security-ssl-expiry',
548
+ name: 'SSL Certificate Expiry',
549
+ category: 'security',
550
+ severity: 'warning',
551
+ description: 'SSL certificate should not expire within 30 days',
552
+ check: (ctx) => {
553
+ if (!ctx.sslCertificate?.expiryDate)
554
+ return null;
555
+ const expiryDate = new Date(ctx.sslCertificate.expiryDate);
556
+ const now = new Date();
557
+ const daysUntilExpiry = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
558
+ if (daysUntilExpiry < 0) {
559
+ return createResult({ id: 'security-ssl-expiry', name: 'SSL Certificate Expiry', category: 'security', severity: 'warning' }, 'fail', 'SSL certificate has expired', {
560
+ recommendation: 'Renew the SSL certificate immediately',
561
+ evidence: {
562
+ found: `Expired ${Math.abs(daysUntilExpiry)} days ago`,
563
+ impact: 'Site is showing security warnings to all visitors',
564
+ },
565
+ });
566
+ }
567
+ if (daysUntilExpiry < 7) {
568
+ return createResult({ id: 'security-ssl-expiry', name: 'SSL Certificate Expiry', category: 'security', severity: 'warning' }, 'fail', `SSL certificate expires in ${daysUntilExpiry} days`, {
569
+ recommendation: 'Renew the SSL certificate urgently',
570
+ evidence: {
571
+ found: `Expires: ${expiryDate.toISOString().split('T')[0]}`,
572
+ impact: 'Certificate will expire very soon',
573
+ },
574
+ });
575
+ }
576
+ if (daysUntilExpiry < 30) {
577
+ return createResult({ id: 'security-ssl-expiry', name: 'SSL Certificate Expiry', category: 'security', severity: 'warning' }, 'warn', `SSL certificate expires in ${daysUntilExpiry} days`, {
578
+ recommendation: 'Plan to renew the SSL certificate soon',
579
+ evidence: {
580
+ found: `Expires: ${expiryDate.toISOString().split('T')[0]}`,
581
+ },
582
+ });
583
+ }
584
+ return createResult({ id: 'security-ssl-expiry', name: 'SSL Certificate Expiry', category: 'security', severity: 'warning' }, 'pass', `SSL certificate valid for ${daysUntilExpiry} days`);
585
+ },
586
+ },
587
+ {
588
+ id: 'security-ssl-name-match',
589
+ name: 'SSL Certificate Name Match',
590
+ category: 'security',
591
+ severity: 'error',
592
+ description: 'SSL certificate CN/SAN must match the domain',
593
+ check: (ctx) => {
594
+ if (ctx.sslCertificate?.nameMismatch === undefined)
595
+ return null;
596
+ if (ctx.sslCertificate.nameMismatch) {
597
+ return createResult({ id: 'security-ssl-name-match', name: 'SSL Certificate Name Match', category: 'security', severity: 'error' }, 'fail', 'SSL certificate name mismatch', {
598
+ recommendation: 'Get a certificate that matches your domain name',
599
+ evidence: {
600
+ found: ctx.sslCertificate.commonName || 'Unknown',
601
+ expected: ctx.sslCertificate.expectedDomain || 'Domain name',
602
+ impact: 'Browsers will show certificate warning',
603
+ },
604
+ });
605
+ }
606
+ return createResult({ id: 'security-ssl-name-match', name: 'SSL Certificate Name Match', category: 'security', severity: 'error' }, 'pass', 'SSL certificate matches domain');
607
+ },
608
+ },
609
+ {
610
+ id: 'security-tls-version',
611
+ name: 'TLS Version',
612
+ category: 'security',
613
+ severity: 'error',
614
+ description: 'Server should use TLS 1.2 or higher',
615
+ check: (ctx) => {
616
+ if (!ctx.tlsVersion)
617
+ return null;
618
+ const version = ctx.tlsVersion;
619
+ const insecureVersions = ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.0', 'TLSv1.1'];
620
+ if (insecureVersions.includes(version)) {
621
+ return createResult({ id: 'security-tls-version', name: 'TLS Version', category: 'security', severity: 'error' }, 'fail', `Insecure TLS version: ${version}`, {
622
+ recommendation: 'Upgrade to TLS 1.2 or TLS 1.3',
623
+ evidence: {
624
+ found: version,
625
+ expected: 'TLSv1.2 or TLSv1.3',
626
+ impact: 'Old TLS versions have known vulnerabilities',
627
+ },
628
+ });
629
+ }
630
+ return createResult({ id: 'security-tls-version', name: 'TLS Version', category: 'security', severity: 'error' }, 'pass', `Using ${version}`);
631
+ },
632
+ },
633
+ {
634
+ id: 'security-ssl-issuer',
635
+ name: 'SSL Certificate Issuer',
636
+ category: 'security',
637
+ severity: 'info',
638
+ description: 'SSL certificate should be from a trusted CA',
639
+ check: (ctx) => {
640
+ if (!ctx.sslCertificate?.issuer)
641
+ return null;
642
+ const issuer = ctx.sslCertificate.issuer;
643
+ const selfSigned = issuer.toLowerCase().includes('self-signed') ||
644
+ ctx.sslCertificate.selfSigned === true;
645
+ if (selfSigned) {
646
+ return createResult({ id: 'security-ssl-issuer', name: 'SSL Certificate Issuer', category: 'security', severity: 'info' }, 'warn', 'Self-signed SSL certificate detected', {
647
+ recommendation: 'Use a certificate from a trusted Certificate Authority',
648
+ evidence: {
649
+ found: issuer,
650
+ impact: 'Self-signed certificates show warnings in browsers',
651
+ },
652
+ });
653
+ }
654
+ return createResult({ id: 'security-ssl-issuer', name: 'SSL Certificate Issuer', category: 'security', severity: 'info' }, 'pass', `Certificate issued by: ${issuer}`);
655
+ },
656
+ },
657
+ {
658
+ id: 'security-password-on-http',
659
+ name: 'Password Fields on HTTP',
660
+ category: 'security',
661
+ severity: 'error',
662
+ description: 'Password fields must only appear on HTTPS pages',
663
+ check: (ctx) => {
664
+ if (ctx.hasPasswordField === undefined)
665
+ return null;
666
+ if (ctx.hasPasswordField && ctx.isHttps === false) {
667
+ return createResult({ id: 'security-password-on-http', name: 'Password Fields on HTTP', category: 'security', severity: 'error' }, 'fail', 'Password field on insecure HTTP page', {
668
+ recommendation: 'Enable HTTPS for all pages with password fields',
669
+ evidence: {
670
+ impact: 'Passwords sent over HTTP can be intercepted',
671
+ },
672
+ });
673
+ }
674
+ if (ctx.hasPasswordField && ctx.isHttps === true) {
675
+ return createResult({ id: 'security-password-on-http', name: 'Password Fields on HTTP', category: 'security', severity: 'error' }, 'pass', 'Password fields are on HTTPS');
676
+ }
677
+ return null;
678
+ },
679
+ },
680
+ {
681
+ id: 'security-forms-on-http',
682
+ name: 'Forms on HTTP',
683
+ category: 'security',
684
+ severity: 'warning',
685
+ description: 'Forms should only submit to HTTPS endpoints',
686
+ check: (ctx) => {
687
+ if (ctx.formsOnHttp === undefined)
688
+ return null;
689
+ if (ctx.formsOnHttp > 0) {
690
+ return createResult({ id: 'security-forms-on-http', name: 'Forms on HTTP', category: 'security', severity: 'warning' }, 'warn', `${ctx.formsOnHttp} form(s) submit to HTTP`, {
691
+ value: ctx.formsOnHttp,
692
+ recommendation: 'Update form actions to use HTTPS',
693
+ evidence: {
694
+ impact: 'Form data sent over HTTP can be intercepted',
695
+ },
696
+ });
697
+ }
698
+ return createResult({ id: 'security-forms-on-http', name: 'Forms on HTTP', category: 'security', severity: 'warning' }, 'pass', 'All forms submit to HTTPS');
699
+ },
700
+ },
701
+ {
702
+ id: 'security-server-disclosure',
703
+ name: 'Server Version Disclosure',
704
+ category: 'security',
705
+ severity: 'info',
706
+ description: 'Server header should not reveal detailed version information',
707
+ check: (ctx) => {
708
+ if (!ctx.responseHeaders)
709
+ return null;
710
+ const serverHeader = ctx.responseHeaders['server'] || ctx.responseHeaders['Server'];
711
+ if (!serverHeader)
712
+ return null;
713
+ const server = String(serverHeader);
714
+ if (/\d+\.\d+/.test(server)) {
715
+ return createResult({ id: 'security-server-disclosure', name: 'Server Version Disclosure', category: 'security', severity: 'info' }, 'info', `Server header reveals version: ${server}`, {
716
+ recommendation: 'Configure server to hide version information',
717
+ evidence: {
718
+ found: server,
719
+ impact: 'Attackers can target known vulnerabilities',
720
+ },
721
+ });
722
+ }
723
+ return null;
724
+ },
725
+ },
726
+ {
727
+ id: 'security-x-powered-by',
728
+ name: 'X-Powered-By Header',
729
+ category: 'security',
730
+ severity: 'info',
731
+ description: 'X-Powered-By header reveals technology stack',
732
+ check: (ctx) => {
733
+ if (!ctx.responseHeaders)
734
+ return null;
735
+ const xPoweredBy = ctx.responseHeaders['x-powered-by'] || ctx.responseHeaders['X-Powered-By'];
736
+ if (xPoweredBy) {
737
+ return createResult({ id: 'security-x-powered-by', name: 'X-Powered-By Header', category: 'security', severity: 'info' }, 'info', `X-Powered-By header present: ${xPoweredBy}`, {
738
+ recommendation: 'Remove X-Powered-By header to reduce attack surface',
739
+ evidence: {
740
+ found: String(xPoweredBy),
741
+ impact: 'Reveals technology stack to potential attackers',
742
+ },
743
+ });
744
+ }
745
+ return null;
746
+ },
747
+ },
748
+ {
749
+ id: 'ssl-sni-support',
750
+ name: 'SNI Support',
751
+ category: 'security',
752
+ severity: 'info',
753
+ description: 'Server should support Server Name Indication (SNI)',
754
+ check: (ctx) => {
755
+ if (ctx.sniSupported === undefined)
756
+ return null;
757
+ if (!ctx.sniSupported) {
758
+ return createResult({ id: 'ssl-sni-support', name: 'SNI Support', category: 'security', severity: 'info' }, 'info', 'Server may not support SNI', {
759
+ recommendation: 'Ensure web server supports SNI for proper HTTPS functionality',
760
+ evidence: {
761
+ impact: 'Some older browsers may have issues with SSL certificates without SNI support'
762
+ }
763
+ });
764
+ }
765
+ return null;
766
+ },
767
+ },
768
+ {
769
+ id: 'sitemap-https-urls',
770
+ name: 'HTTPS URLs in Sitemap',
771
+ category: 'security',
772
+ severity: 'warning',
773
+ description: 'Sitemap should only contain HTTPS URLs',
774
+ check: (ctx) => {
775
+ if (ctx.sitemapHttpUrls === undefined)
776
+ return null;
777
+ if (ctx.sitemapHttpUrls > 0) {
778
+ return createResult({ id: 'sitemap-https-urls', name: 'HTTPS URLs in Sitemap', category: 'security', severity: 'warning' }, 'warn', `Sitemap contains ${ctx.sitemapHttpUrls} HTTP URLs`, {
779
+ value: ctx.sitemapHttpUrls,
780
+ recommendation: 'Replace all HTTP URLs in sitemap.xml with HTTPS versions',
781
+ evidence: {
782
+ found: `${ctx.sitemapHttpUrls} HTTP URLs`,
783
+ expected: 'All URLs should use HTTPS',
784
+ impact: 'HTTP URLs in sitemap can cause mixed content issues and indexing confusion'
785
+ }
786
+ });
787
+ }
788
+ return createResult({ id: 'sitemap-https-urls', name: 'HTTPS URLs in Sitemap', category: 'security', severity: 'warning' }, 'pass', 'All sitemap URLs use HTTPS');
789
+ },
790
+ },
791
+ {
792
+ id: 'security-hsts',
793
+ name: 'HSTS Header',
794
+ category: 'security',
795
+ severity: 'info',
796
+ description: 'HTTPS sites should implement HSTS for security',
797
+ check: (ctx) => {
798
+ if (!ctx.isHttps)
799
+ return null;
800
+ if (ctx.hasHsts === undefined)
801
+ return null;
802
+ if (!ctx.hasHsts) {
803
+ return createResult({ id: 'security-hsts', name: 'HSTS Header', category: 'security', severity: 'info' }, 'info', 'Missing Strict-Transport-Security header', {
804
+ recommendation: 'Implement HSTS to enforce HTTPS connections',
805
+ evidence: {
806
+ expected: 'Strict-Transport-Security: max-age=31536000; includeSubDomains',
807
+ impact: 'Without HSTS, browsers may still attempt insecure HTTP connections',
808
+ learnMore: 'https://web.dev/strict-transport-security/'
809
+ }
810
+ });
811
+ }
812
+ return createResult({ id: 'security-hsts', name: 'HSTS Header', category: 'security', severity: 'info' }, 'pass', 'HSTS header is present');
813
+ },
814
+ },
525
815
  ];
@@ -0,0 +1,10 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const technicalAdvancedRules: SeoRule[];
3
+ declare module './types.js' {
4
+ interface RuleContext {
5
+ metaRefresh?: {
6
+ delay: number;
7
+ url?: string;
8
+ };
9
+ }
10
+ }
@@ -0,0 +1,283 @@
1
+ import { createResult } from './types.js';
2
+ export const technicalAdvancedRules = [
3
+ {
4
+ id: 'meta-refresh-redirect',
5
+ name: 'Meta Refresh Redirect',
6
+ category: 'technical',
7
+ severity: 'warning',
8
+ description: 'Pages should not use meta refresh redirects',
9
+ check: (ctx) => {
10
+ if (!ctx.metaRefresh)
11
+ return null;
12
+ const { delay, url } = ctx.metaRefresh;
13
+ if (url) {
14
+ return createResult({ id: 'meta-refresh-redirect', name: 'Meta Refresh Redirect', category: 'technical', severity: 'warning' }, delay === 0 ? 'warn' : 'fail', `Page uses meta refresh redirect (${delay}s delay)`, {
15
+ value: delay,
16
+ recommendation: 'Use HTTP 301/302 redirects instead of meta refresh for SEO',
17
+ evidence: {
18
+ found: `<meta http-equiv="refresh" content="${delay};url=${url}">`,
19
+ expected: 'HTTP 301/302 redirect',
20
+ impact: delay > 0
21
+ ? 'Meta refresh with delay confuses users and search engines'
22
+ : 'Meta refresh redirects are not as SEO-friendly as HTTP redirects',
23
+ learnMore: 'https://developers.google.com/search/docs/crawling-indexing/http-network-errors#meta-refresh'
24
+ }
25
+ });
26
+ }
27
+ else if (delay > 0) {
28
+ return createResult({ id: 'meta-refresh-redirect', name: 'Meta Refresh Redirect', category: 'technical', severity: 'warning' }, 'warn', `Page auto-refreshes every ${delay} seconds`, {
29
+ value: delay,
30
+ recommendation: 'Avoid auto-refresh; let users control when to refresh content',
31
+ evidence: {
32
+ found: `<meta http-equiv="refresh" content="${delay}">`,
33
+ expected: 'No auto-refresh',
34
+ impact: 'Auto-refresh can be disorienting and wastes bandwidth'
35
+ }
36
+ });
37
+ }
38
+ return null;
39
+ },
40
+ },
41
+ {
42
+ id: 'html-page-size',
43
+ name: 'HTML Page Size',
44
+ category: 'performance',
45
+ severity: 'warning',
46
+ description: 'HTML page should not exceed reasonable size limits',
47
+ check: (ctx) => {
48
+ if (ctx.htmlSize === undefined)
49
+ return null;
50
+ const sizeKb = ctx.htmlSize / 1024;
51
+ const sizeMb = sizeKb / 1024;
52
+ if (sizeMb > 2) {
53
+ return createResult({ id: 'html-page-size', name: 'HTML Page Size', category: 'performance', severity: 'warning' }, 'fail', `HTML size ${sizeMb.toFixed(2)}MB exceeds 2MB limit`, {
54
+ value: ctx.htmlSize,
55
+ recommendation: 'Reduce HTML size by removing inline scripts/styles and optimizing content',
56
+ evidence: {
57
+ found: `${sizeMb.toFixed(2)}MB`,
58
+ expected: '<2MB',
59
+ impact: 'Very large HTML files slow down crawling and may not be fully indexed'
60
+ }
61
+ });
62
+ }
63
+ if (sizeKb > 500) {
64
+ return createResult({ id: 'html-page-size', name: 'HTML Page Size', category: 'performance', severity: 'warning' }, 'warn', `HTML size ${sizeKb.toFixed(0)}KB is large`, {
65
+ value: ctx.htmlSize,
66
+ recommendation: 'Consider reducing HTML size for faster page loads',
67
+ evidence: {
68
+ found: `${sizeKb.toFixed(0)}KB`,
69
+ expected: '<500KB',
70
+ impact: 'Large HTML files increase time to first meaningful paint'
71
+ }
72
+ });
73
+ }
74
+ return createResult({ id: 'html-page-size', name: 'HTML Page Size', category: 'performance', severity: 'warning' }, 'pass', `HTML size ${sizeKb.toFixed(0)}KB is acceptable`);
75
+ },
76
+ },
77
+ {
78
+ id: 'total-page-size',
79
+ name: 'Total Page Size',
80
+ category: 'performance',
81
+ severity: 'warning',
82
+ description: 'Total page weight should be optimized for performance',
83
+ check: (ctx) => {
84
+ if (ctx.totalPageSize === undefined)
85
+ return null;
86
+ const sizeMb = ctx.totalPageSize / (1024 * 1024);
87
+ if (sizeMb > 5) {
88
+ return createResult({ id: 'total-page-size', name: 'Total Page Size', category: 'performance', severity: 'warning' }, 'fail', `Total page size ${sizeMb.toFixed(2)}MB exceeds 5MB`, {
89
+ value: ctx.totalPageSize,
90
+ recommendation: 'Optimize images, defer scripts, and reduce overall page weight',
91
+ evidence: {
92
+ found: `${sizeMb.toFixed(2)}MB`,
93
+ expected: '<5MB',
94
+ impact: 'Very large pages significantly impact mobile users and Core Web Vitals'
95
+ }
96
+ });
97
+ }
98
+ if (sizeMb > 3) {
99
+ return createResult({ id: 'total-page-size', name: 'Total Page Size', category: 'performance', severity: 'warning' }, 'warn', `Total page size ${sizeMb.toFixed(2)}MB is large`, {
100
+ value: ctx.totalPageSize,
101
+ recommendation: 'Consider optimizing resources to improve load times',
102
+ evidence: {
103
+ found: `${sizeMb.toFixed(2)}MB`,
104
+ expected: '<3MB',
105
+ impact: 'Large pages increase bounce rate, especially on mobile'
106
+ }
107
+ });
108
+ }
109
+ return createResult({ id: 'total-page-size', name: 'Total Page Size', category: 'performance', severity: 'warning' }, 'pass', `Total page size ${sizeMb.toFixed(2)}MB is acceptable`);
110
+ },
111
+ },
112
+ {
113
+ id: 'server-response-time',
114
+ name: 'Server Response Time',
115
+ category: 'performance',
116
+ severity: 'warning',
117
+ description: 'Server should respond within acceptable time limits',
118
+ check: (ctx) => {
119
+ const ttfb = ctx.timings?.ttfb;
120
+ if (ttfb === undefined)
121
+ return null;
122
+ if (ttfb > 5000) {
123
+ return createResult({ id: 'server-response-time', name: 'Server Response Time', category: 'performance', severity: 'warning' }, 'fail', `TTFB ${(ttfb / 1000).toFixed(2)}s exceeds 5s`, {
124
+ value: ttfb,
125
+ recommendation: 'Investigate server performance, caching, and database queries',
126
+ evidence: {
127
+ found: `${(ttfb / 1000).toFixed(2)}s`,
128
+ expected: '<5s',
129
+ impact: 'Very slow server response leads to timeout errors and poor UX'
130
+ }
131
+ });
132
+ }
133
+ if (ttfb > 2000) {
134
+ return createResult({ id: 'server-response-time', name: 'Server Response Time', category: 'performance', severity: 'warning' }, 'warn', `TTFB ${(ttfb / 1000).toFixed(2)}s is slow`, {
135
+ value: ttfb,
136
+ recommendation: 'Optimize server response time for better Core Web Vitals',
137
+ evidence: {
138
+ found: `${(ttfb / 1000).toFixed(2)}s`,
139
+ expected: '<2s (ideally <600ms)',
140
+ impact: 'Slow server response negatively affects LCP and user experience'
141
+ }
142
+ });
143
+ }
144
+ return createResult({ id: 'server-response-time', name: 'Server Response Time', category: 'performance', severity: 'warning' }, 'pass', `TTFB ${ttfb}ms is good`);
145
+ },
146
+ },
147
+ {
148
+ id: 'url-length',
149
+ name: 'URL Length',
150
+ category: 'technical',
151
+ severity: 'warning',
152
+ description: 'URLs should not be excessively long',
153
+ check: (ctx) => {
154
+ if (!ctx.url)
155
+ return null;
156
+ const length = ctx.url.length;
157
+ if (length > 2000) {
158
+ return createResult({ id: 'url-length', name: 'URL Length', category: 'technical', severity: 'warning' }, 'fail', `URL length ${length} chars exceeds browser limits`, {
159
+ value: length,
160
+ recommendation: 'Shorten URL to under 2000 characters',
161
+ evidence: {
162
+ found: `${length} characters`,
163
+ expected: '<2000 characters',
164
+ impact: 'URLs over 2000 characters may be truncated or rejected by browsers'
165
+ }
166
+ });
167
+ }
168
+ if (length > 200) {
169
+ return createResult({ id: 'url-length', name: 'URL Length', category: 'technical', severity: 'warning' }, 'warn', `URL length ${length} chars is long`, {
170
+ value: length,
171
+ recommendation: 'Consider using shorter, cleaner URLs for SEO',
172
+ evidence: {
173
+ found: `${length} characters`,
174
+ expected: '<200 characters for best SEO',
175
+ impact: 'Long URLs are harder to share and may be truncated in SERPs'
176
+ }
177
+ });
178
+ }
179
+ return createResult({ id: 'url-length', name: 'URL Length', category: 'technical', severity: 'warning' }, 'pass', `URL length ${length} chars is acceptable`);
180
+ },
181
+ },
182
+ {
183
+ id: 'url-special-chars',
184
+ name: 'URL Special Characters',
185
+ category: 'technical',
186
+ severity: 'warning',
187
+ description: 'URLs should not contain problematic special characters',
188
+ check: (ctx) => {
189
+ if (!ctx.url)
190
+ return null;
191
+ try {
192
+ const urlObj = new URL(ctx.url);
193
+ const path = urlObj.pathname + urlObj.search;
194
+ const issues = [];
195
+ if (/[A-Z]/.test(path)) {
196
+ issues.push('uppercase letters');
197
+ }
198
+ if (path.includes(' ') || path.includes('%20')) {
199
+ issues.push('spaces');
200
+ }
201
+ if (path.includes('_')) {
202
+ issues.push('underscores (use hyphens)');
203
+ }
204
+ if (/\/\//.test(path)) {
205
+ issues.push('double slashes');
206
+ }
207
+ if (/[àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ]/i.test(decodeURIComponent(path))) {
208
+ issues.push('accented characters');
209
+ }
210
+ if (issues.length > 0) {
211
+ return createResult({ id: 'url-special-chars', name: 'URL Special Characters', category: 'technical', severity: 'warning' }, 'warn', `URL contains: ${issues.join(', ')}`, {
212
+ recommendation: 'Use lowercase letters, hyphens, and avoid special characters',
213
+ evidence: {
214
+ found: issues,
215
+ expected: 'Lowercase letters, numbers, hyphens only',
216
+ impact: 'Non-standard URL characters can cause crawling and indexing issues'
217
+ }
218
+ });
219
+ }
220
+ return createResult({ id: 'url-special-chars', name: 'URL Special Characters', category: 'technical', severity: 'warning' }, 'pass', 'URL uses clean, SEO-friendly characters');
221
+ }
222
+ catch {
223
+ return createResult({ id: 'url-special-chars', name: 'URL Special Characters', category: 'technical', severity: 'warning' }, 'fail', 'Invalid URL syntax', {
224
+ recommendation: 'Fix URL syntax',
225
+ evidence: {
226
+ found: ctx.url,
227
+ expected: 'Valid URL',
228
+ impact: 'Invalid URLs cannot be crawled or indexed'
229
+ }
230
+ });
231
+ }
232
+ },
233
+ },
234
+ {
235
+ id: 'password-on-http',
236
+ name: 'Password Fields on HTTP',
237
+ category: 'security',
238
+ severity: 'error',
239
+ description: 'Pages with password fields must use HTTPS',
240
+ check: (ctx) => {
241
+ if (!ctx.hasPasswordField || ctx.isHttps === undefined)
242
+ return null;
243
+ if (ctx.hasPasswordField && !ctx.isHttps) {
244
+ return createResult({ id: 'password-on-http', name: 'Password Fields on HTTP', category: 'security', severity: 'error' }, 'fail', 'Password field detected on non-HTTPS page', {
245
+ recommendation: 'Serve login/registration pages over HTTPS only',
246
+ evidence: {
247
+ found: '<input type="password"> on HTTP',
248
+ expected: 'HTTPS for all pages with password fields',
249
+ impact: 'User credentials can be intercepted in transit',
250
+ learnMore: 'https://web.dev/is-on-https/'
251
+ }
252
+ });
253
+ }
254
+ if (ctx.hasPasswordField && ctx.isHttps) {
255
+ return createResult({ id: 'password-on-http', name: 'Password Fields on HTTP', category: 'security', severity: 'error' }, 'pass', 'Password field properly served over HTTPS');
256
+ }
257
+ return null;
258
+ },
259
+ },
260
+ {
261
+ id: 'forms-on-http',
262
+ name: 'Forms on HTTP',
263
+ category: 'security',
264
+ severity: 'warning',
265
+ description: 'Forms should submit data over HTTPS',
266
+ check: (ctx) => {
267
+ if (ctx.formsOnHttp === undefined)
268
+ return null;
269
+ if (ctx.formsOnHttp > 0) {
270
+ return createResult({ id: 'forms-on-http', name: 'Forms on HTTP', category: 'security', severity: 'warning' }, 'warn', `${ctx.formsOnHttp} form(s) submit to HTTP URLs`, {
271
+ value: ctx.formsOnHttp,
272
+ recommendation: 'Update form actions to use HTTPS URLs',
273
+ evidence: {
274
+ found: ctx.formsOnHttp,
275
+ expected: 0,
276
+ impact: 'Form data submitted over HTTP can be intercepted'
277
+ }
278
+ });
279
+ }
280
+ return null;
281
+ },
282
+ },
283
+ ];