@thg-altitude/schemaorg 1.0.35 → 1.0.37
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/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
import Recipe from './src/components/Recipe.astro'
|
|
10
|
+
import FAQ from './src/components/FAQ.astro'
|
|
10
11
|
|
|
11
12
|
export {
|
|
12
13
|
Product,
|
|
@@ -17,5 +18,6 @@ export {
|
|
|
17
18
|
ProductCollection,
|
|
18
19
|
WebSite,
|
|
19
20
|
EnhancedProductList,
|
|
21
|
+
FAQ,
|
|
20
22
|
Recipe
|
|
21
23
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { name, description } = Astro.props as Props;
|
|
8
|
+
|
|
2
9
|
let schema = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
"@context": "https://schema.org",
|
|
11
|
+
"@type": "CollectionPage",
|
|
12
|
+
"name": name,
|
|
13
|
+
"description": description,
|
|
7
14
|
};
|
|
8
15
|
---
|
|
9
16
|
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
suggestedGender,
|
|
30
30
|
beauty_targetAge,
|
|
31
31
|
gender,
|
|
32
|
+
weight
|
|
32
33
|
} = Astro.props;
|
|
33
34
|
|
|
34
35
|
// Use the enhanced mapping function to extract data from the full product data
|
|
@@ -64,7 +65,9 @@ try {
|
|
|
64
65
|
? Astro.url.origin
|
|
65
66
|
: `${Astro.request.headers.get("X-forwarded-Proto")}://${Astro.request.headers.get("X-forwarded-Host")?.split(", ")[0]}`;
|
|
66
67
|
imageUrl = image
|
|
67
|
-
?
|
|
68
|
+
? (import.meta.env.IMAGE_PROXY_URL
|
|
69
|
+
? `${import.meta.env.IMAGE_PROXY_URL}?url=${image}&format=webp&width=1500&height=1500&fit=cover`
|
|
70
|
+
: image)
|
|
68
71
|
: null;
|
|
69
72
|
pageUrl = urlPath ? `${url}${urlPath}` : url;
|
|
70
73
|
|
|
@@ -88,6 +91,40 @@ try {
|
|
|
88
91
|
schemaError = true;
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// Helper function to parse weight string into value and unit
|
|
95
|
+
function parseWeight(weightString) {
|
|
96
|
+
if (!weightString) return null;
|
|
97
|
+
|
|
98
|
+
// Match patterns like "5kg", "5 kg", "500g", "500 g", "10lb", "10 lb"
|
|
99
|
+
const match = weightString.match(/(\d+(?:\.\d+)?)\s*(kg|g|lb|oz)/i);
|
|
100
|
+
if (match) {
|
|
101
|
+
return {
|
|
102
|
+
value: parseFloat(match[1]),
|
|
103
|
+
unitText: match[2].toLowerCase()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Helper function to convert date from DD-MM-YYYY to ISO-8601 (YYYY-MM-DD)
|
|
110
|
+
function convertToISO8601(dateString) {
|
|
111
|
+
if (!dateString) return null;
|
|
112
|
+
|
|
113
|
+
// Check if already in ISO format (YYYY-MM-DD)
|
|
114
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(dateString)) {
|
|
115
|
+
return dateString;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Convert from DD-MM-YYYY to YYYY-MM-DD
|
|
119
|
+
const match = dateString.match(/^(\d{2})-(\d{2})-(\d{4})/);
|
|
120
|
+
if (match) {
|
|
121
|
+
const [, day, month, year] = match;
|
|
122
|
+
return `${year}-${month}-${day}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return dateString; // Return as-is if format not recognized
|
|
126
|
+
}
|
|
127
|
+
|
|
91
128
|
function generateProductSchema() {
|
|
92
129
|
const hasMultipleVariants = variants && variants.length > 1;
|
|
93
130
|
let variantsArray = [];
|
|
@@ -160,11 +197,71 @@ function generateProductSchema() {
|
|
|
160
197
|
sku: variant.sku.toString(),
|
|
161
198
|
name: variantName,
|
|
162
199
|
...(description && { description }),
|
|
163
|
-
...(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
200
|
+
...(() => {
|
|
201
|
+
// Try multiple possible locations for variant image and extract URL string
|
|
202
|
+
let variantImageUrl = variant.image || variant.imageUrl || variant.images?.[0] || variant.media?.[0]?.url || image;
|
|
203
|
+
|
|
204
|
+
// If image is an object, try to extract the URL string
|
|
205
|
+
if (variantImageUrl && typeof variantImageUrl === 'object') {
|
|
206
|
+
variantImageUrl = variantImageUrl.original || variantImageUrl.url || variantImageUrl.src || variantImageUrl.large || variantImageUrl.medium;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (variantImageUrl && typeof variantImageUrl === 'string') {
|
|
210
|
+
const finalImageUrl = import.meta.env.IMAGE_PROXY_URL
|
|
211
|
+
? `${import.meta.env.IMAGE_PROXY_URL}?url=${variantImageUrl}&format=webp&width=1500&height=1500&fit=cover`
|
|
212
|
+
: variantImageUrl;
|
|
213
|
+
return { image: finalImageUrl };
|
|
214
|
+
}
|
|
215
|
+
return {};
|
|
216
|
+
})(),
|
|
217
|
+
// Extract weight as QuantitativeValue
|
|
218
|
+
...(() => {
|
|
219
|
+
// First check if weight is at product level (beauty products - mediumProduct.weight)
|
|
220
|
+
const productWeight = weight || Astro.props.mediumProduct?.weight;
|
|
221
|
+
|
|
222
|
+
if (productWeight && typeof productWeight === 'object' && productWeight.value && productWeight.unit) {
|
|
223
|
+
return {
|
|
224
|
+
weight: {
|
|
225
|
+
"@type": "QuantitativeValue",
|
|
226
|
+
"value": parseFloat(productWeight.value),
|
|
227
|
+
"unitText": productWeight.unit.toLowerCase()
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if weight is at variant level
|
|
233
|
+
if (variant.weight && typeof variant.weight === 'object' && variant.weight.value && variant.weight.unit) {
|
|
234
|
+
return {
|
|
235
|
+
weight: {
|
|
236
|
+
"@type": "QuantitativeValue",
|
|
237
|
+
"value": parseFloat(variant.weight.value),
|
|
238
|
+
"unitText": variant.weight.unit.toLowerCase()
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Otherwise, try to parse from choices
|
|
244
|
+
const weightChoice = variant.choices?.find(c =>
|
|
245
|
+
c.optionKey === 'Weight' ||
|
|
246
|
+
(c.optionKey === 'Size' && c.title && (c.title.includes('kg') || c.title.includes('g') || c.title.includes('lb'))) ||
|
|
247
|
+
(c.title && (c.title.includes('kg') || c.title.includes('lb') || /\d+\s*g\b/.test(c.title)))
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (weightChoice?.title) {
|
|
251
|
+
const parsedWeight = parseWeight(weightChoice.title);
|
|
252
|
+
if (parsedWeight) {
|
|
253
|
+
return {
|
|
254
|
+
weight: {
|
|
255
|
+
"@type": "QuantitativeValue",
|
|
256
|
+
"value": parsedWeight.value,
|
|
257
|
+
"unitText": parsedWeight.unitText
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {};
|
|
264
|
+
})(),
|
|
168
265
|
...(variant.choices && variant.choices.some(c => c.optionKey === 'Size' && c.title && !c.title.includes('kg') && !c.title.includes('g') && !c.title.includes('lb')) && {
|
|
169
266
|
size: variant.choices.find(c => c.optionKey === 'Size' && c.title && !c.title.includes('kg') && !c.title.includes('g') && !c.title.includes('lb'))?.title
|
|
170
267
|
}),
|
|
@@ -175,20 +272,10 @@ function generateProductSchema() {
|
|
|
175
272
|
...(variant.choices && variant.choices.some(c => c.optionKey === 'Material') && {
|
|
176
273
|
material: variant.choices.find(c => c.optionKey === 'Material')?.title
|
|
177
274
|
}),
|
|
178
|
-
// Handle non-standard properties (
|
|
275
|
+
// Handle non-standard properties (flavour) as additionalProperty
|
|
179
276
|
...(() => {
|
|
180
277
|
const additionalProperties = [];
|
|
181
278
|
|
|
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
279
|
// Flavour (as additionalProperty since it doesn't exist in schema.org)
|
|
193
280
|
const flavourChoice = variant.choices?.find(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste');
|
|
194
281
|
if (flavourChoice?.title) {
|
|
@@ -224,7 +311,7 @@ function generateProductSchema() {
|
|
|
224
311
|
offers: {
|
|
225
312
|
"@type": "Offer",
|
|
226
313
|
url: `${pageUrl}?variation=${variant?.sku}`,
|
|
227
|
-
price: variant.price.price.amount,
|
|
314
|
+
price: parseFloat(variant.price.price.amount),
|
|
228
315
|
priceCurrency: currency,
|
|
229
316
|
itemCondition: "https://schema.org/NewCondition",
|
|
230
317
|
availability: variant?.inStock
|
|
@@ -240,7 +327,7 @@ function generateProductSchema() {
|
|
|
240
327
|
{
|
|
241
328
|
"@type": "UnitPriceSpecification",
|
|
242
329
|
priceCurrency: currency,
|
|
243
|
-
price: variant.price.price.amount,
|
|
330
|
+
price: parseFloat(variant.price.price.amount),
|
|
244
331
|
valueAddedTaxIncluded: true,
|
|
245
332
|
},
|
|
246
333
|
...(variant?.price?.rrp?.amount
|
|
@@ -248,7 +335,7 @@ function generateProductSchema() {
|
|
|
248
335
|
{
|
|
249
336
|
"@type": "UnitPriceSpecification",
|
|
250
337
|
priceCurrency: currency,
|
|
251
|
-
price: variant.price.rrp.amount,
|
|
338
|
+
price: parseFloat(variant.price.rrp.amount),
|
|
252
339
|
valueAddedTaxIncluded: true,
|
|
253
340
|
priceType: "https://schema.org/StrikethroughPrice",
|
|
254
341
|
},
|
|
@@ -325,7 +412,7 @@ function generateProductSchema() {
|
|
|
325
412
|
name: variantName,
|
|
326
413
|
sku: variant.sku.toString(),
|
|
327
414
|
url: pageUrl,
|
|
328
|
-
price: variant.price.price.amount,
|
|
415
|
+
price: parseFloat(variant.price.price.amount),
|
|
329
416
|
priceCurrency: currency,
|
|
330
417
|
itemCondition: "https://schema.org/NewCondition",
|
|
331
418
|
availability: variant?.inStock
|
|
@@ -342,7 +429,7 @@ function generateProductSchema() {
|
|
|
342
429
|
{
|
|
343
430
|
"@type": "UnitPriceSpecification",
|
|
344
431
|
priceCurrency: currency,
|
|
345
|
-
price: variant.price.price.amount,
|
|
432
|
+
price: parseFloat(variant.price.price.amount),
|
|
346
433
|
valueAddedTaxIncluded: true,
|
|
347
434
|
},
|
|
348
435
|
...(variant?.price?.rrp?.amount
|
|
@@ -350,7 +437,7 @@ function generateProductSchema() {
|
|
|
350
437
|
{
|
|
351
438
|
"@type": "UnitPriceSpecification",
|
|
352
439
|
priceCurrency: currency,
|
|
353
|
-
price: variant.price.rrp.amount,
|
|
440
|
+
price: parseFloat(variant.price.rrp.amount),
|
|
354
441
|
valueAddedTaxIncluded: true,
|
|
355
442
|
priceType: "https://schema.org/StrikethroughPrice",
|
|
356
443
|
},
|
|
@@ -389,10 +476,7 @@ function generateProductSchema() {
|
|
|
389
476
|
if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Material'))) {
|
|
390
477
|
variesBy.push("https://schema.org/material");
|
|
391
478
|
}
|
|
392
|
-
// For non-standard properties like
|
|
393
|
-
if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Amount'))) {
|
|
394
|
-
variesBy.push("https://schema.org/additionalProperty");
|
|
395
|
-
}
|
|
479
|
+
// For non-standard properties like flavour, use additionalProperty
|
|
396
480
|
if (variants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste'))) {
|
|
397
481
|
if (!variesBy.includes("https://schema.org/additionalProperty")) {
|
|
398
482
|
variesBy.push("https://schema.org/additionalProperty");
|
|
@@ -597,8 +681,8 @@ function generateSaleEventSchema() {
|
|
|
597
681
|
name: brand,
|
|
598
682
|
url: url,
|
|
599
683
|
},
|
|
600
|
-
startDate: saleEventStartDate,
|
|
601
|
-
...(saleEventEndDate && { endDate: saleEventEndDate }),
|
|
684
|
+
startDate: convertToISO8601(saleEventStartDate),
|
|
685
|
+
...(saleEventEndDate && { endDate: convertToISO8601(saleEventEndDate) }),
|
|
602
686
|
};
|
|
603
687
|
|
|
604
688
|
if (streetAddress && addressLocality && postalCode && country) {
|
|
@@ -707,7 +791,7 @@ function generateReviewsSchema() {
|
|
|
707
791
|
|
|
708
792
|
{
|
|
709
793
|
schema && !schemaError && (
|
|
710
|
-
<script type="application/ld+json" set:html={(() => {
|
|
794
|
+
<script type="application/ld+json" set:html={JSON.stringify((() => {
|
|
711
795
|
// Clean the schema object by removing undefined, null, and empty values
|
|
712
796
|
function cleanObject(obj) {
|
|
713
797
|
if (obj === null || obj === undefined || obj === '') {
|
|
@@ -734,7 +818,7 @@ function generateReviewsSchema() {
|
|
|
734
818
|
}
|
|
735
819
|
|
|
736
820
|
const cleanedSchema = cleanObject(schema);
|
|
737
|
-
return cleanedSchema
|
|
738
|
-
})()} />
|
|
821
|
+
return cleanedSchema || {};
|
|
822
|
+
})(), null, 2)} />
|
|
739
823
|
)
|
|
740
824
|
}
|
|
@@ -17,26 +17,40 @@ breadcrumbs?.length > 0 && breadcrumbs.forEach((crumb,index)=>{breadcrumbsList.p
|
|
|
17
17
|
} )})
|
|
18
18
|
|
|
19
19
|
products?.length >0 && products?.forEach((product)=>{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
if (!product?.title || !product?.sku) return; // Skip products without required fields
|
|
21
|
+
|
|
22
|
+
const productItem = {
|
|
23
|
+
"@type": "Product",
|
|
24
|
+
"@id": product?.url,
|
|
25
|
+
"name": product?.title,
|
|
26
|
+
"url": product?.url,
|
|
27
|
+
"sku": product?.sku
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Add image if available
|
|
31
|
+
if (product?.images?.[0]?.original) {
|
|
32
|
+
productItem.image = product.images[0].original;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add offers if price data is available
|
|
36
|
+
if (product.defaultVariant?.price?.price?.currency && product.defaultVariant?.price?.price?.amount) {
|
|
37
|
+
productItem.offers = {
|
|
38
|
+
"@type": "Offer",
|
|
39
|
+
"priceCurrency": product.defaultVariant.price.price.currency,
|
|
40
|
+
"price": product.defaultVariant.price.price.amount
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add aggregateRating only if review data exists
|
|
45
|
+
if (product?.reviews?.averageScore && product?.reviews?.total) {
|
|
46
|
+
productItem.aggregateRating = {
|
|
47
|
+
"@type": "AggregateRating",
|
|
48
|
+
"ratingValue": product.reviews.averageScore,
|
|
49
|
+
"reviewCount": product.reviews.total
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
productList.push(productItem);
|
|
40
54
|
})
|
|
41
55
|
|
|
42
56
|
let schema = {
|
|
@@ -56,4 +70,17 @@ let schema = {
|
|
|
56
70
|
]
|
|
57
71
|
};
|
|
58
72
|
---
|
|
59
|
-
<script type="application/ld+json" set:html={JSON.stringify(schema)
|
|
73
|
+
<script type="application/ld+json" set:html={JSON.stringify(schema, (key, value) => {
|
|
74
|
+
// Filter out undefined, null, and empty string values
|
|
75
|
+
if (value === undefined || value === null || value === '') {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// Filter out empty arrays and objects
|
|
79
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
})}></script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
faqs: { title: string; body: string }[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface faqQA {
|
|
7
|
+
"@type": "Question";
|
|
8
|
+
name: string;
|
|
9
|
+
acceptedAnswer: {
|
|
10
|
+
"@type": "Answer";
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { faqs } = Astro.props as Props;
|
|
16
|
+
|
|
17
|
+
const validFaqs = faqs.reduce((acc, faq) => {
|
|
18
|
+
if (faq?.title && faq?.body) {
|
|
19
|
+
acc.push({
|
|
20
|
+
"@type": "Question",
|
|
21
|
+
name: faq.title,
|
|
22
|
+
acceptedAnswer: {
|
|
23
|
+
"@type": "Answer",
|
|
24
|
+
text: faq.body,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return acc;
|
|
29
|
+
}, [] as faqQA[]);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
validFaqs.length > 0 && (
|
|
34
|
+
<script
|
|
35
|
+
type="application/ld+json"
|
|
36
|
+
set:html={JSON.stringify({
|
|
37
|
+
"@context": "https://schema.org",
|
|
38
|
+
"@type": "FAQPage",
|
|
39
|
+
mainEntity: validFaqs,
|
|
40
|
+
})}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -22,15 +22,50 @@ if (!suggestedAge && Astro.props.productContent?.propertyNameToValueMap?.beauty_
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
let url = import.meta.env.DEV
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
? Astro.url.origin
|
|
26
|
+
: `${Astro.request.headers.get(
|
|
27
|
+
'X-forwarded-Proto'
|
|
28
|
+
)}://${Astro.request.headers.get('X-forwarded-Host')?.split(', ')[0]}`
|
|
29
29
|
// Check if we have multiple variants to determine if we need ProductGroup
|
|
30
30
|
const hasMultipleVariants = Astro.props.variants && Astro.props.variants.length > 1;
|
|
31
31
|
|
|
32
|
+
let reviewsArray: {}[] = []
|
|
33
|
+
Astro.props.reviews?.forEach((review)=>{
|
|
34
|
+
reviewsArray.push({
|
|
35
|
+
"@type": "Review",
|
|
36
|
+
itemReviewed: {
|
|
37
|
+
"@id": Astro.props.sku?.toString() || "",
|
|
38
|
+
name: Astro.props.name || "",
|
|
39
|
+
},
|
|
40
|
+
reviewRating: {
|
|
41
|
+
"@type": "Rating",
|
|
42
|
+
bestRating: "5",
|
|
43
|
+
ratingValue: review?.elements?.[0]?.score,
|
|
44
|
+
},
|
|
45
|
+
author: {
|
|
46
|
+
"@type": "Person",
|
|
47
|
+
name: stripHtml(review?.authorName),
|
|
48
|
+
},
|
|
49
|
+
datePublished: review?.posted,
|
|
50
|
+
reviewBody: stripHtml(review?.elements?.[1]?.value),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let aggregateRating;
|
|
55
|
+
if (Astro.props.reviewsCount > 0) {
|
|
56
|
+
aggregateRating = {
|
|
57
|
+
"@type": "AggregateRating",
|
|
58
|
+
"bestRating": 5,
|
|
59
|
+
"worstRating": 1
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (Astro.props.reviewsAverage) aggregateRating.ratingValue = Astro.props.reviewsAverage;
|
|
63
|
+
if (Astro.props.reviewsCount) aggregateRating.reviewCount = Astro.props.reviewsCount;
|
|
64
|
+
}
|
|
65
|
+
|
|
32
66
|
let variantsArray = []
|
|
33
67
|
let offersArray = []
|
|
68
|
+
let schema;
|
|
34
69
|
|
|
35
70
|
if (hasMultipleVariants) {
|
|
36
71
|
// Create individual Product objects for each variant
|
|
@@ -40,19 +75,16 @@ if (hasMultipleVariants) {
|
|
|
40
75
|
// Build variant name with attributes in logical order: Name + Weight + Flavor + Size + Color + Material
|
|
41
76
|
let variantName = Astro.props.name || "";
|
|
42
77
|
const attributes = [];
|
|
43
|
-
|
|
78
|
+
|
|
44
79
|
// Check for amount/quantity first
|
|
45
80
|
if (variant.amount) attributes.push(variant.amount);
|
|
46
81
|
|
|
47
82
|
// Check for weight in different possible property names
|
|
48
83
|
if (variant.weight) attributes.push(variant.weight);
|
|
49
|
-
else if (variant.size
|
|
50
|
-
else if (variant.size && variant.size.includes('g')) attributes.push(variant.size);
|
|
84
|
+
else if (variant.size?.includes('kg') || variant.size?.includes('g')) attributes.push(variant.size);
|
|
51
85
|
|
|
52
86
|
// Check for flavor in different possible property names
|
|
53
|
-
if (variant.flavor) attributes.push(variant.flavor);
|
|
54
|
-
else if (variant.flavour) attributes.push(variant.flavour);
|
|
55
|
-
else if (variant.taste) attributes.push(variant.taste);
|
|
87
|
+
if (variant.flavor || variant.flavour || variant.taste) attributes.push(variant.flavor || variant.flavour || variant.taste);
|
|
56
88
|
else if (variant.name && variant.name !== Astro.props.name) {
|
|
57
89
|
// If variant has its own name that's different from product name, use it as flavor
|
|
58
90
|
const variantSpecificName = variant.name.replace(Astro.props.name, '').trim();
|
|
@@ -72,17 +104,51 @@ if (hasMultipleVariants) {
|
|
|
72
104
|
"@type": "Product",
|
|
73
105
|
"sku": variant.sku.toString()
|
|
74
106
|
};
|
|
75
|
-
|
|
107
|
+
|
|
76
108
|
if (variantName) variantObj.name = variantName;
|
|
77
109
|
if (Astro.props.description) variantObj.description = Astro.props.description;
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
// Try multiple possible locations for variant image and extract URL string
|
|
111
|
+
let variantImageUrl = variant.image || variant.imageUrl || variant.images?.[0] || variant.media?.[0]?.url || Astro.props.image;
|
|
112
|
+
|
|
113
|
+
// If image is an object, try to extract the URL string
|
|
114
|
+
if (variantImageUrl && typeof variantImageUrl === 'object') {
|
|
115
|
+
variantImageUrl = variantImageUrl.original || variantImageUrl.url || variantImageUrl.src || variantImageUrl.large || variantImageUrl.medium;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (variantImageUrl && typeof variantImageUrl === 'string') {
|
|
119
|
+
variantObj.image = import.meta.env.IMAGE_PROXY_URL
|
|
120
|
+
? `${import.meta.env.IMAGE_PROXY_URL}?url=${variantImageUrl}&format=webp&width=1500&height=1500&fit=cover`
|
|
121
|
+
: variantImageUrl;
|
|
80
122
|
}
|
|
81
123
|
// Use correct schema.org properties
|
|
82
124
|
if (variant.color) variantObj.color = variant.color;
|
|
83
125
|
if (variant.size) variantObj.size = variant.size;
|
|
84
126
|
if (variant.material) variantObj.material = variant.material;
|
|
85
|
-
|
|
127
|
+
|
|
128
|
+
// Handle weight as QuantitativeValue
|
|
129
|
+
if (variant.weight) {
|
|
130
|
+
if (typeof variant.weight === 'object' && variant.weight.value && variant.weight.unit) {
|
|
131
|
+
// Already in correct format
|
|
132
|
+
variantObj.weight = {
|
|
133
|
+
"@type": "QuantitativeValue",
|
|
134
|
+
"value": parseFloat(variant.weight.value),
|
|
135
|
+
"unitText": variant.weight.unit.toLowerCase()
|
|
136
|
+
};
|
|
137
|
+
} else if (typeof variant.weight === 'string') {
|
|
138
|
+
// Parse string like "5kg" or "5 kg"
|
|
139
|
+
const match = variant.weight.match(/(\d+(?:\.\d+)?)\s*(kg|g|lb|oz)/i);
|
|
140
|
+
if (match) {
|
|
141
|
+
variantObj.weight = {
|
|
142
|
+
"@type": "QuantitativeValue",
|
|
143
|
+
"value": parseFloat(match[1]),
|
|
144
|
+
"unitText": match[2].toLowerCase()
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
// Fallback to string if can't parse
|
|
148
|
+
variantObj.weight = variant.weight;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
86
152
|
|
|
87
153
|
// Extract color from variant.choices (Look Fantastic structure) - add as direct property
|
|
88
154
|
if (!variantObj.color && variant.choices && Array.isArray(variant.choices)) {
|
|
@@ -102,19 +168,8 @@ if (hasMultipleVariants) {
|
|
|
102
168
|
// Handle non-standard properties as additionalProperty
|
|
103
169
|
const additionalProperties = [];
|
|
104
170
|
|
|
105
|
-
if (variant.amount) {
|
|
106
|
-
additionalProperties.push({
|
|
107
|
-
"@type": "PropertyValue",
|
|
108
|
-
"name": "amount",
|
|
109
|
-
"value": variant.amount
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
171
|
// Handle flavour as additionalProperty since it doesn't exist in schema.org
|
|
114
|
-
|
|
115
|
-
if (variant.flavor) flavorValue = variant.flavor;
|
|
116
|
-
else if (variant.flavour) flavorValue = variant.flavour;
|
|
117
|
-
else if (variant.taste) flavorValue = variant.taste;
|
|
172
|
+
const flavorValue = variant.flavor || variant.flavour || variant.taste || null;
|
|
118
173
|
if (flavorValue) {
|
|
119
174
|
additionalProperties.push({
|
|
120
175
|
"@type": "PropertyValue",
|
|
@@ -135,7 +190,7 @@ if (hasMultipleVariants) {
|
|
|
135
190
|
"availability": variant.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
|
|
136
191
|
};
|
|
137
192
|
|
|
138
|
-
if (variant?.price?.price?.amount) offerObj.price = variant.price.price.amount;
|
|
193
|
+
if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
|
|
139
194
|
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
140
195
|
|
|
141
196
|
variantObj.offers = offerObj;
|
|
@@ -143,6 +198,49 @@ if (hasMultipleVariants) {
|
|
|
143
198
|
}
|
|
144
199
|
});
|
|
145
200
|
}
|
|
201
|
+
|
|
202
|
+
// Determine what properties vary between variants
|
|
203
|
+
const propertyChecks = [
|
|
204
|
+
{ prop: 'weight', url: 'https://schema.org/weight' },
|
|
205
|
+
{ prop: 'size', url: 'https://schema.org/size' },
|
|
206
|
+
{ prop: 'color', url: 'https://schema.org/color' },
|
|
207
|
+
{ prop: 'material', url: 'https://schema.org/material' },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// Check what attributes exist across variants - ensure variants is an array
|
|
211
|
+
// For non-standard properties like amount and flavour, we use additionalProperty
|
|
212
|
+
const variesBy = propertyChecks
|
|
213
|
+
.filter(({ prop }) => Astro.props.variants?.some(v => v?.[prop]))
|
|
214
|
+
.map(({ url }) => url);
|
|
215
|
+
|
|
216
|
+
// Check for flavor with multiple possible property names - treat as additionalProperty
|
|
217
|
+
if (Astro.props.variants?.some(v => v && (v.flavor || v.flavour || v.taste))) {
|
|
218
|
+
if (!variesBy.includes("https://schema.org/additionalProperty")) {
|
|
219
|
+
variesBy.push("https://schema.org/additionalProperty");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for shade/colour in variant.choices (Look Fantastic structure) - now uses color property
|
|
224
|
+
if (Astro.props.variants?.some(v => v && v.choices && Array.isArray(v.choices) &&
|
|
225
|
+
v.choices.some(c => c.optionKey === 'Shade' || c.optionKey === 'shade' ||
|
|
226
|
+
c.optionKey === 'Colour' || c.optionKey === 'colour' ||
|
|
227
|
+
c.optionKey === 'Color' || c.optionKey === 'color'))) {
|
|
228
|
+
if (!variesBy.includes("https://schema.org/color")) {
|
|
229
|
+
variesBy.push("https://schema.org/color");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
schema = {
|
|
234
|
+
"@type": "ProductGroup",
|
|
235
|
+
"@context": "https://schema.org",
|
|
236
|
+
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
237
|
+
"url": `${url}${Astro.props.url || ""}`,
|
|
238
|
+
"productGroupID": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
239
|
+
"hasVariant": variantsArray
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (variesBy.length > 0) schema.variesBy = variesBy;
|
|
243
|
+
|
|
146
244
|
} else {
|
|
147
245
|
// Single variant - create offers array as before
|
|
148
246
|
if (Array.isArray(Astro.props.variants)) {
|
|
@@ -156,169 +254,14 @@ if (hasMultipleVariants) {
|
|
|
156
254
|
"availability": variant.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
|
|
157
255
|
};
|
|
158
256
|
|
|
159
|
-
if (variant?.price?.price?.amount) offerObj.price = variant.price.price.amount;
|
|
257
|
+
if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
|
|
160
258
|
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
161
259
|
|
|
162
260
|
offersArray.push(offerObj);
|
|
163
261
|
}
|
|
164
262
|
});
|
|
165
263
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let reviewsArray = []
|
|
169
|
-
if (Astro.props.reviews && Array.isArray(Astro.props.reviews)) {
|
|
170
|
-
Astro.props.reviews.forEach((review)=>{
|
|
171
|
-
if (review && typeof review === 'object') {
|
|
172
|
-
const reviewObj = {
|
|
173
|
-
"@type": "Review",
|
|
174
|
-
"itemReviewed": {
|
|
175
|
-
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
176
|
-
"name": Astro.props.name || ""
|
|
177
|
-
},
|
|
178
|
-
"reviewRating": {
|
|
179
|
-
"@type": "Rating",
|
|
180
|
-
"bestRating": "5"
|
|
181
|
-
},
|
|
182
|
-
"author": {
|
|
183
|
-
"@type": "Person"
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
if (review?.elements?.[0]?.score) reviewObj.reviewRating.ratingValue = review.elements[0].score;
|
|
188
|
-
if (review?.authorName) reviewObj.author.name = stripHtml(review.authorName);
|
|
189
|
-
if (review?.posted) reviewObj.datePublished = review.posted;
|
|
190
|
-
if (review?.elements?.[1]?.value) reviewObj.reviewBody = stripHtml(review.elements[1].value);
|
|
191
|
-
|
|
192
|
-
reviewsArray.push(reviewObj);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let aggregateRating = undefined;
|
|
198
|
-
if (Astro.props.reviewsCount > 0) {
|
|
199
|
-
aggregateRating = {
|
|
200
|
-
"@type": "AggregateRating",
|
|
201
|
-
"bestRating": 5,
|
|
202
|
-
"worstRating": 1
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
if (Astro.props.reviewsAverage) aggregateRating.ratingValue = Astro.props.reviewsAverage;
|
|
206
|
-
if (Astro.props.reviewsCount) aggregateRating.reviewCount = Astro.props.reviewsCount;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let schema;
|
|
210
264
|
|
|
211
|
-
if (hasMultipleVariants) {
|
|
212
|
-
// Determine what properties vary between variants
|
|
213
|
-
const variesBy = [];
|
|
214
|
-
|
|
215
|
-
// Check what attributes exist across variants - ensure variants is an array
|
|
216
|
-
if (Array.isArray(Astro.props.variants)) {
|
|
217
|
-
if (Astro.props.variants.some(v => v && v.weight)) variesBy.push("https://schema.org/weight");
|
|
218
|
-
if (Astro.props.variants.some(v => v && v.size)) variesBy.push("https://schema.org/size");
|
|
219
|
-
if (Astro.props.variants.some(v => v && v.color)) variesBy.push("https://schema.org/color");
|
|
220
|
-
if (Astro.props.variants.some(v => v && v.material)) variesBy.push("https://schema.org/material");
|
|
221
|
-
|
|
222
|
-
// For non-standard properties like amount, flavour, and shade, we use additionalProperty
|
|
223
|
-
if (Astro.props.variants.some(v => v && v.amount)) variesBy.push("https://schema.org/additionalProperty");
|
|
224
|
-
|
|
225
|
-
// Check for flavor with multiple possible property names - treat as additionalProperty
|
|
226
|
-
if (Astro.props.variants.some(v => v && (v.flavor || v.flavour || v.taste))) {
|
|
227
|
-
if (!variesBy.includes("https://schema.org/additionalProperty")) {
|
|
228
|
-
variesBy.push("https://schema.org/additionalProperty");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Check for shade/colour in variant.choices (Look Fantastic structure) - now uses color property
|
|
233
|
-
if (Astro.props.variants.some(v => v && v.choices && Array.isArray(v.choices) &&
|
|
234
|
-
v.choices.some(c => c.optionKey === 'Shade' || c.optionKey === 'shade' ||
|
|
235
|
-
c.optionKey === 'Colour' || c.optionKey === 'colour' ||
|
|
236
|
-
c.optionKey === 'Color' || c.optionKey === 'color'))) {
|
|
237
|
-
if (!variesBy.includes("https://schema.org/color")) {
|
|
238
|
-
variesBy.push("https://schema.org/color");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
schema = {
|
|
244
|
-
"@type": "ProductGroup",
|
|
245
|
-
"@context": "https://schema.org",
|
|
246
|
-
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
247
|
-
"url": `${url}${Astro.props.url || ""}`,
|
|
248
|
-
"productGroupID": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
249
|
-
"hasVariant": variantsArray
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
if (Astro.props.name) schema.name = Astro.props.name;
|
|
253
|
-
if (Astro.props.description) schema.description = Astro.props.description;
|
|
254
|
-
if (Astro.props.image && import.meta.env.IMAGE_PROXY_URL) {
|
|
255
|
-
schema.image = `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`;
|
|
256
|
-
}
|
|
257
|
-
if (Astro.props.brand) {
|
|
258
|
-
schema.brand = {
|
|
259
|
-
"@type": "Brand",
|
|
260
|
-
"name": Astro.props.brand
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
if (variesBy.length > 0) schema.variesBy = variesBy;
|
|
264
|
-
|
|
265
|
-
// Handle custom properties as additionalProperty (schema.org compliant)
|
|
266
|
-
const additionalProperties = [];
|
|
267
|
-
if (suggestedGender) {
|
|
268
|
-
additionalProperties.push({
|
|
269
|
-
"@type": "PropertyValue",
|
|
270
|
-
"name": "suggestedGender",
|
|
271
|
-
"value": suggestedGender
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (suggestedAge) {
|
|
275
|
-
additionalProperties.push({
|
|
276
|
-
"@type": "PropertyValue",
|
|
277
|
-
"name": "suggestedAge",
|
|
278
|
-
"value": suggestedAge
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
if (Astro.props.beauty_targetAge) {
|
|
282
|
-
additionalProperties.push({
|
|
283
|
-
"@type": "PropertyValue",
|
|
284
|
-
"name": "targetAge",
|
|
285
|
-
"value": Astro.props.beauty_targetAge
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
if (Astro.props.gender) {
|
|
289
|
-
additionalProperties.push({
|
|
290
|
-
"@type": "PropertyValue",
|
|
291
|
-
"name": "GenderType",
|
|
292
|
-
"value": Astro.props.gender
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Add flavour as additionalProperty for ProductGroup level
|
|
297
|
-
if (Array.isArray(Astro.props.variants)) {
|
|
298
|
-
const flavourValues = Astro.props.variants.map(v => {
|
|
299
|
-
if (v && typeof v === 'object') {
|
|
300
|
-
return v.flavor || v.flavour || v.taste;
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}).filter(Boolean);
|
|
304
|
-
|
|
305
|
-
if (flavourValues.length > 0) {
|
|
306
|
-
// If multiple unique flavours, list them all; if single flavour, just that one
|
|
307
|
-
const uniqueFlavours = [...new Set(flavourValues)];
|
|
308
|
-
additionalProperties.push({
|
|
309
|
-
"@type": "PropertyValue",
|
|
310
|
-
"name": "flavour",
|
|
311
|
-
"value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (additionalProperties.length > 0) {
|
|
317
|
-
schema.additionalProperty = additionalProperties;
|
|
318
|
-
}
|
|
319
|
-
if (aggregateRating) schema.aggregateRating = aggregateRating;
|
|
320
|
-
if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
321
|
-
} else {
|
|
322
265
|
// Single product schema as before
|
|
323
266
|
schema = {
|
|
324
267
|
"@type": "Product",
|
|
@@ -327,77 +270,80 @@ if (hasMultipleVariants) {
|
|
|
327
270
|
"sku": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
328
271
|
"offers": offersArray
|
|
329
272
|
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Extract duplicate code
|
|
276
|
+
if (Astro.props.name) schema.name = Astro.props.name;
|
|
277
|
+
if (Astro.props.description) schema.description = Astro.props.description;
|
|
278
|
+
if (Astro.props.image) {
|
|
279
|
+
schema.image = import.meta.env.IMAGE_PROXY_URL
|
|
280
|
+
? `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`
|
|
281
|
+
: Astro.props.image;
|
|
282
|
+
}
|
|
283
|
+
if (Astro.props.brand) {
|
|
284
|
+
schema.brand = {
|
|
285
|
+
"@type": "Brand",
|
|
286
|
+
"name": Astro.props.brand
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Handle custom properties as additionalProperty (schema.org compliant)
|
|
291
|
+
const additionalProperties: {"@type":string, name:string, value:string}[] = [];
|
|
292
|
+
if (suggestedGender) {
|
|
293
|
+
additionalProperties.push({
|
|
294
|
+
"@type": "PropertyValue",
|
|
295
|
+
"name": "suggestedGender",
|
|
296
|
+
"value": suggestedGender
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (suggestedAge) {
|
|
300
|
+
additionalProperties.push({
|
|
301
|
+
"@type": "PropertyValue",
|
|
302
|
+
"name": "suggestedAge",
|
|
303
|
+
"value": suggestedAge
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (Astro.props.beauty_targetAge) {
|
|
307
|
+
additionalProperties.push({
|
|
308
|
+
"@type": "PropertyValue",
|
|
309
|
+
"name": "beauty_targetAge",
|
|
310
|
+
"value": Astro.props.beauty_targetAge
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (Astro.props.gender) {
|
|
314
|
+
additionalProperties.push({
|
|
315
|
+
"@type": "PropertyValue",
|
|
316
|
+
"name": "gender",
|
|
317
|
+
"value": Astro.props.gender
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Add flavour as additionalProperty for single product
|
|
322
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
323
|
+
const flavourValues: string[] = Astro.props.variants.map(v => {
|
|
324
|
+
if (v && typeof v === 'object') {
|
|
325
|
+
return v.flavor || v.flavour || v.taste;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}).filter(Boolean);
|
|
330
329
|
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
schema.image = `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`;
|
|
335
|
-
}
|
|
336
|
-
if (Astro.props.brand) {
|
|
337
|
-
schema.brand = {
|
|
338
|
-
"@type": "Brand",
|
|
339
|
-
"name": Astro.props.brand
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Handle custom properties as additionalProperty (schema.org compliant)
|
|
344
|
-
const additionalProperties = [];
|
|
345
|
-
if (suggestedGender) {
|
|
330
|
+
if (flavourValues.length > 0) {
|
|
331
|
+
// If multiple unique flavours, list them all; if single flavour, just that one
|
|
332
|
+
const uniqueFlavours = [...new Set(flavourValues)];
|
|
346
333
|
additionalProperties.push({
|
|
347
334
|
"@type": "PropertyValue",
|
|
348
|
-
"name": "
|
|
349
|
-
"value":
|
|
335
|
+
"name": "flavour",
|
|
336
|
+
"value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
|
|
350
337
|
});
|
|
351
338
|
}
|
|
352
|
-
if (suggestedAge) {
|
|
353
|
-
additionalProperties.push({
|
|
354
|
-
"@type": "PropertyValue",
|
|
355
|
-
"name": "suggestedAge",
|
|
356
|
-
"value": suggestedAge
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
if (Astro.props.beauty_targetAge) {
|
|
360
|
-
additionalProperties.push({
|
|
361
|
-
"@type": "PropertyValue",
|
|
362
|
-
"name": "beauty_targetAge",
|
|
363
|
-
"value": Astro.props.beauty_targetAge
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
if (Astro.props.gender) {
|
|
367
|
-
additionalProperties.push({
|
|
368
|
-
"@type": "PropertyValue",
|
|
369
|
-
"name": "gender",
|
|
370
|
-
"value": Astro.props.gender
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Add flavour as additionalProperty for single product
|
|
375
|
-
if (Array.isArray(Astro.props.variants)) {
|
|
376
|
-
const flavourValues = Astro.props.variants.map(v => {
|
|
377
|
-
if (v && typeof v === 'object') {
|
|
378
|
-
return v.flavor || v.flavour || v.taste;
|
|
379
|
-
}
|
|
380
|
-
return null;
|
|
381
|
-
}).filter(Boolean);
|
|
382
|
-
|
|
383
|
-
if (flavourValues.length > 0) {
|
|
384
|
-
// If multiple unique flavours, list them all; if single flavour, just that one
|
|
385
|
-
const uniqueFlavours = [...new Set(flavourValues)];
|
|
386
|
-
additionalProperties.push({
|
|
387
|
-
"@type": "PropertyValue",
|
|
388
|
-
"name": "flavour",
|
|
389
|
-
"value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (additionalProperties.length > 0) {
|
|
395
|
-
schema.additionalProperty = additionalProperties;
|
|
396
|
-
}
|
|
397
|
-
if (aggregateRating) schema.aggregateRating = aggregateRating;
|
|
398
|
-
if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
399
339
|
}
|
|
400
340
|
|
|
341
|
+
if (additionalProperties.length > 0) {
|
|
342
|
+
schema.additionalProperty = additionalProperties;
|
|
343
|
+
}
|
|
344
|
+
if (aggregateRating) schema.aggregateRating = aggregateRating;
|
|
345
|
+
if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
346
|
+
|
|
401
347
|
---
|
|
402
348
|
|
|
403
349
|
<script type="application/ld+json" set:html={JSON.stringify(schema, (key, value) => {
|