@thg-altitude/schemaorg 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 18.15.0
1
+ 22.15.0
package/index.js CHANGED
@@ -6,8 +6,9 @@ import Organization from './src/components/Organization.astro'
6
6
  import ProductCollection from './src/components/ProductCollection.astro'
7
7
  import WebSite from './src/components/WebSite.astro'
8
8
  import EnhancedProductList from './src/components/EnhancedProductList.astro'
9
+ import Recipe from './src/components/Recipe.astro'
9
10
 
10
- export {
11
+ export {
11
12
  Product,
12
13
  EnhancedProduct,
13
14
  Breadcrumb,
@@ -15,5 +16,6 @@ export {
15
16
  Organization,
16
17
  ProductCollection,
17
18
  WebSite,
18
- EnhancedProductList
19
+ EnhancedProductList,
20
+ Recipe
19
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thg-altitude/schemaorg",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { mapProductSchemaDataEnhanced } from '../utils/productSchema.js';
3
+
2
4
  const {
3
5
  image,
4
6
  sku,
@@ -23,8 +25,32 @@ const {
23
25
  streetAddress,
24
26
  addressLocality,
25
27
  postalCode,
28
+ suggestedAge,
29
+ suggestedGender,
30
+ beauty_targetAge,
31
+ gender,
26
32
  } = Astro.props;
27
33
 
34
+ // Use the enhanced mapping function to extract data from the full product data
35
+ const mappedData = mapProductSchemaDataEnhanced(Astro.props);
36
+
37
+ // Extract suggestedGender and suggestedAge using the comprehensive mapping function
38
+ let extractedSuggestedGender = suggestedGender || mappedData.suggestedGender;
39
+ let extractedSuggestedAge = suggestedAge || mappedData.suggestedAge;
40
+
41
+ // Fallback to the original simple extraction if the mapping didn't find anything
42
+ if (!extractedSuggestedGender && Astro.props.productContent?.propertyNameToValueMap?.gender) {
43
+ extractedSuggestedGender = Array.isArray(Astro.props.productContent.propertyNameToValueMap.gender)
44
+ ? Astro.props.productContent.propertyNameToValueMap.gender[0]
45
+ : Astro.props.productContent.propertyNameToValueMap.gender;
46
+ }
47
+
48
+ if (!extractedSuggestedAge && Astro.props.productContent?.propertyNameToValueMap?.beauty_targetAge) {
49
+ extractedSuggestedAge = Array.isArray(Astro.props.productContent.propertyNameToValueMap.beauty_targetAge)
50
+ ? Astro.props.productContent.propertyNameToValueMap.beauty_targetAge[0]
51
+ : Astro.props.productContent.propertyNameToValueMap.beauty_targetAge;
52
+ }
53
+
28
54
  let url;
29
55
  let schema = null;
30
56
  let schemaError = false;
@@ -63,49 +89,258 @@ try {
63
89
  }
64
90
 
65
91
  function generateProductSchema() {
92
+ const hasMultipleVariants = variants && variants.length > 1;
93
+ let variantsArray = [];
94
+
66
95
  if (variants && variants.length > 0 && currency) {
67
- offersArray = variants
68
- .filter((variant) => variant?.sku && variant?.price?.price?.amount)
69
- .map((variant) => ({
70
- "@type": "Offer",
71
- sku: variant.sku.toString(),
72
- url:
73
- variants.length > 1
74
- ? `${pageUrl}?variation=${variant?.sku}`
75
- : pageUrl,
76
- price: variant.price.price.amount,
77
- priceCurrency: currency,
78
- itemCondition: "http://schema.org/NewCondition",
79
- availability: variant?.inStock
80
- ? "https://schema.org/InStock"
81
- : "https://schema.org/OutOfStock",
82
- ...(variant?.barcode && { gtin13: variant.barcode.toString() }),
83
- ...(brand && {
84
- seller: {
85
- "@type": "Organization",
86
- name: brand,
87
- },
88
- }),
89
- priceSpecification: [
90
- {
91
- "@type": "UnitPriceSpecification",
92
- priceCurrency: currency,
93
- price: variant.price.price.amount,
94
- valueAddedTaxIncluded: true,
95
- },
96
- ...(variant?.price?.rrp?.amount
97
- ? [
96
+ if (hasMultipleVariants) {
97
+ // Create individual Product objects for each variant
98
+ variantsArray = variants
99
+ .filter((variant) => variant?.sku && variant?.price?.price?.amount)
100
+ .map((variant) => {
101
+ // Build variant name with attributes from choices in logical order
102
+ let variantName = name;
103
+ const attributes = [];
104
+
105
+ // Extract properties in preferred order: Amount + Weight + Flavor + Size + Color + Material
106
+ if (variant.choices && variant.choices.length > 0) {
107
+ // Amount (only if explicitly marked as Amount, not weight-related)
108
+ const amountChoice = variant.choices.find(c =>
109
+ c.optionKey === 'Amount' &&
110
+ c.title &&
111
+ !c.title.includes('kg') &&
112
+ !c.title.includes('g') &&
113
+ !c.title.includes('lb')
114
+ );
115
+ if (amountChoice && amountChoice.title) attributes.push(amountChoice.title);
116
+
117
+ // Weight (from Weight optionKey OR size containing weight units)
118
+ const weightChoice = variant.choices.find(c =>
119
+ c.optionKey === 'Weight' ||
120
+ (c.optionKey === 'Size' && c.title && (c.title.includes('kg') || c.title.includes('g') || c.title.includes('lb'))) ||
121
+ (c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title)))
122
+ );
123
+ if (weightChoice && weightChoice.title) attributes.push(weightChoice.title);
124
+
125
+ // Flavor
126
+ const flavorChoice = variant.choices.find(c =>
127
+ c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste'
128
+ );
129
+ if (flavorChoice && flavorChoice.title) attributes.push(flavorChoice.title);
130
+
131
+ // Size (excluding weight-related sizes and already processed weight)
132
+ const sizeChoice = variant.choices.find(c =>
133
+ c.optionKey === 'Size' &&
134
+ c.title &&
135
+ !c.title.includes('kg') &&
136
+ !c.title.includes('g') &&
137
+ !c.title.includes('lb') &&
138
+ c !== weightChoice // Don't duplicate if already used as weight
139
+ );
140
+ if (sizeChoice && sizeChoice.title) attributes.push(sizeChoice.title);
141
+
142
+ // Color
143
+ const colorChoice = variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour);
144
+ if (colorChoice) {
145
+ if (colorChoice.title) attributes.push(colorChoice.title);
146
+ else if (colorChoice.colour) attributes.push(colorChoice.colour);
147
+ }
148
+
149
+ // Material
150
+ const materialChoice = variant.choices.find(c => c.optionKey === 'Material');
151
+ if (materialChoice && materialChoice.title) attributes.push(materialChoice.title);
152
+ }
153
+
154
+ if (attributes.length > 0) {
155
+ variantName = `${name} ${attributes.join(' ')}`;
156
+ }
157
+
158
+ return {
159
+ "@type": "Product",
160
+ sku: variant.sku.toString(),
161
+ name: variantName,
162
+ ...(description && { description }),
163
+ ...(imageUrl && { image: imageUrl }),
164
+ // Extract variant-specific properties from choices using correct schema.org properties
165
+ ...(variant.choices && variant.choices.some(c => c.optionKey === 'Weight' || (c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title)))) && {
166
+ weight: variant.choices.find(c => c.optionKey === 'Weight' || (c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title))))?.title
167
+ }),
168
+ ...(variant.choices && variant.choices.some(c => c.optionKey === 'Size' && c.title && !c.title.includes('kg') && !c.title.includes('g') && !c.title.includes('lb')) && {
169
+ size: variant.choices.find(c => c.optionKey === 'Size' && c.title && !c.title.includes('kg') && !c.title.includes('g') && !c.title.includes('lb'))?.title
170
+ }),
171
+ ...(variant.choices && variant.choices.some(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour) && {
172
+ color: variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour')?.title ||
173
+ variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour)?.colour
174
+ }),
175
+ ...(variant.choices && variant.choices.some(c => c.optionKey === 'Material') && {
176
+ material: variant.choices.find(c => c.optionKey === 'Material')?.title
177
+ }),
178
+ // Handle non-standard properties (amount, flavour) as additionalProperty
179
+ ...(() => {
180
+ const additionalProperties = [];
181
+
182
+ // Amount
183
+ const amountChoice = variant.choices?.find(c => c.optionKey === 'Amount');
184
+ if (amountChoice?.title) {
185
+ additionalProperties.push({
186
+ "@type": "PropertyValue",
187
+ "name": "amount",
188
+ "value": amountChoice.title
189
+ });
190
+ }
191
+
192
+ // Flavour (as additionalProperty since it doesn't exist in schema.org)
193
+ const flavourChoice = variant.choices?.find(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste');
194
+ if (flavourChoice?.title) {
195
+ additionalProperties.push({
196
+ "@type": "PropertyValue",
197
+ "name": "flavour",
198
+ "value": flavourChoice.title
199
+ });
200
+ }
201
+
202
+ return additionalProperties.length > 0 ? { additionalProperty: additionalProperties } : {};
203
+ })(),
204
+ ...(variant?.barcode && { gtin13: variant.barcode.toString() }),
205
+ offers: {
206
+ "@type": "Offer",
207
+ url: `${pageUrl}?variation=${variant?.sku}`,
208
+ price: variant.price.price.amount,
209
+ priceCurrency: currency,
210
+ itemCondition: "https://schema.org/NewCondition",
211
+ availability: variant?.inStock
212
+ ? "https://schema.org/InStock"
213
+ : "https://schema.org/OutOfStock",
214
+ ...(brand && {
215
+ seller: {
216
+ "@type": "Organization",
217
+ name: brand,
218
+ },
219
+ }),
220
+ priceSpecification: [
98
221
  {
99
222
  "@type": "UnitPriceSpecification",
100
223
  priceCurrency: currency,
101
- price: variant.price.rrp.amount,
224
+ price: variant.price.price.amount,
102
225
  valueAddedTaxIncluded: true,
103
- priceType: "https://schema.org/ListPrice",
104
226
  },
105
- ]
106
- : []),
107
- ],
108
- }));
227
+ ...(variant?.price?.rrp?.amount
228
+ ? [
229
+ {
230
+ "@type": "UnitPriceSpecification",
231
+ priceCurrency: currency,
232
+ price: variant.price.rrp.amount,
233
+ valueAddedTaxIncluded: true,
234
+ priceType: "https://schema.org/StrikethroughPrice",
235
+ },
236
+ ]
237
+ : []),
238
+ ],
239
+ }
240
+ };
241
+ });
242
+ } else {
243
+ // Single variant - create offers array as before
244
+ offersArray = variants
245
+ .filter((variant) => variant?.sku && variant?.price?.price?.amount)
246
+ .map((variant) => {
247
+ // Build variant name with attributes from choices in logical order
248
+ let variantName = name;
249
+ const attributes = [];
250
+
251
+ // Extract properties in preferred order: Amount + Weight + Flavor + Size + Color + Material
252
+ if (variant.choices && variant.choices.length > 0) {
253
+ // Amount (only if explicitly marked as Amount, not weight-related)
254
+ const amountChoice = variant.choices.find(c =>
255
+ c.optionKey === 'Amount' &&
256
+ c.title &&
257
+ !c.title.includes('kg') &&
258
+ !c.title.includes('g') &&
259
+ !c.title.includes('lb')
260
+ );
261
+ if (amountChoice && amountChoice.title) attributes.push(amountChoice.title);
262
+
263
+ // Weight (from Weight optionKey OR size containing weight units)
264
+ const weightChoice = variant.choices.find(c =>
265
+ c.optionKey === 'Weight' ||
266
+ (c.optionKey === 'Size' && c.title && (c.title.includes('kg') || c.title.includes('g') || c.title.includes('lb'))) ||
267
+ (c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title)))
268
+ );
269
+ if (weightChoice && weightChoice.title) attributes.push(weightChoice.title);
270
+
271
+ // Flavor
272
+ const flavorChoice = variant.choices.find(c =>
273
+ c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste'
274
+ );
275
+ if (flavorChoice && flavorChoice.title) attributes.push(flavorChoice.title);
276
+
277
+ // Size (excluding weight-related sizes and already processed weight)
278
+ const sizeChoice = variant.choices.find(c =>
279
+ c.optionKey === 'Size' &&
280
+ c.title &&
281
+ !c.title.includes('kg') &&
282
+ !c.title.includes('g') &&
283
+ !c.title.includes('lb') &&
284
+ c !== weightChoice // Don't duplicate if already used as weight
285
+ );
286
+ if (sizeChoice && sizeChoice.title) attributes.push(sizeChoice.title);
287
+
288
+ // Color
289
+ const colorChoice = variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour);
290
+ if (colorChoice) {
291
+ if (colorChoice.title) attributes.push(colorChoice.title);
292
+ else if (colorChoice.colour) attributes.push(colorChoice.colour);
293
+ }
294
+
295
+ // Material
296
+ const materialChoice = variant.choices.find(c => c.optionKey === 'Material');
297
+ if (materialChoice && materialChoice.title) attributes.push(materialChoice.title);
298
+ }
299
+
300
+ if (attributes.length > 0) {
301
+ variantName = `${name} ${attributes.join(' ')}`;
302
+ }
303
+
304
+ return {
305
+ "@type": "Offer",
306
+ name: variantName,
307
+ sku: variant.sku.toString(),
308
+ url: pageUrl,
309
+ price: variant.price.price.amount,
310
+ priceCurrency: currency,
311
+ itemCondition: "https://schema.org/NewCondition",
312
+ availability: variant?.inStock
313
+ ? "https://schema.org/InStock"
314
+ : "https://schema.org/OutOfStock",
315
+ ...(variant?.barcode && { gtin13: variant.barcode.toString() }),
316
+ ...(brand && {
317
+ seller: {
318
+ "@type": "Organization",
319
+ name: brand,
320
+ },
321
+ }),
322
+ priceSpecification: [
323
+ {
324
+ "@type": "UnitPriceSpecification",
325
+ priceCurrency: currency,
326
+ price: variant.price.price.amount,
327
+ valueAddedTaxIncluded: true,
328
+ },
329
+ ...(variant?.price?.rrp?.amount
330
+ ? [
331
+ {
332
+ "@type": "UnitPriceSpecification",
333
+ priceCurrency: currency,
334
+ price: variant.price.rrp.amount,
335
+ valueAddedTaxIncluded: true,
336
+ priceType: "https://schema.org/StrikethroughPrice",
337
+ },
338
+ ]
339
+ : []),
340
+ ],
341
+ };
342
+ });
343
+ }
109
344
  }
