recker 1.0.28 → 1.0.29-next.72485b7

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 (53) hide show
  1. package/README.md +28 -1
  2. package/dist/ai/memory.d.ts +35 -0
  3. package/dist/ai/memory.js +136 -0
  4. package/dist/browser/types/ai-client.d.ts +29 -0
  5. package/dist/browser/types/ai-client.js +1 -0
  6. package/dist/cli/tui/scroll-buffer.js +4 -4
  7. package/dist/cli/tui/shell.d.ts +1 -0
  8. package/dist/cli/tui/shell.js +375 -18
  9. package/dist/mcp/server.js +15 -0
  10. package/dist/mcp/tools/scrape.d.ts +3 -0
  11. package/dist/mcp/tools/scrape.js +156 -0
  12. package/dist/mcp/tools/security.d.ts +3 -0
  13. package/dist/mcp/tools/security.js +471 -0
  14. package/dist/mcp/tools/seo.d.ts +3 -0
  15. package/dist/mcp/tools/seo.js +427 -0
  16. package/dist/scrape/index.d.ts +2 -0
  17. package/dist/scrape/index.js +1 -0
  18. package/dist/scrape/spider.d.ts +61 -0
  19. package/dist/scrape/spider.js +250 -0
  20. package/dist/seo/analyzer.js +27 -0
  21. package/dist/seo/index.d.ts +3 -1
  22. package/dist/seo/index.js +1 -0
  23. package/dist/seo/rules/accessibility.js +620 -54
  24. package/dist/seo/rules/best-practices.d.ts +2 -0
  25. package/dist/seo/rules/best-practices.js +188 -0
  26. package/dist/seo/rules/crawl.d.ts +2 -0
  27. package/dist/seo/rules/crawl.js +307 -0
  28. package/dist/seo/rules/cwv.d.ts +2 -0
  29. package/dist/seo/rules/cwv.js +337 -0
  30. package/dist/seo/rules/ecommerce.d.ts +2 -0
  31. package/dist/seo/rules/ecommerce.js +252 -0
  32. package/dist/seo/rules/i18n.d.ts +2 -0
  33. package/dist/seo/rules/i18n.js +222 -0
  34. package/dist/seo/rules/index.d.ts +32 -0
  35. package/dist/seo/rules/index.js +71 -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/local.d.ts +2 -0
  39. package/dist/seo/rules/local.js +265 -0
  40. package/dist/seo/rules/pwa.d.ts +2 -0
  41. package/dist/seo/rules/pwa.js +302 -0
  42. package/dist/seo/rules/readability.d.ts +2 -0
  43. package/dist/seo/rules/readability.js +255 -0
  44. package/dist/seo/rules/security.js +406 -28
  45. package/dist/seo/rules/social.d.ts +2 -0
  46. package/dist/seo/rules/social.js +373 -0
  47. package/dist/seo/rules/types.d.ts +155 -0
  48. package/dist/seo/seo-spider.d.ts +47 -0
  49. package/dist/seo/seo-spider.js +362 -0
  50. package/dist/seo/types.d.ts +24 -0
  51. package/dist/types/ai-client.d.ts +29 -0
  52. package/dist/types/ai-client.js +1 -0
  53. package/package.json +1 -1
