recker 1.0.27 → 1.0.28-next.3bf98c7

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 (72) 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 +2 -0
  5. package/dist/cli/tui/shell.js +492 -1
  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/index.d.ts +2 -0
  10. package/dist/scrape/index.js +1 -0
  11. package/dist/scrape/spider.d.ts +61 -0
  12. package/dist/scrape/spider.js +250 -0
  13. package/dist/scrape/types.d.ts +2 -1
  14. package/dist/seo/analyzer.d.ts +42 -0
  15. package/dist/seo/analyzer.js +742 -0
  16. package/dist/seo/index.d.ts +7 -0
  17. package/dist/seo/index.js +3 -0
  18. package/dist/seo/rules/accessibility.d.ts +2 -0
  19. package/dist/seo/rules/accessibility.js +694 -0
  20. package/dist/seo/rules/best-practices.d.ts +2 -0
  21. package/dist/seo/rules/best-practices.js +188 -0
  22. package/dist/seo/rules/content.d.ts +2 -0
  23. package/dist/seo/rules/content.js +236 -0
  24. package/dist/seo/rules/crawl.d.ts +2 -0
  25. package/dist/seo/rules/crawl.js +307 -0
  26. package/dist/seo/rules/cwv.d.ts +2 -0
  27. package/dist/seo/rules/cwv.js +337 -0
  28. package/dist/seo/rules/ecommerce.d.ts +2 -0
  29. package/dist/seo/rules/ecommerce.js +252 -0
  30. package/dist/seo/rules/i18n.d.ts +2 -0
  31. package/dist/seo/rules/i18n.js +222 -0
  32. package/dist/seo/rules/images.d.ts +2 -0
  33. package/dist/seo/rules/images.js +180 -0
  34. package/dist/seo/rules/index.d.ts +52 -0
  35. package/dist/seo/rules/index.js +143 -0
  36. package/dist/seo/rules/internal-linking.d.ts +2 -0
  37. package/dist/seo/rules/internal-linking.js +375 -0
  38. package/dist/seo/rules/links.d.ts +2 -0
  39. package/dist/seo/rules/links.js +150 -0
  40. package/dist/seo/rules/local.d.ts +2 -0
  41. package/dist/seo/rules/local.js +265 -0
  42. package/dist/seo/rules/meta.d.ts +2 -0
  43. package/dist/seo/rules/meta.js +523 -0
  44. package/dist/seo/rules/mobile.d.ts +2 -0
  45. package/dist/seo/rules/mobile.js +71 -0
  46. package/dist/seo/rules/performance.d.ts +2 -0
  47. package/dist/seo/rules/performance.js +246 -0
  48. package/dist/seo/rules/pwa.d.ts +2 -0
  49. package/dist/seo/rules/pwa.js +302 -0
  50. package/dist/seo/rules/readability.d.ts +2 -0
  51. package/dist/seo/rules/readability.js +255 -0
  52. package/dist/seo/rules/schema.d.ts +2 -0
  53. package/dist/seo/rules/schema.js +54 -0
  54. package/dist/seo/rules/security.d.ts +2 -0
  55. package/dist/seo/rules/security.js +525 -0
  56. package/dist/seo/rules/social.d.ts +2 -0
  57. package/dist/seo/rules/social.js +373 -0
  58. package/dist/seo/rules/structural.d.ts +2 -0
  59. package/dist/seo/rules/structural.js +155 -0
  60. package/dist/seo/rules/technical.d.ts +2 -0
  61. package/dist/seo/rules/technical.js +223 -0
  62. package/dist/seo/rules/thresholds.d.ts +196 -0
  63. package/dist/seo/rules/thresholds.js +118 -0
  64. package/dist/seo/rules/types.d.ts +346 -0
  65. package/dist/seo/rules/types.js +11 -0
  66. package/dist/seo/seo-spider.d.ts +47 -0
  67. package/dist/seo/seo-spider.js +362 -0
  68. package/dist/seo/types.d.ts +184 -0
  69. package/dist/seo/types.js +1 -0
  70. package/dist/utils/columns.d.ts +14 -0
  71. package/dist/utils/columns.js +69 -0
  72. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const performanceRules = [
4
+ {
5
+ id: 'perf-preconnect',
6
+ name: 'Preconnect Hints',
7
+ category: 'performance',
8
+ severity: 'info',
9
+ description: 'Use preconnect for important third-party origins',
10
+ check: (ctx) => {
11
+ if (!ctx.hasPreconnect && ctx.externalLinks && ctx.externalLinks > 5) {
12
+ return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'info', 'No preconnect hints found', { recommendation: 'Add <link rel="preconnect" href="..."> for important third-party domains' });
13
+ }
14
+ if (ctx.preconnectCount && ctx.preconnectCount > 0) {
15
+ return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preconnectCount} preconnect hint(s) found`, { value: ctx.preconnectCount });
16
+ }
17
+ return null;
18
+ },
19
+ },
20
+ {
21
+ id: 'perf-dns-prefetch',
22
+ name: 'DNS Prefetch',
23
+ category: 'performance',
24
+ severity: 'info',
25
+ description: 'Use dns-prefetch for external domains',
26
+ check: (ctx) => {
27
+ if (ctx.dnsPrefetchCount && ctx.dnsPrefetchCount > 0) {
28
+ return createResult({ id: 'perf-dns-prefetch', name: 'DNS Prefetch', category: 'performance', severity: 'info' }, 'pass', `${ctx.dnsPrefetchCount} dns-prefetch hint(s) found`, { value: ctx.dnsPrefetchCount });
29
+ }
30
+ return null;
31
+ },
32
+ },
33
+ {
34
+ id: 'perf-preload',
35
+ name: 'Preload Hints',
36
+ category: 'performance',
37
+ severity: 'info',
38
+ description: 'Use preload for critical resources',
39
+ check: (ctx) => {
40
+ if (ctx.preloadCount && ctx.preloadCount > 0) {
41
+ return createResult({ id: 'perf-preload', name: 'Preload Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preloadCount} preload hint(s) found`, { value: ctx.preloadCount });
42
+ }
43
+ return null;
44
+ },
45
+ },
46
+ {
47
+ id: 'perf-render-blocking',
48
+ name: 'Render Blocking',
49
+ category: 'performance',
50
+ severity: 'warning',
51
+ description: 'Minimize render-blocking resources',
52
+ check: (ctx) => {
53
+ const blocking = ctx.renderBlockingResources ?? 0;
54
+ if (blocking > 5) {
55
+ return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'warn', `${blocking} render-blocking resources in <head>`, { value: blocking, recommendation: 'Use async/defer for scripts, preload for critical CSS' });
56
+ }
57
+ if (blocking > 0) {
58
+ return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'info', `${blocking} render-blocking resource(s) in <head>`, { value: blocking });
59
+ }
60
+ return null;
61
+ },
62
+ },
63
+ {
64
+ id: 'perf-inline-styles',
65
+ name: 'Inline Styles',
66
+ category: 'performance',
67
+ severity: 'info',
68
+ description: 'Excessive inline styles can increase page size',
69
+ check: (ctx) => {
70
+ const inline = ctx.inlineStylesCount ?? 0;
71
+ if (inline > 10) {
72
+ return createResult({ id: 'perf-inline-styles', name: 'Inline Styles', category: 'performance', severity: 'info' }, 'info', `${inline} inline style blocks found`, { value: inline, recommendation: 'Consider consolidating inline styles into external CSS' });
73
+ }
74
+ return null;
75
+ },
76
+ },
77
+ {
78
+ id: 'cwv-lcp-lazy',
79
+ name: 'LCP Image Lazy',
80
+ category: 'performance',
81
+ severity: 'warning',
82
+ description: 'Above-the-fold images should not use lazy loading',
83
+ check: (ctx) => {
84
+ if (ctx.lcpHints?.hasLazyLcp) {
85
+ return createResult({ id: 'cwv-lcp-lazy', name: 'LCP Image Lazy', category: 'performance', severity: 'warning' }, 'warn', 'First large image uses lazy loading (may hurt LCP)', { recommendation: 'Remove loading="lazy" from above-the-fold images' });
86
+ }
87
+ return null;
88
+ },
89
+ },
90
+ {
91
+ id: 'cwv-lcp-priority',
92
+ name: 'LCP Priority Hints',
93
+ category: 'performance',
94
+ severity: 'info',
95
+ description: 'Use fetchpriority="high" for LCP images',
96
+ check: (ctx) => {
97
+ if (ctx.lcpHints?.hasLargeImages && !ctx.lcpHints?.hasPriorityHints) {
98
+ return createResult({ id: 'cwv-lcp-priority', name: 'LCP Priority Hints', category: 'performance', severity: 'info' }, 'info', 'No fetchpriority="high" found on large images', { recommendation: 'Add fetchpriority="high" to LCP candidate images' });
99
+ }
100
+ return null;
101
+ },
102
+ },
103
+ {
104
+ id: 'cwv-cls-images',
105
+ name: 'CLS Image Dimensions',
106
+ category: 'performance',
107
+ severity: 'warning',
108
+ description: 'Images without dimensions cause layout shifts',
109
+ check: (ctx) => {
110
+ const missing = ctx.clsHints?.imagesWithoutDimensions ?? ctx.imagesMissingDimensions ?? 0;
111
+ if (missing > 0) {
112
+ return createResult({ id: 'cwv-cls-images', name: 'CLS Image Dimensions', category: 'performance', severity: 'warning' }, 'warn', `${missing} image(s) without width/height (causes CLS)`, { value: missing, recommendation: 'Add width and height attributes to all images' });
113
+ }
114
+ return null;
115
+ },
116
+ },
117
+ {
118
+ id: 'timing-ttfb',
119
+ name: 'Time to First Byte',
120
+ category: 'performance',
121
+ severity: 'error',
122
+ description: 'TTFB should be under 600ms (ideally under 200ms)',
123
+ check: (ctx) => {
124
+ const ttfb = ctx.timings?.ttfb;
125
+ if (ttfb === undefined)
126
+ return null;
127
+ const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.ttfb;
128
+ if (ttfb <= good) {
129
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'pass', `Excellent TTFB (${ttfb}ms)`, { value: ttfb });
130
+ }
131
+ if (ttfb <= needsImprovement) {
132
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'info', `TTFB needs improvement (${ttfb}ms)`, { value: ttfb, recommendation: `Optimize server response time to under ${good}ms` });
133
+ }
134
+ if (ttfb <= poor) {
135
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'warn', `Slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Optimize server, use CDN, enable caching' });
136
+ }
137
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'fail', `Very slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Critical: Server is too slow. Check server, database, and network' });
138
+ },
139
+ },
140
+ {
141
+ id: 'timing-total',
142
+ name: 'Total Load Time',
143
+ category: 'performance',
144
+ severity: 'warning',
145
+ description: 'Total page load should be under 2.5s',
146
+ check: (ctx) => {
147
+ const total = ctx.timings?.total;
148
+ if (total === undefined)
149
+ return null;
150
+ const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.total;
151
+ if (total <= good) {
152
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'pass', `Fast page load (${total}ms)`, { value: total });
153
+ }
154
+ if (total <= needsImprovement) {
155
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'info', `Page load time acceptable (${total}ms)`, { value: total, recommendation: `Aim for under ${good}ms for better user experience` });
156
+ }
157
+ if (total <= poor) {
158
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'warn', `Slow page load (${total}ms)`, { value: total, recommendation: 'Optimize assets, enable compression, use CDN' });
159
+ }
160
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'fail', `Very slow page load (${total}ms)`, { value: total, recommendation: 'Critical performance issue. Full optimization needed.' });
161
+ },
162
+ },
163
+ {
164
+ id: 'timing-dns',
165
+ name: 'DNS Lookup',
166
+ category: 'performance',
167
+ severity: 'info',
168
+ description: 'DNS lookup should be under 50ms',
169
+ check: (ctx) => {
170
+ const dns = ctx.timings?.dnsLookup;
171
+ if (dns === undefined)
172
+ return null;
173
+ const { good, poor } = SEO_THRESHOLDS.timing.dnsLookup;
174
+ if (dns <= good) {
175
+ return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'pass', `Fast DNS lookup (${dns}ms)`, { value: dns });
176
+ }
177
+ if (dns <= poor) {
178
+ return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'info', `DNS lookup could be faster (${dns}ms)`, { value: dns, recommendation: 'Consider using faster DNS provider or dns-prefetch' });
179
+ }
180
+ return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'warn', `Slow DNS lookup (${dns}ms)`, { value: dns, recommendation: 'DNS is slow. Consider Cloudflare, Google DNS, or dns-prefetch' });
181
+ },
182
+ },
183
+ {
184
+ id: 'timing-tls',
185
+ name: 'TLS Handshake',
186
+ category: 'performance',
187
+ severity: 'info',
188
+ description: 'TLS handshake should be under 100ms',
189
+ check: (ctx) => {
190
+ const tls = ctx.timings?.tlsHandshake;
191
+ if (tls === undefined)
192
+ return null;
193
+ const { good, poor } = SEO_THRESHOLDS.timing.tlsHandshake;
194
+ if (tls <= good) {
195
+ return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'pass', `Fast TLS handshake (${tls}ms)`, { value: tls });
196
+ }
197
+ if (tls <= poor) {
198
+ return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'info', `TLS handshake could be faster (${tls}ms)`, { value: tls, recommendation: 'Consider TLS 1.3, HTTP/2, or preconnect hints' });
199
+ }
200
+ return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'warn', `Slow TLS handshake (${tls}ms)`, { value: tls, recommendation: 'TLS is slow. Check server configuration and certificate chain' });
201
+ },
202
+ },
203
+ {
204
+ id: 'response-html-size',
205
+ name: 'HTML Size',
206
+ category: 'performance',
207
+ severity: 'warning',
208
+ description: 'HTML should be under 500KB (ideally under 100KB)',
209
+ check: (ctx) => {
210
+ const size = ctx.htmlSize;
211
+ if (size === undefined)
212
+ return null;
213
+ const { good, warning, poor } = SEO_THRESHOLDS.responseSize.html;
214
+ const sizeKb = Math.round(size / 1024);
215
+ if (size <= good) {
216
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'pass', `HTML size is good (${sizeKb}KB)`, { value: sizeKb });
217
+ }
218
+ if (size <= warning) {
219
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'info', `HTML size is acceptable (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Consider reducing HTML size for faster parsing' });
220
+ }
221
+ if (size <= poor) {
222
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'warn', `HTML is large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Reduce HTML size. Check for inline data, remove unused code' });
223
+ }
224
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'fail', `HTML is very large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Critical: HTML too large. Use pagination, lazy loading, or split content' });
225
+ },
226
+ },
227
+ {
228
+ id: 'response-compression',
229
+ name: 'Compression',
230
+ category: 'performance',
231
+ severity: 'warning',
232
+ description: 'Response should be compressed (gzip/brotli)',
233
+ check: (ctx) => {
234
+ if (ctx.htmlSize === undefined)
235
+ return null;
236
+ if (ctx.isCompressed === false && ctx.htmlSize > 1024) {
237
+ return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'warn', 'Response is not compressed', { recommendation: 'Enable gzip or brotli compression on server' });
238
+ }
239
+ if (ctx.isCompressed === true) {
240
+ const ratio = ctx.compressedSize && ctx.htmlSize ? Math.round((ctx.compressedSize / ctx.htmlSize) * 100) : undefined;
241
+ return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'pass', ratio ? `Response compressed (${ratio}% of original)` : 'Response is compressed');
242
+ }
243
+ return null;
244
+ },
245
+ },
246
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const pwaRules: SeoRule[];
@@ -0,0 +1,302 @@
1
+ import { createResult } from './types.js';
2
+ export const pwaRules = [
3
+ {
4
+ id: 'pwa-manifest-link',
5
+ name: 'Web App Manifest',
6
+ category: 'technical',
7
+ severity: 'info',
8
+ description: 'Pages should link to a web app manifest for PWA support',
9
+ check: (ctx) => {
10
+ if (ctx.hasManifest === undefined)
11
+ return null;
12
+ if (!ctx.hasManifest) {
13
+ return createResult({ id: 'pwa-manifest-link', name: 'Web App Manifest', category: 'technical', severity: 'info' }, 'info', 'No web app manifest linked', {
14
+ recommendation: 'Add a manifest.json for PWA installability',
15
+ evidence: {
16
+ expected: '<link rel="manifest" href="/manifest.json">',
17
+ impact: 'Required for "Add to Home Screen" and PWA features',
18
+ learnMore: 'https://web.dev/add-manifest/',
19
+ },
20
+ });
21
+ }
22
+ return createResult({ id: 'pwa-manifest-link', name: 'Web App Manifest', category: 'technical', severity: 'info' }, 'pass', `Manifest linked${ctx.manifestUrl ? `: ${ctx.manifestUrl}` : ''}`);
23
+ },
24
+ },
25
+ {
26
+ id: 'pwa-theme-color',
27
+ name: 'Theme Color',
28
+ category: 'mobile',
29
+ severity: 'info',
30
+ description: 'Pages should define a theme color for browser UI',
31
+ check: (ctx) => {
32
+ if (ctx.themeColor === undefined)
33
+ return null;
34
+ if (!ctx.themeColor) {
35
+ return createResult({ id: 'pwa-theme-color', name: 'Theme Color', category: 'mobile', severity: 'info' }, 'info', 'No theme-color meta tag', {
36
+ recommendation: 'Add theme-color for browser UI customization',
37
+ evidence: {
38
+ expected: '<meta name="theme-color" content="#4285f4">',
39
+ impact: 'Controls browser toolbar color on mobile devices',
40
+ },
41
+ });
42
+ }
43
+ return createResult({ id: 'pwa-theme-color', name: 'Theme Color', category: 'mobile', severity: 'info' }, 'pass', `Theme color: ${ctx.themeColor}`);
44
+ },
45
+ },
46
+ {
47
+ id: 'pwa-apple-touch-icon',
48
+ name: 'Apple Touch Icon',
49
+ category: 'mobile',
50
+ severity: 'info',
51
+ description: 'iOS devices need apple-touch-icon for home screen',
52
+ check: (ctx) => {
53
+ if (ctx.hasAppleTouchIcon === undefined)
54
+ return null;
55
+ if (!ctx.hasAppleTouchIcon) {
56
+ return createResult({ id: 'pwa-apple-touch-icon', name: 'Apple Touch Icon', category: 'mobile', severity: 'info' }, 'info', 'No apple-touch-icon found', {
57
+ recommendation: 'Add apple-touch-icon for iOS home screen',
58
+ evidence: {
59
+ expected: '<link rel="apple-touch-icon" href="/apple-touch-icon.png">',
60
+ impact: 'iOS uses this icon when user adds site to home screen',
61
+ example: 'Recommended size: 180x180 pixels',
62
+ },
63
+ });
64
+ }
65
+ return createResult({ id: 'pwa-apple-touch-icon', name: 'Apple Touch Icon', category: 'mobile', severity: 'info' }, 'pass', 'Apple touch icon present');
66
+ },
67
+ },
68
+ {
69
+ id: 'pwa-apple-mobile-capable',
70
+ name: 'Apple Mobile Web App',
71
+ category: 'mobile',
72
+ severity: 'info',
73
+ description: 'Enable standalone mode on iOS devices',
74
+ check: (ctx) => {
75
+ if (ctx.hasAppleMobileWebAppCapable === undefined)
76
+ return null;
77
+ if (!ctx.hasAppleMobileWebAppCapable) {
78
+ return createResult({ id: 'pwa-apple-mobile-capable', name: 'Apple Mobile Web App', category: 'mobile', severity: 'info' }, 'info', 'No apple-mobile-web-app-capable meta tag', {
79
+ recommendation: 'Add for full-screen iOS experience',
80
+ evidence: {
81
+ expected: '<meta name="apple-mobile-web-app-capable" content="yes">',
82
+ impact: 'Enables standalone app mode when launched from home screen',
83
+ },
84
+ });
85
+ }
86
+ return createResult({ id: 'pwa-apple-mobile-capable', name: 'Apple Mobile Web App', category: 'mobile', severity: 'info' }, 'pass', 'Apple mobile web app capable enabled');
87
+ },
88
+ },
89
+ {
90
+ id: 'pwa-apple-status-bar',
91
+ name: 'Apple Status Bar Style',
92
+ category: 'mobile',
93
+ severity: 'info',
94
+ description: 'Configure iOS status bar appearance',
95
+ check: (ctx) => {
96
+ if (!ctx.hasAppleMobileWebAppCapable)
97
+ return null;
98
+ if (ctx.appleStatusBarStyle === undefined)
99
+ return null;
100
+ if (!ctx.appleStatusBarStyle) {
101
+ return createResult({ id: 'pwa-apple-status-bar', name: 'Apple Status Bar Style', category: 'mobile', severity: 'info' }, 'info', 'No apple-mobile-web-app-status-bar-style defined', {
102
+ recommendation: 'Define status bar style for iOS',
103
+ evidence: {
104
+ expected: '<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">',
105
+ example: 'Options: default, black, black-translucent',
106
+ },
107
+ });
108
+ }
109
+ return createResult({ id: 'pwa-apple-status-bar', name: 'Apple Status Bar Style', category: 'mobile', severity: 'info' }, 'pass', `Status bar style: ${ctx.appleStatusBarStyle}`);
110
+ },
111
+ },
112
+ {
113
+ id: 'pwa-maskable-icon',
114
+ name: 'Maskable Icon',
115
+ category: 'mobile',
116
+ severity: 'info',
117
+ description: 'Manifest should include maskable icons for Android',
118
+ check: (ctx) => {
119
+ if (ctx.hasMaskableIcon === undefined)
120
+ return null;
121
+ if (!ctx.hasMaskableIcon) {
122
+ return createResult({ id: 'pwa-maskable-icon', name: 'Maskable Icon', category: 'mobile', severity: 'info' }, 'info', 'No maskable icon in manifest', {
123
+ recommendation: 'Add maskable icon for adaptive icons on Android',
124
+ evidence: {
125
+ example: `{
126
+ "icons": [{
127
+ "src": "/icon-maskable.png",
128
+ "sizes": "512x512",
129
+ "type": "image/png",
130
+ "purpose": "maskable"
131
+ }]
132
+ }`,
133
+ impact: 'Maskable icons adapt to different Android icon shapes',
134
+ learnMore: 'https://web.dev/maskable-icon/',
135
+ },
136
+ });
137
+ }
138
+ return createResult({ id: 'pwa-maskable-icon', name: 'Maskable Icon', category: 'mobile', severity: 'info' }, 'pass', 'Maskable icon defined');
139
+ },
140
+ },
141
+ {
142
+ id: 'pwa-start-url',
143
+ name: 'Start URL',
144
+ category: 'technical',
145
+ severity: 'info',
146
+ description: 'Manifest should define a start_url',
147
+ check: (ctx) => {
148
+ if (!ctx.hasManifest)
149
+ return null;
150
+ if (ctx.manifestStartUrl === undefined)
151
+ return null;
152
+ if (!ctx.manifestStartUrl) {
153
+ return createResult({ id: 'pwa-start-url', name: 'Start URL', category: 'technical', severity: 'info' }, 'info', 'Manifest missing start_url', {
154
+ recommendation: 'Define start_url in manifest for PWA launch',
155
+ evidence: {
156
+ expected: '"start_url": "/?source=pwa"',
157
+ impact: 'Controls which page opens when PWA is launched',
158
+ },
159
+ });
160
+ }
161
+ return createResult({ id: 'pwa-start-url', name: 'Start URL', category: 'technical', severity: 'info' }, 'pass', `Start URL: ${ctx.manifestStartUrl}`);
162
+ },
163
+ },
164
+ {
165
+ id: 'pwa-display-mode',
166
+ name: 'Display Mode',
167
+ category: 'technical',
168
+ severity: 'info',
169
+ description: 'Manifest should define display mode',
170
+ check: (ctx) => {
171
+ if (!ctx.hasManifest)
172
+ return null;
173
+ if (ctx.manifestDisplay === undefined)
174
+ return null;
175
+ const display = ctx.manifestDisplay;
176
+ if (!display) {
177
+ return createResult({ id: 'pwa-display-mode', name: 'Display Mode', category: 'technical', severity: 'info' }, 'info', 'Manifest missing display mode', {
178
+ recommendation: 'Set display mode for app-like experience',
179
+ evidence: {
180
+ expected: '"display": "standalone"',
181
+ example: 'Options: fullscreen, standalone, minimal-ui, browser',
182
+ },
183
+ });
184
+ }
185
+ const goodModes = ['standalone', 'fullscreen', 'minimal-ui'];
186
+ if (!goodModes.includes(display)) {
187
+ return createResult({ id: 'pwa-display-mode', name: 'Display Mode', category: 'technical', severity: 'info' }, 'info', `Display mode: ${display} (browser-like)`, {
188
+ recommendation: 'Consider standalone or fullscreen for app-like experience',
189
+ });
190
+ }
191
+ return createResult({ id: 'pwa-display-mode', name: 'Display Mode', category: 'technical', severity: 'info' }, 'pass', `Display mode: ${display}`);
192
+ },
193
+ },
194
+ {
195
+ id: 'pwa-scope',
196
+ name: 'Navigation Scope',
197
+ category: 'technical',
198
+ severity: 'info',
199
+ description: 'Manifest should define navigation scope',
200
+ check: (ctx) => {
201
+ if (!ctx.hasManifest)
202
+ return null;
203
+ if (ctx.manifestScope === undefined)
204
+ return null;
205
+ if (!ctx.manifestScope) {
206
+ return createResult({ id: 'pwa-scope', name: 'Navigation Scope', category: 'technical', severity: 'info' }, 'info', 'Manifest missing scope', {
207
+ recommendation: 'Define scope to control PWA navigation boundaries',
208
+ evidence: {
209
+ expected: '"scope": "/"',
210
+ impact: 'Limits which URLs are part of the app experience',
211
+ },
212
+ });
213
+ }
214
+ return createResult({ id: 'pwa-scope', name: 'Navigation Scope', category: 'technical', severity: 'info' }, 'pass', `Scope: ${ctx.manifestScope}`);
215
+ },
216
+ },
217
+ {
218
+ id: 'pwa-icons-sizes',
219
+ name: 'Icon Sizes',
220
+ category: 'mobile',
221
+ severity: 'info',
222
+ description: 'Manifest should include multiple icon sizes',
223
+ check: (ctx) => {
224
+ if (!ctx.hasManifest)
225
+ return null;
226
+ if (ctx.manifestIconSizes === undefined)
227
+ return null;
228
+ const sizes = ctx.manifestIconSizes;
229
+ const requiredSizes = [192, 512];
230
+ const missingSizes = requiredSizes.filter(s => !sizes.includes(s));
231
+ if (missingSizes.length > 0) {
232
+ return createResult({ id: 'pwa-icons-sizes', name: 'Icon Sizes', category: 'mobile', severity: 'info' }, 'info', `Missing icon sizes: ${missingSizes.join(', ')}px`, {
233
+ recommendation: 'Add icons in required sizes',
234
+ evidence: {
235
+ found: sizes.length > 0 ? sizes.map(s => `${s}px`).join(', ') : 'No icons',
236
+ expected: '192x192 and 512x512 minimum',
237
+ impact: 'Required for Chrome "Add to Home Screen" prompt',
238
+ },
239
+ });
240
+ }
241
+ return createResult({ id: 'pwa-icons-sizes', name: 'Icon Sizes', category: 'mobile', severity: 'info' }, 'pass', `Icon sizes: ${sizes.map(s => `${s}px`).join(', ')}`);
242
+ },
243
+ },
244
+ {
245
+ id: 'pwa-short-name',
246
+ name: 'Short Name',
247
+ category: 'technical',
248
+ severity: 'info',
249
+ description: 'Manifest should have a short_name for home screen',
250
+ check: (ctx) => {
251
+ if (!ctx.hasManifest)
252
+ return null;
253
+ if (ctx.manifestShortName === undefined && ctx.manifestName === undefined)
254
+ return null;
255
+ const shortName = ctx.manifestShortName;
256
+ const name = ctx.manifestName;
257
+ if (!shortName && !name) {
258
+ return createResult({ id: 'pwa-short-name', name: 'Short Name', category: 'technical', severity: 'info' }, 'info', 'Manifest missing name and short_name', {
259
+ recommendation: 'Add short_name (max 12 chars) for home screen label',
260
+ evidence: {
261
+ expected: '"short_name": "MyApp"',
262
+ impact: 'Used as app label on home screen',
263
+ },
264
+ });
265
+ }
266
+ if (shortName && shortName.length > 12) {
267
+ return createResult({ id: 'pwa-short-name', name: 'Short Name', category: 'technical', severity: 'info' }, 'info', `Short name too long: ${shortName.length} chars`, {
268
+ recommendation: 'Keep short_name under 12 characters',
269
+ evidence: {
270
+ found: shortName,
271
+ expected: 'Max 12 characters',
272
+ impact: 'May be truncated on home screen',
273
+ },
274
+ });
275
+ }
276
+ return createResult({ id: 'pwa-short-name', name: 'Short Name', category: 'technical', severity: 'info' }, 'pass', `App name: ${shortName || name}`);
277
+ },
278
+ },
279
+ {
280
+ id: 'pwa-background-color',
281
+ name: 'Background Color',
282
+ category: 'mobile',
283
+ severity: 'info',
284
+ description: 'Manifest should define background_color for splash screen',
285
+ check: (ctx) => {
286
+ if (!ctx.hasManifest)
287
+ return null;
288
+ if (ctx.manifestBackgroundColor === undefined)
289
+ return null;
290
+ if (!ctx.manifestBackgroundColor) {
291
+ return createResult({ id: 'pwa-background-color', name: 'Background Color', category: 'mobile', severity: 'info' }, 'info', 'Manifest missing background_color', {
292
+ recommendation: 'Define background_color for splash screen',
293
+ evidence: {
294
+ expected: '"background_color": "#ffffff"',
295
+ impact: 'Shown as splash screen background during app launch',
296
+ },
297
+ });
298
+ }
299
+ return createResult({ id: 'pwa-background-color', name: 'Background Color', category: 'mobile', severity: 'info' }, 'pass', `Background color: ${ctx.manifestBackgroundColor}`);
300
+ },
301
+ },
302
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const readabilityRules: SeoRule[];