110
345
 
111
346
  const category =
@@ -116,38 +351,199 @@ function generateProductSchema() {
116
351
  const reviewsArray = generateReviewsSchema();
117
352
 
118
353
  if (name && sku) {
119
- const productSchema = {
120
- "@type": "Product",
121
- "@id": pageUrl,
122
- url: pageUrl,
123
- sku: sku.toString(),
124
- name: name,
125
- ...(gtin13 && { gtin13: gtin13.toString() }),
126
- ...(description && { description }),
127
- ...(imageUrl && { image: imageUrl }),
128
- ...(brand && {
129
- brand: {
130
- "@type": "Brand",
131
- name: brand,
132
- },
133
- }),
134
- ...(reviewsCount > 0 &&
135
- reviewsAverage && {
136
- aggregateRating: {
137
- "@type": "AggregateRating",
138
- ratingValue: reviewsAverage,
139
- reviewCount: reviewsCount,
140
- bestRating: 5,
141
- worstRating: 1,
354
+ const hasMultipleVariants = variants && variants.length > 1;
355
+
356
+ if (hasMultipleVariants) {
357
+ // Determine what properties vary between variants
358
+ const variesBy = [];
359
+
360
+ // Check what attributes exist across variants based on choices using correct schema.org properties
361
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Weight' || (c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title)))))) {
362
+ variesBy.push("https://schema.org/weight");
363
+ }
364
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Size' && c.title && !c.title.includes('kg') && !c.title.includes('g') && !c.title.includes('lb')))) {
365
+ variesBy.push("https://schema.org/size");
366
+ }
367
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour))) {
368
+ variesBy.push("https://schema.org/color");
369
+ }
370
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Material'))) {
371
+ variesBy.push("https://schema.org/material");
372
+ }
373
+ // For non-standard properties like amount and flavour, use additionalProperty
374
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Amount'))) {
375
+ variesBy.push("https://schema.org/additionalProperty");
376
+ }
377
+ if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste'))) {
378
+ variesBy.push("https://schema.org/additionalProperty");
379
+ }
380
+
381
+ const productGroupSchema = {
382
+ "@type": "ProductGroup",
383
+ "@id": pageUrl,
384
+ url: pageUrl,
385
+ name: name,
386
+ productGroupID: sku.toString(),
387
+ ...(gtin13 && { gtin13: gtin13.toString() }),
388
+ ...(description && { description }),
389
+ ...(imageUrl && { image: imageUrl }),
390
+ ...(brand && {
391
+ brand: {
392
+ "@type": "Brand",
393
+ name: brand,
142
394
  },
143
395
  }),
144
- ...(reviewsArray && reviewsArray.length > 0 && { review: reviewsArray }),
145
- ...(category && { category }),
146
- ...(keywords && { keywords }),
147
- ...(offersArray.length > 0 && { offers: offersArray }),
148
- };
396
+ ...(variesBy.length > 0 && { variesBy }),
397
+ hasVariant: variantsArray,
398
+ ...(reviewsCount > 0 &&
399
+ reviewsAverage && {
400
+ aggregateRating: {
401
+ "@type": "AggregateRating",
402
+ ratingValue: reviewsAverage,
403
+ reviewCount: reviewsCount,
404
+ bestRating: 5,
405
+ worstRating: 1,
406
+ },
407
+ }),
408
+ ...(reviewsArray && reviewsArray.length > 0 && { review: reviewsArray }),
409
+ ...(category && { category }),
410
+ ...(keywords && { keywords }),
411
+ // Handle custom properties as additionalProperty
412
+ ...(() => {
413
+ const additionalProperties = [];
414
+ if (extractedSuggestedGender) {
415
+ additionalProperties.push({
416
+ "@type": "PropertyValue",
417
+ "name": "suggestedGender",
418
+ "value": extractedSuggestedGender
419
+ });
420
+ }
421
+ if (extractedSuggestedAge) {
422
+ additionalProperties.push({
423
+ "@type": "PropertyValue",
424
+ "name": "suggestedAge",
425
+ "value": extractedSuggestedAge
426
+ });
427
+ }
428
+ if (beauty_targetAge) {
429
+ additionalProperties.push({
430
+ "@type": "PropertyValue",
431
+ "name": "targetAge",
432
+ "value": beauty_targetAge
433
+ });
434
+ }
435
+ if (gender) {
436
+ additionalProperties.push({
437
+ "@type": "PropertyValue",
438
+ "name": "GenderType",
439
+ "value": gender
440
+ });
441
+ }
442
+
443
+ // Add flavour as additionalProperty for ProductGroup level
444
+ const flavourValues = variants?.map(v =>
445
+ v.choices?.find(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste')?.title
446
+ ).filter(Boolean);
447
+
448
+ if (flavourValues && flavourValues.length > 0) {
449
+ // If multiple unique flavours, list them all; if single flavour, just that one
450
+ const uniqueFlavours = [...new Set(flavourValues)];
451
+ additionalProperties.push({
452
+ "@type": "PropertyValue",
453
+ "name": "flavour",
454
+ "value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
455
+ });
456
+ }
457
+
458
+ return additionalProperties.length > 0 ? { additionalProperty: additionalProperties } : {};
459
+ })(),
460
+ };
149
461
 