@@ -0,0 +1,337 @@
1
+ import { createResult } from './types.js';
2
+ export const cwvRules = [
3
+ {
4
+ id: 'cwv-lcp-hero-image',
5
+ name: 'LCP Hero Image',
6
+ category: 'performance',
7
+ severity: 'warning',
8
+ description: 'Hero images should be optimized for fast LCP',
9
+ check: (ctx) => {
10
+ if (!ctx.lcpCandidate)
11
+ return null;
12
+ const issues = [];
13
+ if (ctx.lcpCandidate.loading === 'lazy') {
14
+ issues.push('LCP image has loading="lazy" (should be eager or omitted)');
15
+ }
16
+ if (!ctx.lcpCandidate.fetchpriority) {
17
+ issues.push('Missing fetchpriority="high" on LCP image');
18
+ }
19
+ if (!ctx.hasLcpPreload) {
20
+ issues.push('LCP image not preloaded');
21
+ }
22
+ if (issues.length > 0) {
23
+ return createResult({ id: 'cwv-lcp-hero-image', name: 'LCP Hero Image', category: 'performance', severity: 'warning' }, 'warn', `LCP optimization issues: ${issues.length} found`, {
24
+ recommendation: 'Optimize LCP image loading',
25
+ evidence: {
26
+ found: issues,
27
+ expected: 'LCP image with fetchpriority="high", no lazy loading, and preload link',
28
+ example: `<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
29
+ <img src="hero.webp" fetchpriority="high" alt="Hero">`,
30
+ impact: 'LCP is a Core Web Vital - aim for < 2.5s',
31
+ learnMore: 'https://web.dev/lcp/',
32
+ },
33
+ });
34
+ }
35
+ return createResult({ id: 'cwv-lcp-hero-image', name: 'LCP Hero Image', category: 'performance', severity: 'warning' }, 'pass', 'LCP image appears optimized');
36
+ },
37
+ },
38
+ {
39
+ id: 'cwv-lcp-text-visible',
40
+ name: 'LCP Text Visibility',
41
+ category: 'performance',
42
+ severity: 'warning',
43
+ description: 'Text LCP elements should render immediately without font blocking',
44
+ check: (ctx) => {
45
+ if (!ctx.webFonts || ctx.webFonts.length === 0)
46
+ return null;
47
+ const blockingFonts = ctx.webFonts.filter(f => !f.hasSwap && !f.hasOptional);
48
+ if (blockingFonts.length > 0) {
49
+ return createResult({ id: 'cwv-lcp-text-visible', name: 'LCP Text Visibility', category: 'performance', severity: 'warning' }, 'warn', `${blockingFonts.length} web font(s) may block text rendering`, {
50
+ recommendation: 'Use font-display: swap or optional for web fonts',
51
+ evidence: {
52
+ found: blockingFonts.map(f => f.family).filter(Boolean).slice(0, 3),
53
+ expected: 'font-display: swap or font-display: optional',
54
+ example: '@font-face { font-display: swap; }',
55
+ impact: 'Blocking fonts delay LCP for text elements',
56
+ learnMore: 'https://web.dev/font-display/',
57
+ },
58
+ });
59
+ }
60
+ return createResult({ id: 'cwv-lcp-text-visible', name: 'LCP Text Visibility', category: 'performance', severity: 'warning' }, 'pass', 'Web fonts use non-blocking display');
61
+ },
62
+ },
63
+ {
64
+ id: 'cwv-render-blocking-css',
65
+ name: 'Render-Blocking CSS',
66
+ category: 'performance',
67
+ severity: 'warning',
68
+ description: 'Minimize render-blocking CSS for faster LCP',
69
+ check: (ctx) => {
70
+ if (ctx.renderBlockingStylesheets === undefined)
71
+ return null;
72
+ if (ctx.renderBlockingStylesheets > 3) {
73
+ return createResult({ id: 'cwv-render-blocking-css', name: 'Render-Blocking CSS', category: 'performance', severity: 'warning' }, 'warn', `${ctx.renderBlockingStylesheets} render-blocking stylesheets`, {
74
+ recommendation: 'Reduce render-blocking CSS or use critical CSS inline',
75
+ evidence: {
76
+ found: ctx.renderBlockingStylesheets,
77
+ expected: '1-3 stylesheets or inline critical CSS',
78
+ example: '<style>/* critical CSS */</style>\n<link rel="stylesheet" href="main.css" media="print" onload="this.media=\'all\'">',
79
+ impact: 'Each blocking stylesheet delays first paint',
80
+ },
81
+ });
82
+ }
83
+ return createResult({ id: 'cwv-render-blocking-css', name: 'Render-Blocking CSS', category: 'performance', severity: 'warning' }, 'pass', `${ctx.renderBlockingStylesheets} render-blocking stylesheet(s)`);
84
+ },
85
+ },
86
+ {
87
+ id: 'cwv-render-blocking-js',
88
+ name: 'Render-Blocking JavaScript',
89
+ category: 'performance',
90
+ severity: 'warning',
91
+ description: 'Scripts in head should use async or defer',
92
+ check: (ctx) => {
93
+ if (ctx.renderBlockingScripts === undefined)
94
+ return null;
95
+ if (ctx.renderBlockingScripts > 0) {
96
+ return createResult({ id: 'cwv-render-blocking-js', name: 'Render-Blocking JavaScript', category: 'performance', severity: 'warning' }, 'warn', `${ctx.renderBlockingScripts} render-blocking script(s) in <head>`, {
97
+ recommendation: 'Add async or defer to scripts, or move to end of body',
98
+ evidence: {
99
+ found: ctx.renderBlockingScripts,
100
+ expected: 'Scripts with async, defer, or type="module"',
101
+ example: '<script src="app.js" defer></script>',
102
+ impact: 'Blocking scripts delay HTML parsing and LCP',
103
+ },
104
+ });
105
+ }
106
+ return createResult({ id: 'cwv-render-blocking-js', name: 'Render-Blocking JavaScript', category: 'performance', severity: 'warning' }, 'pass', 'No render-blocking scripts in head');
107
+ },
108
+ },
109
+ {
110
+ id: 'cwv-cls-image-dimensions',
111
+ name: 'Image Dimensions',
112
+ category: 'performance',
113
+ severity: 'warning',
114
+ description: 'Images should have explicit width and height to prevent layout shift',
115
+ check: (ctx) => {
116
+ if (ctx.imagesMissingDimensions === undefined)
117
+ return null;
118
+ if (ctx.imagesMissingDimensions > 0) {
119
+ const total = ctx.totalImages || 0;
120
+ const percent = total > 0 ? Math.round((ctx.imagesMissingDimensions / total) * 100) : 0;
121
+ return createResult({ id: 'cwv-cls-image-dimensions', name: 'Image Dimensions', category: 'performance', severity: 'warning' }, 'warn', `${ctx.imagesMissingDimensions} image(s) missing width/height (${percent}%)`, {
122
+ recommendation: 'Add explicit width and height attributes to all images',
123
+ evidence: {
124
+ found: `${ctx.imagesMissingDimensions} of ${total} images`,
125
+ expected: 'width and height on all <img> elements',
126
+ example: '<img src="photo.jpg" width="800" height="600" alt="...">',
127
+ impact: 'Missing dimensions cause layout shift when images load',
128
+ learnMore: 'https://web.dev/cls/',
129
+ },
130
+ });
131
+ }
132
+ return createResult({ id: 'cwv-cls-image-dimensions', name: 'Image Dimensions', category: 'performance', severity: 'warning' }, 'pass', 'All images have dimensions');
133
+ },
134
+ },
135
+ {
136
+ id: 'cwv-cls-aspect-ratio',
137
+ name: 'Aspect Ratio CSS',
138
+ category: 'performance',
139
+ severity: 'info',
140
+ description: 'Use CSS aspect-ratio for responsive media containers',
141
+ check: (ctx) => {
142
+ if (!ctx.hasAspectRatioCss && ctx.hasResponsiveImages) {
143
+ return createResult({ id: 'cwv-cls-aspect-ratio', name: 'Aspect Ratio CSS', category: 'performance', severity: 'info' }, 'info', 'Consider using CSS aspect-ratio for media containers', {
144
+ recommendation: 'Use aspect-ratio CSS property for responsive containers',
145
+ evidence: {
146
+ example: '.video-container { aspect-ratio: 16 / 9; }',
147
+ impact: 'Reserves space before media loads, preventing CLS',
148
+ learnMore: 'https://web.dev/aspect-ratio/',
149
+ },
150
+ });
151
+ }
152
+ return null;
153
+ },
154
+ },
155
+ {
156
+ id: 'cwv-cls-dynamic-content',
157
+ name: 'Dynamic Content Space',
158
+ category: 'performance',
159
+ severity: 'info',
160
+ description: 'Reserve space for dynamically injected content',
161
+ check: (ctx) => {
162
+ const potentialShifts = [];
163
+ if (ctx.hasAdsWithoutReservedSpace) {
164
+ potentialShifts.push('Ads without reserved space');
165
+ }
166
+ if (ctx.hasBannersWithoutMinHeight) {
167
+ potentialShifts.push('Banners/alerts without min-height');
168
+ }
169
+ if (ctx.hasInfiniteScroll) {
170
+ potentialShifts.push('Infinite scroll may cause shifts');
171
+ }
172
+ if (potentialShifts.length > 0) {
173
+ return createResult({ id: 'cwv-cls-dynamic-content', name: 'Dynamic Content Space', category: 'performance', severity: 'info' }, 'info', 'Potential layout shift sources detected', {
174
+ recommendation: 'Reserve space for dynamic content with min-height or skeleton loaders',
175
+ evidence: {
176
+ found: potentialShifts,
177
+ example: '.ad-slot { min-height: 250px; }',
178
+ impact: 'Dynamic content is a major source of CLS',
179
+ },
180
+ });
181
+ }
182
+ return null;
183
+ },
184
+ },
185
+ {
186
+ id: 'cwv-cls-font-fallback',
187
+ name: 'Font Fallback Metrics',
188
+ category: 'performance',
189
+ severity: 'info',
190
+ description: 'Web fonts should have size-matched fallbacks',
191
+ check: (ctx) => {
192
+ if (!ctx.webFonts || ctx.webFonts.length === 0)
193
+ return null;
194
+ const fontsWithoutMetrics = ctx.webFonts.filter(f => !f.hasSizeAdjust && !f.hasAscentOverride);
195
+ if (fontsWithoutMetrics.length > 0 && ctx.webFonts.length > 0) {
196
+ return createResult({ id: 'cwv-cls-font-fallback', name: 'Font Fallback Metrics', category: 'performance', severity: 'info' }, 'info', 'Web fonts may cause layout shift during swap', {
197
+ recommendation: 'Use size-adjust or metric overrides for fallback fonts',
198
+ evidence: {
199
+ example: `@font-face {
200
+ font-family: 'Custom Font';
201
+ src: url('custom.woff2') format('woff2');
202
+ font-display: swap;
203
+ size-adjust: 92%; /* Match fallback metrics */
204
+ }`,
205
+ impact: 'Font swapping can cause visible layout shift',
206
+ learnMore: 'https://web.dev/css-size-adjust/',
207
+ },
208
+ });
209
+ }
210
+ return null;
211
+ },
212
+ },
213
+ {
214
+ id: 'cwv-inp-main-thread',
215
+ name: 'Main Thread Blocking',
216
+ category: 'performance',
217
+ severity: 'warning',
218
+ description: 'Large inline scripts can block the main thread',
219
+ check: (ctx) => {
220
+ if (ctx.largeInlineScripts === undefined)
221
+ return null;
222
+ if (ctx.largeInlineScripts > 0) {
223
+ return createResult({ id: 'cwv-inp-main-thread', name: 'Main Thread Blocking', category: 'performance', severity: 'warning' }, 'warn', `${ctx.largeInlineScripts} large inline script(s) may block main thread`, {
224
+ recommendation: 'Move large scripts to external files with defer',
225
+ evidence: {
226
+ found: `${ctx.largeInlineScripts} inline scripts > 10KB`,
227
+ expected: 'Small inline scripts only for critical initialization',
228
+ impact: 'Large scripts block main thread and delay interactions',
229
+ learnMore: 'https://web.dev/optimize-inp/',
230
+ },
231
+ });
232
+ }
233
+ return null;
234
+ },
235
+ },
236
+ {
237
+ id: 'cwv-inp-event-handlers',
238
+ name: 'Inline Event Handlers',
239
+ category: 'performance',
240
+ severity: 'info',
241
+ description: 'Avoid inline event handlers for better performance',
242
+ check: (ctx) => {
243
+ if (ctx.inlineEventHandlers === undefined)
244
+ return null;
245
+ if (ctx.inlineEventHandlers > 10) {
246
+ return createResult({ id: 'cwv-inp-event-handlers', name: 'Inline Event Handlers', category: 'performance', severity: 'info' }, 'info', `${ctx.inlineEventHandlers} inline event handlers found`, {
247
+ recommendation: 'Use event delegation instead of inline handlers',
248
+ evidence: {
249
+ found: ctx.inlineEventHandlers,
250
+ expected: 'Minimal inline handlers, prefer addEventListener',
251
+ example: 'document.addEventListener("click", handleClick)',
252
+ impact: 'Many inline handlers increase parsing time',
253
+ },
254
+ });
255
+ }
256
+ return null;
257
+ },
258
+ },
259
+ {
260
+ id: 'cwv-inp-heavy-animations',
261
+ name: 'Heavy Animations',
262
+ category: 'performance',
263
+ severity: 'info',
264
+ description: 'Animations should use transform/opacity for best performance',
265
+ check: (ctx) => {
266
+ if (!ctx.hasHeavyAnimations)
267
+ return null;
268
+ return createResult({ id: 'cwv-inp-heavy-animations', name: 'Heavy Animations', category: 'performance', severity: 'info' }, 'info', 'Potentially expensive animations detected', {
269
+ recommendation: 'Use transform and opacity for animations, avoid layout properties',
270
+ evidence: {
271
+ issue: 'Animating width, height, top, left triggers layout',
272
+ expected: 'Animate transform: translate(), scale(), rotate() and opacity',
273
+ learnMore: 'https://web.dev/animations-guide/',
274
+ },
275
+ });
276
+ },
277
+ },
278
+ {
279
+ id: 'cwv-resource-hints',
280
+ name: 'Resource Hints',
281
+ category: 'performance',
282
+ severity: 'info',
283
+ description: 'Use resource hints to speed up critical resources',
284
+ check: (ctx) => {
285
+ const hints = [];
286
+ if (!ctx.hasPreconnect && ctx.externalOrigins && ctx.externalOrigins > 2) {
287
+ hints.push('preconnect for critical third-party origins');
288
+ }
289
+ if (!ctx.hasDnsPrefetch && ctx.externalOrigins && ctx.externalOrigins > 3) {
290
+ hints.push('dns-prefetch for secondary origins');
291
+ }
292
+ if (!ctx.hasPreload && ctx.hasCriticalResources) {
293
+ hints.push('preload for critical CSS/fonts/images');
294
+ }
295
+ if (hints.length > 0) {
296
+ return createResult({ id: 'cwv-resource-hints', name: 'Resource Hints', category: 'performance', severity: 'info' }, 'info', `Consider adding: ${hints.join(', ')}`, {
297
+ recommendation: 'Add resource hints in <head> for faster loading',
298
+ evidence: {
299
+ expected: hints,
300
+ example: `<link rel="preconnect" href="https://fonts.googleapis.com">
301
+ <link rel="dns-prefetch" href="https://analytics.example.com">
302
+ <link rel="preload" as="font" href="/fonts/main.woff2" crossorigin>`,
303
+ learnMore: 'https://web.dev/preconnect-and-dns-prefetch/',
304
+ },
305
+ });
306
+ }
307
+ return createResult({ id: 'cwv-resource-hints', name: 'Resource Hints', category: 'performance', severity: 'info' }, 'pass', 'Resource hints configured');
308
+ },
309
+ },
310
+ {
311
+ id: 'cwv-critical-css',
312
+ name: 'Critical CSS',
313
+ category: 'performance',
314
+ severity: 'info',
315
+ description: 'Inline critical CSS for above-the-fold content',
316
+ check: (ctx) => {
317
+ if (ctx.hasInlineCriticalCss) {
318
+ return createResult({ id: 'cwv-critical-css', name: 'Critical CSS', category: 'performance', severity: 'info' }, 'pass', 'Inline critical CSS detected');
319
+ }
320
+ if (ctx.renderBlockingStylesheets && ctx.renderBlockingStylesheets > 0) {
321
+ return createResult({ id: 'cwv-critical-css', name: 'Critical CSS', category: 'performance', severity: 'info' }, 'info', 'Consider inlining critical CSS', {
322
+ recommendation: 'Inline critical CSS in <head> and defer non-critical styles',
323
+ evidence: {
324
+ example: `<style>
325
+ /* Critical above-the-fold styles */
326
+ body { margin: 0; font-family: system-ui; }
327
+ </style>
328
+ <link rel="stylesheet" href="main.css" media="print" onload="this.media='all'">`,
329
+ impact: 'Critical CSS eliminates render-blocking stylesheets',
330
+ learnMore: 'https://web.dev/extract-critical-css/',
331
+ },
332
+ });
333
+ }
334
+ return null;
335
+ },
336
+ },
337
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const ecommerceRules: SeoRule[];
@@ -0,0 +1,252 @@
1
+ import { createResult } from './types.js';
2
+ export const ecommerceRules = [
3
+ {
4
+ id: 'ecommerce-product-schema',
5
+ name: 'Product Schema',
6
+ category: 'structured-data',
7
+ severity: 'warning',
8
+ description: 'Product pages should have Product schema for rich snippets',
9
+ check: (ctx) => {
10
+ if (!ctx.jsonLdTypes)
11
+ return null;
12
+ if (!ctx.isProductPage)
13
+ return null;
14
+ const hasProduct = ctx.jsonLdTypes.includes('Product');
15
+ if (!hasProduct) {
16
+ return createResult({ id: 'ecommerce-product-schema', name: 'Product Schema', category: 'structured-data', severity: 'warning' }, 'warn', 'Product page missing Product schema', {
17
+ recommendation: 'Add Product structured data for rich snippets in search results',
18
+ evidence: {
19
+ expected: 'Product schema with name, image, price, availability',
20
+ example: `{
21
+ "@type": "Product",
22
+ "name": "Product Name",
23
+ "image": "https://example.com/image.jpg",
24
+ "offers": {
25
+ "@type": "Offer",
26
+ "price": "99.99",
27
+ "priceCurrency": "USD",
28
+ "availability": "https://schema.org/InStock"
29
+ }
30
+ }`,
31
+ learnMore: 'https://developers.google.com/search/docs/appearance/structured-data/product',
32
+ },
33
+ });
34
+ }
35
+ return createResult({ id: 'ecommerce-product-schema', name: 'Product Schema', category: 'structured-data', severity: 'warning' }, 'pass', 'Product schema found');
36
+ },
37
+ },
38
+ {
39
+ id: 'ecommerce-product-price',
40
+ name: 'Product Price',
41
+ category: 'structured-data',
42
+ severity: 'warning',
43
+ description: 'Product schema should include price information',
44
+ check: (ctx) => {
45
+ if (!ctx.productSchema)
46
+ return null;
47
+ const hasPrice = ctx.productSchema.offers?.price !== undefined ||
48
+ ctx.productSchema.offers?.lowPrice !== undefined;
49
+ const hasCurrency = ctx.productSchema.offers?.priceCurrency !== undefined;
50
+ if (!hasPrice) {
51
+ return createResult({ id: 'ecommerce-product-price', name: 'Product Price', category: 'structured-data', severity: 'warning' }, 'warn', 'Product schema missing price', {
52
+ recommendation: 'Add price to Product offers for price display in search results',
53
+ evidence: {
54
+ expected: 'offers.price or offers.lowPrice with priceCurrency',
55
+ impact: 'Products without price may not show in Google Shopping results',
56
+ },
57
+ });
58
+ }
59
+ if (!hasCurrency) {
60
+ return createResult({ id: 'ecommerce-product-price', name: 'Product Price', category: 'structured-data', severity: 'warning' }, 'warn', 'Product schema missing currency', {
61
+ recommendation: 'Add priceCurrency (e.g., USD, EUR, BRL) to offers',
62
+ evidence: {
63
+ found: `price: ${ctx.productSchema.offers?.price}`,
64
+ expected: 'priceCurrency: "USD" or similar ISO 4217 code',
65
+ },
66
+ });
67
+ }
68
+ return createResult({ id: 'ecommerce-product-price', name: 'Product Price', category: 'structured-data', severity: 'warning' }, 'pass', `Price: ${ctx.productSchema.offers?.priceCurrency} ${ctx.productSchema.offers?.price || ctx.productSchema.offers?.lowPrice}`);
69
+ },
70
+ },
71
+ {
72
+ id: 'ecommerce-product-availability',
73
+ name: 'Product Availability',
74
+ category: 'structured-data',
75
+ severity: 'info',
76
+ description: 'Product schema should include availability status',
77
+ check: (ctx) => {
78
+ if (!ctx.productSchema?.offers)
79
+ return null;
80
+ const availability = ctx.productSchema.offers.availability;
81
+ if (!availability) {
82
+ return createResult({ id: 'ecommerce-product-availability', name: 'Product Availability', category: 'structured-data', severity: 'info' }, 'info', 'Product schema missing availability', {
83
+ recommendation: 'Add availability to help users know if product is in stock',
84
+ evidence: {
85
+ expected: 'availability: "https://schema.org/InStock" or similar',
86
+ example: 'InStock, OutOfStock, PreOrder, BackOrder, Discontinued',
87
+ learnMore: 'https://schema.org/ItemAvailability',
88
+ },
89
+ });
90
+ }
91
+ const availType = availability.replace('https://schema.org/', '').replace('http://schema.org/', '');
92
+ return createResult({ id: 'ecommerce-product-availability', name: 'Product Availability', category: 'structured-data', severity: 'info' }, 'pass', `Availability: ${availType}`);
93
+ },
94
+ },
95
+ {
96
+ id: 'ecommerce-product-image',
97
+ name: 'Product Image',
98
+ category: 'structured-data',
99
+ severity: 'warning',
100
+ description: 'Product schema should include high-quality images',
101
+ check: (ctx) => {
102
+ if (!ctx.productSchema)
103
+ return null;
104
+ const hasImage = ctx.productSchema.image !== undefined;
105
+ if (!hasImage) {
106
+ return createResult({ id: 'ecommerce-product-image', name: 'Product Image', category: 'structured-data', severity: 'warning' }, 'warn', 'Product schema missing image', {
107
+ recommendation: 'Add product images for visual search results',
108
+ evidence: {
109
+ expected: 'At least one high-quality product image',
110
+ impact: 'Products without images are less likely to appear in image search and shopping results',
111
+ },
112
+ });
113
+ }
114
+ const imageCount = Array.isArray(ctx.productSchema.image)
115
+ ? ctx.productSchema.image.length
116
+ : 1;
117
+ return createResult({ id: 'ecommerce-product-image', name: 'Product Image', category: 'structured-data', severity: 'warning' }, 'pass', `${imageCount} product image(s) in schema`);
118
+ },
119
+ },
120
+ {
121
+ id: 'ecommerce-product-reviews',
122
+ name: 'Product Reviews',
123
+ category: 'structured-data',
124
+ severity: 'info',
125
+ description: 'Product schema can include aggregate ratings for star snippets',
126
+ check: (ctx) => {
127
+ if (!ctx.productSchema)
128
+ return null;
129
+ const hasRating = ctx.productSchema.aggregateRating !== undefined;
130
+ const hasReviews = ctx.productSchema.review !== undefined;
131
+ if (!hasRating && !hasReviews) {
132
+ return createResult({ id: 'ecommerce-product-reviews', name: 'Product Reviews', category: 'structured-data', severity: 'info' }, 'info', 'Product schema has no reviews or ratings', {
133
+ recommendation: 'Add aggregateRating for star ratings in search results',
134
+ evidence: {
135
+ example: `"aggregateRating": {
136
+ "@type": "AggregateRating",
137
+ "ratingValue": "4.5",
138
+ "reviewCount": "42"
139
+ }`,
140
+ impact: 'Star ratings in search results can improve click-through rate by 20-30%',
141
+ },
142
+ });
143
+ }
144
+ if (hasRating && ctx.productSchema.aggregateRating) {
145
+ const rating = ctx.productSchema.aggregateRating;
146
+ return createResult({ id: 'ecommerce-product-reviews', name: 'Product Reviews', category: 'structured-data', severity: 'info' }, 'pass', `Rating: ${rating.ratingValue ?? '?'}/5 (${rating.reviewCount ?? rating.ratingCount ?? '?'} reviews)`);
147
+ }
148
+ return createResult({ id: 'ecommerce-product-reviews', name: 'Product Reviews', category: 'structured-data', severity: 'info' }, 'pass', 'Product has review data');
149
+ },
150
+ },
151
+ {
152
+ id: 'ecommerce-product-brand',
153
+ name: 'Product Brand',
154
+ category: 'structured-data',
155
+ severity: 'info',
156
+ description: 'Product schema should include brand information',
157
+ check: (ctx) => {
158
+ if (!ctx.productSchema)
159
+ return null;
160
+ const hasBrand = ctx.productSchema.brand !== undefined;
161
+ if (!hasBrand) {
162
+ return createResult({ id: 'ecommerce-product-brand', name: 'Product Brand', category: 'structured-data', severity: 'info' }, 'info', 'Product schema missing brand', {
163
+ recommendation: 'Add brand for better product identification',
164
+ evidence: {
165
+ example: `"brand": { "@type": "Brand", "name": "Brand Name" }`,
166
+ impact: 'Brand helps Google understand product context for shopping queries',
167
+ },
168
+ });
169
+ }
170
+ const brandName = typeof ctx.productSchema.brand === 'string'
171
+ ? ctx.productSchema.brand
172
+ : ctx.productSchema.brand?.name;
173
+ return createResult({ id: 'ecommerce-product-brand', name: 'Product Brand', category: 'structured-data', severity: 'info' }, 'pass', `Brand: ${brandName || 'specified'}`);
174
+ },
175
+ },
176
+ {
177
+ id: 'ecommerce-product-sku',
178
+ name: 'Product Identifiers',
179
+ category: 'structured-data',
180
+ severity: 'info',
181
+ description: 'Product schema should include unique identifiers (SKU, GTIN, MPN)',
182
+ check: (ctx) => {
183
+ if (!ctx.productSchema)
184
+ return null;
185
+ const hasSku = ctx.productSchema.sku !== undefined;
186
+ const hasGtin = ctx.productSchema.gtin !== undefined ||
187
+ ctx.productSchema.gtin13 !== undefined ||
188
+ ctx.productSchema.gtin14 !== undefined ||
189
+ ctx.productSchema.gtin8 !== undefined;
190
+ const hasMpn = ctx.productSchema.mpn !== undefined;
191
+ const identifiers = [];
192
+ if (hasSku)
193
+ identifiers.push('SKU');
194
+ if (hasGtin)
195
+ identifiers.push('GTIN');
196
+ if (hasMpn)
197
+ identifiers.push('MPN');
198
+ if (identifiers.length === 0) {
199
+ return createResult({ id: 'ecommerce-product-sku', name: 'Product Identifiers', category: 'structured-data', severity: 'info' }, 'info', 'Product schema missing identifiers', {
200
+ recommendation: 'Add SKU, GTIN, or MPN for better product matching',
201
+ evidence: {
202
+ expected: 'At least one of: sku, gtin, gtin13, gtin14, gtin8, mpn',
203
+ impact: 'Product identifiers help Google match products across retailers',
204
+ learnMore: 'https://support.google.com/merchants/answer/6324461',
205
+ },
206
+ });
207
+ }
208
+ return createResult({ id: 'ecommerce-product-sku', name: 'Product Identifiers', category: 'structured-data', severity: 'info' }, 'pass', `Identifiers: ${identifiers.join(', ')}`);
209
+ },
210
+ },
211
+ {
212
+ id: 'ecommerce-offer-valid-dates',
213
+ name: 'Offer Valid Dates',
214
+ category: 'structured-data',
215
+ severity: 'info',
216
+ description: 'Time-sensitive offers should have valid date ranges',
217
+ check: (ctx) => {
218
+ if (!ctx.productSchema?.offers)
219
+ return null;
220
+ const offers = ctx.productSchema.offers;
221
+ const priceValidUntil = offers.priceValidUntil;
222
+ const validFrom = offers.validFrom;
223
+ const validThrough = offers.validThrough;
224
+ if (priceValidUntil) {
225
+ const endDate = new Date(priceValidUntil);
226
+ const now = new Date();
227
+ if (endDate < now) {
228
+ return createResult({ id: 'ecommerce-offer-valid-dates', name: 'Offer Valid Dates', category: 'structured-data', severity: 'info' }, 'warn', 'Offer priceValidUntil date has passed', {
229
+ evidence: {
230
+ found: priceValidUntil,
231
+ issue: 'Expired offer dates should be updated or removed',
232
+ impact: 'Expired dates may cause Google to distrust your pricing data',
233
+ },
234
+ });
235
+ }
236
+ }
237
+ if (validThrough) {
238
+ const endDate = new Date(validThrough);
239
+ const now = new Date();
240
+ if (endDate < now) {
241
+ return createResult({ id: 'ecommerce-offer-valid-dates', name: 'Offer Valid Dates', category: 'structured-data', severity: 'info' }, 'warn', 'Offer validThrough date has passed', {
242
+ evidence: {
243
+ found: validThrough,
244
+ issue: 'Expired validity dates should be updated',
245
+ },
246
+ });
247
+ }
248
+ }
249
+ return null;
250
+ },
251
+ },
252
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const i18nRules: SeoRule[];