150
- graphArray.push(productSchema);
462
+ graphArray.push(productGroupSchema);
463
+ } else {
464
+ // Single product schema as before
465
+ const productSchema = {
466
+ "@type": "Product",
467
+ "@id": pageUrl,
468
+ url: pageUrl,
469
+ sku: sku.toString(),
470
+ name: name,
471
+ ...(gtin13 && { gtin13: gtin13.toString() }),
472
+ ...(description && { description }),
473
+ ...(imageUrl && { image: imageUrl }),
474
+ ...(brand && {
475
+ brand: {
476
+ "@type": "Brand",
477
+ name: brand,
478
+ },
479
+ }),
480
+ ...(reviewsCount > 0 &&
481
+ reviewsAverage && {
482
+ aggregateRating: {
483
+ "@type": "AggregateRating",
484
+ ratingValue: reviewsAverage,
485
+ reviewCount: reviewsCount,
486
+ bestRating: 5,
487
+ worstRating: 1,
488
+ },
489
+ }),
490
+ ...(reviewsArray && reviewsArray.length > 0 && { review: reviewsArray }),
491
+ ...(category && { category }),
492
+ ...(keywords && { keywords }),
493
+ // Handle custom properties as additionalProperty
494
+ ...(() => {
495
+ const additionalProperties = [];
496
+ if (extractedSuggestedGender) {
497
+ additionalProperties.push({
498
+ "@type": "PropertyValue",
499
+ "name": "suggestedGender",
500
+ "value": extractedSuggestedGender
501
+ });
502
+ }
503
+ if (extractedSuggestedAge) {
504
+ additionalProperties.push({
505
+ "@type": "PropertyValue",
506
+ "name": "suggestedAge",
507
+ "value": extractedSuggestedAge
508
+ });
509
+ }
510
+ if (beauty_targetAge) {
511
+ additionalProperties.push({
512
+ "@type": "PropertyValue",
513
+ "name": "beauty_targetAge",
514
+ "value": beauty_targetAge
515
+ });
516
+ }
517
+ if (gender) {
518
+ additionalProperties.push({
519
+ "@type": "PropertyValue",
520
+ "name": "gender",
521
+ "value": gender
522
+ });
523
+ }
524
+
525
+ // Add flavour as additionalProperty for single product
526
+ const flavourValues = variants?.map(v =>
527
+ v.choices?.find(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste')?.title
528
+ ).filter(Boolean);
529
+
530
+ if (flavourValues && flavourValues.length > 0) {
531
+ // If multiple unique flavours, list them all; if single flavour, just that one
532
+ const uniqueFlavours = [...new Set(flavourValues)];
533
+ additionalProperties.push({
534
+ "@type": "PropertyValue",
535
+ "name": "flavour",
536
+ "value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
537
+ });
538
+ }
539
+
540
+ return additionalProperties.length > 0 ? { additionalProperty: additionalProperties } : {};
541
+ })(),
542
+ ...(offersArray.length > 0 && { offers: offersArray }),
543
+ };
544
+
545
+ graphArray.push(productSchema);
546
+ }
151
547
  }
152
548
  }
153
549
 
@@ -281,6 +677,34 @@ function generateReviewsSchema() {
281
677
 
282
678
  {
283
679
  schema && !schemaError && (
284
- <script type="application/ld+json" set:html={JSON.stringify(schema)} />
680
+ <script type="application/ld+json" set:html={(() => {
681
+ // Clean the schema object by removing undefined, null, and empty values
682
+ function cleanObject(obj) {
683
+ if (obj === null || obj === undefined || obj === '') {
684
+ return undefined;
685
+ }
686
+
687
+ if (Array.isArray(obj)) {
688
+ const cleaned = obj.map(cleanObject).filter(item => item !== undefined);
689
+ return cleaned.length > 0 ? cleaned : undefined;
690
+ }
691
+
692
+ if (typeof obj === 'object' && obj !== null) {
693
+ const cleaned = {};
694
+ for (const [key, value] of Object.entries(obj)) {
695
+ const cleanedValue = cleanObject(value);
696
+ if (cleanedValue !== undefined) {
697
+ cleaned[key] = cleanedValue;
698
+ }
699
+ }
700
+ return Object.keys(cleaned).length > 0 ? cleaned : undefined;
701
+ }
702
+
703
+ return obj;
704
+ }
705
+
706
+ const cleanedSchema = cleanObject(schema);
707
+ return cleanedSchema ? JSON.stringify(cleanedSchema, null, 2) : '{}';
708
+ })()} />
285
709
  )
286
710
  }