@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 +1 -1
- package/index.js +4 -2
- package/package.json +1 -1
- package/src/components/EnhancedProduct.astro +491 -67
- package/src/components/Product.astro +364 -52
- package/src/components/Recipe.astro +175 -0
- package/src/utils/productSchema.js +119 -0
|
@@ -1,70 +1,382 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { stripHtml } from '
|
|
2
|
+
import { mapProductSchemaData, stripHtml } from '../utils/productSchema.js';
|
|
3
|
+
|
|
4
|
+
// Use the mapping function to extract data from the full product data
|
|
5
|
+
const mappedData = mapProductSchemaData(Astro.props);
|
|
6
|
+
|
|
7
|
+
// Extract suggestedGender and suggestedAge using the comprehensive mapping function
|
|
8
|
+
let suggestedGender = Astro.props.suggestedGender || mappedData.suggestedGender;
|
|
9
|
+
let suggestedAge = Astro.props.suggestedAge || mappedData.suggestedAge;
|
|
10
|
+
|
|
11
|
+
// Fallback to the original simple extraction if the mapping didn't find anything
|
|
12
|
+
if (!suggestedGender && Astro.props.productContent?.propertyNameToValueMap?.gender) {
|
|
13
|
+
suggestedGender = Array.isArray(Astro.props.productContent.propertyNameToValueMap.gender)
|
|
14
|
+
? Astro.props.productContent.propertyNameToValueMap.gender[0]
|
|
15
|
+
: Astro.props.productContent.propertyNameToValueMap.gender;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!suggestedAge && Astro.props.productContent?.propertyNameToValueMap?.beauty_targetAge) {
|
|
19
|
+
suggestedAge = Array.isArray(Astro.props.productContent.propertyNameToValueMap.beauty_targetAge)
|
|
20
|
+
? Astro.props.productContent.propertyNameToValueMap.beauty_targetAge[0]
|
|
21
|
+
: Astro.props.productContent.propertyNameToValueMap.beauty_targetAge;
|
|
22
|
+
}
|
|
23
|
+
|
|
3
24
|
let url = import.meta.env.DEV
|
|
4
25
|
? Astro.url.origin
|
|
5
26
|
: `${Astro.request.headers.get(
|
|
6
27
|
'X-forwarded-Proto'
|
|
7
28
|
)}://${Astro.request.headers.get('X-forwarded-Host')?.split(', ')[0]}`
|
|
29
|
+
// Check if we have multiple variants to determine if we need ProductGroup
|
|
30
|
+
const hasMultipleVariants = Astro.props.variants && Astro.props.variants.length > 1;
|
|
31
|
+
|
|
32
|
+
let variantsArray = []
|
|
8
33
|
let offersArray = []
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
|
|
35
|
+
if (hasMultipleVariants) {
|
|
36
|
+
// Create individual Product objects for each variant
|
|
37
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
38
|
+
Astro.props.variants.forEach((variant) => {
|
|
39
|
+
if (variant && typeof variant === 'object' && variant.sku) {
|
|
40
|
+
// Build variant name with attributes in logical order: Name + Weight + Flavor + Size + Color + Material
|
|
41
|
+
let variantName = Astro.props.name || "";
|
|
42
|
+
const attributes = [];
|
|
43
|
+
|
|
44
|
+
// Check for amount/quantity first
|
|
45
|
+
if (variant.amount) attributes.push(variant.amount);
|
|
46
|
+
|
|
47
|
+
// Check for weight in different possible property names
|
|
48
|
+
if (variant.weight) attributes.push(variant.weight);
|
|
49
|
+
else if (variant.size && variant.size.includes('kg')) attributes.push(variant.size);
|
|
50
|
+
else if (variant.size && variant.size.includes('g')) attributes.push(variant.size);
|
|
51
|
+
|
|
52
|
+
// 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);
|
|
56
|
+
else if (variant.name && variant.name !== Astro.props.name) {
|
|
57
|
+
// If variant has its own name that's different from product name, use it as flavor
|
|
58
|
+
const variantSpecificName = variant.name.replace(Astro.props.name, '').trim();
|
|
59
|
+
if (variantSpecificName) attributes.push(variantSpecificName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add other attributes
|
|
63
|
+
if (variant.size && !variant.size.includes('kg') && !variant.size.includes('g')) attributes.push(variant.size);
|
|
64
|
+
if (variant.color) attributes.push(variant.color);
|
|
65
|
+
if (variant.material) attributes.push(variant.material);
|
|
66
|
+
|
|
67
|
+
if (attributes.length > 0 && Astro.props.name) {
|
|
68
|
+
variantName = `${Astro.props.name} ${attributes.join(' ')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const variantObj = {
|
|
72
|
+
"@type": "Product",
|
|
73
|
+
"sku": variant.sku.toString()
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (variantName) variantObj.name = variantName;
|
|
77
|
+
if (Astro.props.description) variantObj.description = Astro.props.description;
|
|
78
|
+
if ((variant.image || Astro.props.image) && import.meta.env.IMAGE_PROXY_URL) {
|
|
79
|
+
variantObj.image = `${import.meta.env.IMAGE_PROXY_URL}?url=${variant.image || Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`;
|
|
80
|
+
}
|
|
81
|
+
// Use correct schema.org properties
|
|
82
|
+
if (variant.color) variantObj.color = variant.color;
|
|
83
|
+
if (variant.size) variantObj.size = variant.size;
|
|
84
|
+
if (variant.material) variantObj.material = variant.material;
|
|
85
|
+
if (variant.weight) variantObj.weight = variant.weight;
|
|
86
|
+
|
|
87
|
+
// Handle non-standard properties as additionalProperty
|
|
88
|
+
const additionalProperties = [];
|
|
89
|
+
|
|
90
|
+
if (variant.amount) {
|
|
91
|
+
additionalProperties.push({
|
|
92
|
+
"@type": "PropertyValue",
|
|
93
|
+
"name": "amount",
|
|
94
|
+
"value": variant.amount
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle flavour as additionalProperty since it doesn't exist in schema.org
|
|
99
|
+
let flavorValue = null;
|
|
100
|
+
if (variant.flavor) flavorValue = variant.flavor;
|
|
101
|
+
else if (variant.flavour) flavorValue = variant.flavour;
|
|
102
|
+
else if (variant.taste) flavorValue = variant.taste;
|
|
103
|
+
if (flavorValue) {
|
|
104
|
+
additionalProperties.push({
|
|
105
|
+
"@type": "PropertyValue",
|
|
106
|
+
"name": "flavour",
|
|
107
|
+
"value": flavorValue
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (additionalProperties.length > 0) {
|
|
112
|
+
variantObj.additionalProperty = additionalProperties;
|
|
113
|
+
}
|
|
114
|
+
if (variant.gtin13) variantObj.gtin13 = variant.gtin13.toString();
|
|
115
|
+
|
|
116
|
+
const offerObj = {
|
|
117
|
+
"@type": "Offer",
|
|
118
|
+
"url": `${url}${Astro.props.url || ""}?variation=${variant.sku}`,
|
|
119
|
+
"itemCondition": "https://schema.org/NewCondition",
|
|
120
|
+
"availability": variant.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (variant?.price?.price?.amount) offerObj.price = variant.price.price.amount;
|
|
124
|
+
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
125
|
+
|
|
126
|
+
variantObj.offers = offerObj;
|
|
127
|
+
variantsArray.push(variantObj);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Single variant - create offers array as before
|
|
133
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
134
|
+
Astro.props.variants.forEach((variant) => {
|
|
135
|
+
if (variant && typeof variant === 'object' && variant.sku) {
|
|
136
|
+
const offerObj = {
|
|
137
|
+
"@type": "Offer",
|
|
138
|
+
"sku": variant.sku.toString(),
|
|
139
|
+
"url": `${url}${Astro.props.url || ""}`,
|
|
140
|
+
"itemCondition": "https://schema.org/NewCondition",
|
|
141
|
+
"availability": variant.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (variant?.price?.price?.amount) offerObj.price = variant.price.price.amount;
|
|
145
|
+
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
146
|
+
|
|
147
|
+
offersArray.push(offerObj);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
20
152
|
|
|
21
153
|
let reviewsArray = []
|
|
22
|
-
Astro.props.reviews
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
154
|
+
if (Astro.props.reviews && Array.isArray(Astro.props.reviews)) {
|
|
155
|
+
Astro.props.reviews.forEach((review)=>{
|
|
156
|
+
if (review && typeof review === 'object') {
|
|
157
|
+
const reviewObj = {
|
|
158
|
+
"@type": "Review",
|
|
159
|
+
"itemReviewed": {
|
|
160
|
+
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
161
|
+
"name": Astro.props.name || ""
|
|
162
|
+
},
|
|
163
|
+
"reviewRating": {
|
|
164
|
+
"@type": "Rating",
|
|
165
|
+
"bestRating": "5"
|
|
166
|
+
},
|
|
167
|
+
"author": {
|
|
168
|
+
"@type": "Person"
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (review?.elements?.[0]?.score) reviewObj.reviewRating.ratingValue = review.elements[0].score;
|
|
173
|
+
if (review?.authorName) reviewObj.author.name = stripHtml(review.authorName);
|
|
174
|
+
if (review?.posted) reviewObj.datePublished = review.posted;
|
|
175
|
+
if (review?.elements?.[1]?.value) reviewObj.reviewBody = stripHtml(review.elements[1].value);
|
|
176
|
+
|
|
177
|
+
reviewsArray.push(reviewObj);
|
|
178
|
+
}
|
|
40
179
|
});
|
|
41
|
-
}
|
|
180
|
+
}
|
|
42
181
|
|
|
43
|
-
let aggregateRating =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
182
|
+
let aggregateRating = undefined;
|
|
183
|
+
if (Astro.props.reviewsCount > 0) {
|
|
184
|
+
aggregateRating = {
|
|
185
|
+
"@type": "AggregateRating",
|
|
186
|
+
"bestRating": 5,
|
|
187
|
+
"worstRating": 1
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (Astro.props.reviewsAverage) aggregateRating.ratingValue = Astro.props.reviewsAverage;
|
|
191
|
+
if (Astro.props.reviewsCount) aggregateRating.reviewCount = Astro.props.reviewsCount;
|
|
192
|
+
}
|
|
50
193
|
|
|
51
|
-
let schema
|
|
52
|
-
|
|
194
|
+
let schema;
|
|
195
|
+
|
|
196
|
+
if (hasMultipleVariants) {
|
|
197
|
+
// Determine what properties vary between variants
|
|
198
|
+
const variesBy = [];
|
|
199
|
+
|
|
200
|
+
// Check what attributes exist across variants - ensure variants is an array
|
|
201
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
202
|
+
if (Astro.props.variants.some(v => v && v.weight)) variesBy.push("https://schema.org/weight");
|
|
203
|
+
if (Astro.props.variants.some(v => v && v.size)) variesBy.push("https://schema.org/size");
|
|
204
|
+
if (Astro.props.variants.some(v => v && v.color)) variesBy.push("https://schema.org/color");
|
|
205
|
+
if (Astro.props.variants.some(v => v && v.material)) variesBy.push("https://schema.org/material");
|
|
206
|
+
|
|
207
|
+
// For non-standard properties like amount and flavour, we use additionalProperty
|
|
208
|
+
if (Astro.props.variants.some(v => v && v.amount)) variesBy.push("https://schema.org/additionalProperty");
|
|
209
|
+
|
|
210
|
+
// Check for flavor with multiple possible property names - treat as additionalProperty
|
|
211
|
+
if (Astro.props.variants.some(v => v && (v.flavor || v.flavour || v.taste))) {
|
|
212
|
+
variesBy.push("https://schema.org/additionalProperty");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
schema = {
|
|
217
|
+
"@type": "ProductGroup",
|
|
53
218
|
"@context": "https://schema.org",
|
|
54
|
-
"@id": Astro.props.sku.toString(),
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
219
|
+
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
220
|
+
"url": `${url}${Astro.props.url || ""}`,
|
|
221
|
+
"productGroupID": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
222
|
+
"hasVariant": variantsArray
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (Astro.props.name) schema.name = Astro.props.name;
|
|
226
|
+
if (Astro.props.description) schema.description = Astro.props.description;
|
|
227
|
+
if (Astro.props.image && import.meta.env.IMAGE_PROXY_URL) {
|
|
228
|
+
schema.image = `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`;
|
|
229
|
+
}
|
|
230
|
+
if (Astro.props.brand) {
|
|
231
|
+
schema.brand = {
|
|
60
232
|
"@type": "Brand",
|
|
61
|
-
"name": Astro.props.brand
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
233
|
+
"name": Astro.props.brand
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (variesBy.length > 0) schema.variesBy = variesBy;
|
|
237
|
+
|
|
238
|
+
// Handle custom properties as additionalProperty (schema.org compliant)
|
|
239
|
+
const additionalProperties = [];
|
|
240
|
+
if (suggestedGender) {
|
|
241
|
+
additionalProperties.push({
|
|
242
|
+
"@type": "PropertyValue",
|
|
243
|
+
"name": "suggestedGender",
|
|
244
|
+
"value": suggestedGender
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (suggestedAge) {
|
|
248
|
+
additionalProperties.push({
|
|
249
|
+
"@type": "PropertyValue",
|
|
250
|
+
"name": "suggestedAge",
|
|
251
|
+
"value": suggestedAge
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (Astro.props.beauty_targetAge) {
|
|
255
|
+
additionalProperties.push({
|
|
256
|
+
"@type": "PropertyValue",
|
|
257
|
+
"name": "targetAge",
|
|
258
|
+
"value": Astro.props.beauty_targetAge
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (Astro.props.gender) {
|
|
262
|
+
additionalProperties.push({
|
|
263
|
+
"@type": "PropertyValue",
|
|
264
|
+
"name": "GenderType",
|
|
265
|
+
"value": Astro.props.gender
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Add flavour as additionalProperty for ProductGroup level
|
|
270
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
271
|
+
const flavourValues = Astro.props.variants.map(v => {
|
|
272
|
+
if (v && typeof v === 'object') {
|
|
273
|
+
return v.flavor || v.flavour || v.taste;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}).filter(Boolean);
|
|
277
|
+
|
|
278
|
+
if (flavourValues.length > 0) {
|
|
279
|
+
// If multiple unique flavours, list them all; if single flavour, just that one
|
|
280
|
+
const uniqueFlavours = [...new Set(flavourValues)];
|
|
281
|
+
additionalProperties.push({
|
|
282
|
+
"@type": "PropertyValue",
|
|
283
|
+
"name": "flavour",
|
|
284
|
+
"value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (additionalProperties.length > 0) {
|
|
290
|
+
schema.additionalProperty = additionalProperties;
|
|
291
|
+
}
|
|
292
|
+
if (aggregateRating) schema.aggregateRating = aggregateRating;
|
|
293
|
+
if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
294
|
+
} else {
|
|
295
|
+
// Single product schema as before
|
|
296
|
+
schema = {
|
|
297
|
+
"@type": "Product",
|
|
298
|
+
"@context": "https://schema.org",
|
|
299
|
+
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
300
|
+
"sku": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
65
301
|
"offers": offersArray
|
|
66
|
-
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (Astro.props.name) schema.name = Astro.props.name;
|
|
305
|
+
if (Astro.props.description) schema.description = Astro.props.description;
|
|
306
|
+
if (Astro.props.image && import.meta.env.IMAGE_PROXY_URL) {
|
|
307
|
+
schema.image = `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`;
|
|
308
|
+
}
|
|
309
|
+
if (Astro.props.brand) {
|
|
310
|
+
schema.brand = {
|
|
311
|
+
"@type": "Brand",
|
|
312
|
+
"name": Astro.props.brand
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Handle custom properties as additionalProperty (schema.org compliant)
|
|
317
|
+
const additionalProperties = [];
|
|
318
|
+
if (suggestedGender) {
|
|
319
|
+
additionalProperties.push({
|
|
320
|
+
"@type": "PropertyValue",
|
|
321
|
+
"name": "suggestedGender",
|
|
322
|
+
"value": suggestedGender
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (suggestedAge) {
|
|
326
|
+
additionalProperties.push({
|
|
327
|
+
"@type": "PropertyValue",
|
|
328
|
+
"name": "suggestedAge",
|
|
329
|
+
"value": suggestedAge
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (Astro.props.beauty_targetAge) {
|
|
333
|
+
additionalProperties.push({
|
|
334
|
+
"@type": "PropertyValue",
|
|
335
|
+
"name": "beauty_targetAge",
|
|
336
|
+
"value": Astro.props.beauty_targetAge
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (Astro.props.gender) {
|
|
340
|
+
additionalProperties.push({
|
|
341
|
+
"@type": "PropertyValue",
|
|
342
|
+
"name": "gender",
|
|
343
|
+
"value": Astro.props.gender
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add flavour as additionalProperty for single product
|
|
348
|
+
if (Array.isArray(Astro.props.variants)) {
|
|
349
|
+
const flavourValues = Astro.props.variants.map(v => {
|
|
350
|
+
if (v && typeof v === 'object') {
|
|
351
|
+
return v.flavor || v.flavour || v.taste;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}).filter(Boolean);
|
|
355
|
+
|
|
356
|
+
if (flavourValues.length > 0) {
|
|
357
|
+
// If multiple unique flavours, list them all; if single flavour, just that one
|
|
358
|
+
const uniqueFlavours = [...new Set(flavourValues)];
|
|
359
|
+
additionalProperties.push({
|
|
360
|
+
"@type": "PropertyValue",
|
|
361
|
+
"name": "flavour",
|
|
362
|
+
"value": uniqueFlavours.length === 1 ? uniqueFlavours[0] : uniqueFlavours.join(', ')
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (additionalProperties.length > 0) {
|
|
368
|
+
schema.additionalProperty = additionalProperties;
|
|
369
|
+
}
|
|
370
|
+
if (aggregateRating) schema.aggregateRating = aggregateRating;
|
|
371
|
+
if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
372
|
+
}
|
|
67
373
|
|
|
68
374
|
---
|
|
69
375
|
|
|
70
|
-
<script type="application/ld+json" set:html={JSON.stringify(schema)
|
|
376
|
+
<script type="application/ld+json" set:html={JSON.stringify(schema, (key, value) => {
|
|
377
|
+
// Filter out undefined, null, and empty string values
|
|
378
|
+
if (value === undefined || value === null || value === '') {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
})}></script>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { contents, author, image, name, title } = Astro.props
|
|
3
|
+
|
|
4
|
+
const recipeDetails = contents?.find(
|
|
5
|
+
(obj) => obj.type === 'recipeDetails'
|
|
6
|
+
)?.props
|
|
7
|
+
const hasRecipe =
|
|
8
|
+
contents?.find((obj) => obj.type?.includes('recipe')) !== undefined
|
|
9
|
+
|
|
10
|
+
const recipeIngredientsData = contents?.find((obj) => obj.type === 'recipeIngredients')?.props
|
|
11
|
+
const recipeInstructionsData = contents?.find((obj) => obj.type === 'recipeInstructions')?.props
|
|
12
|
+
const recipeNutritionData = contents?.find((obj) => obj.type === 'recipeNutrition')?.props
|
|
13
|
+
|
|
14
|
+
// Build recipe schema data
|
|
15
|
+
const recipeSchemaData = {}
|
|
16
|
+
|
|
17
|
+
// Add basic recipe information - try multiple possible property names
|
|
18
|
+
// Check props first, then recipeDetails
|
|
19
|
+
if (name || title || recipeDetails?.title || recipeDetails?.name) {
|
|
20
|
+
recipeSchemaData.name = name || title || recipeDetails.title || recipeDetails.name
|
|
21
|
+
}
|
|
22
|
+
if (recipeDetails?.description || recipeDetails?.summary) {
|
|
23
|
+
recipeSchemaData.description = recipeDetails.description || recipeDetails.summary
|
|
24
|
+
}
|
|
25
|
+
if (recipeDetails?.cookTime || recipeDetails?.cookingTime || recipeDetails?.cook_time) {
|
|
26
|
+
recipeSchemaData.cookTime = recipeDetails.cookTime || recipeDetails.cookingTime || recipeDetails.cook_time
|
|
27
|
+
}
|
|
28
|
+
if (recipeDetails?.prepTime || recipeDetails?.preparationTime || recipeDetails?.prep_time) {
|
|
29
|
+
recipeSchemaData.prepTime = recipeDetails.prepTime || recipeDetails.preparationTime || recipeDetails.prep_time
|
|
30
|
+
}
|
|
31
|
+
if (recipeDetails?.servings || recipeDetails?.serves || recipeDetails?.yield) {
|
|
32
|
+
recipeSchemaData.recipeYield = recipeDetails.servings || recipeDetails.serves || recipeDetails.yield
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add ingredients - convert to simple string array
|
|
36
|
+
if (recipeIngredientsData) {
|
|
37
|
+
let ingredients = null
|
|
38
|
+
|
|
39
|
+
// Try different property names
|
|
40
|
+
ingredients = recipeIngredientsData.ingredients ||
|
|
41
|
+
recipeIngredientsData.items ||
|
|
42
|
+
recipeIngredientsData.list ||
|
|
43
|
+
recipeIngredientsData.ingredientList
|
|
44
|
+
|
|
45
|
+
// If it's an array directly
|
|
46
|
+
if (!ingredients && Array.isArray(recipeIngredientsData)) {
|
|
47
|
+
ingredients = recipeIngredientsData
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If it's an object with nested data
|
|
51
|
+
if (!ingredients && typeof recipeIngredientsData === 'object') {
|
|
52
|
+
// Try to find any array property
|
|
53
|
+
for (const key in recipeIngredientsData) {
|
|
54
|
+
if (Array.isArray(recipeIngredientsData[key])) {
|
|
55
|
+
ingredients = recipeIngredientsData[key]
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ingredients && Array.isArray(ingredients) && ingredients.length > 0) {
|
|
62
|
+
// Convert ingredient objects to strings
|
|
63
|
+
recipeSchemaData.recipeIngredient = ingredients.map(ingredient => {
|
|
64
|
+
if (typeof ingredient === 'string') {
|
|
65
|
+
return ingredient
|
|
66
|
+
} else if (ingredient?.ingredient) {
|
|
67
|
+
// Combine quantity, unit, and ingredient into a string
|
|
68
|
+
const parts = []
|
|
69
|
+
if (ingredient.quantity) parts.push(ingredient.quantity)
|
|
70
|
+
if (ingredient.unit) parts.push(ingredient.unit)
|
|
71
|
+
if (ingredient.ingredient) parts.push(ingredient.ingredient)
|
|
72
|
+
return parts.join(' ')
|
|
73
|
+
} else if (ingredient?.name) {
|
|
74
|
+
return ingredient.name
|
|
75
|
+
}
|
|
76
|
+
return String(ingredient)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add instructions - convert to simple string array
|
|
82
|
+
if (recipeInstructionsData) {
|
|
83
|
+
let instructions = null
|
|
84
|
+
|
|
85
|
+
// Try different property names
|
|
86
|
+
instructions = recipeInstructionsData.instructions ||
|
|
87
|
+
recipeInstructionsData.steps ||
|
|
88
|
+
recipeInstructionsData.directions ||
|
|
89
|
+
recipeInstructionsData.method ||
|
|
90
|
+
recipeInstructionsData.stepList
|
|
91
|
+
|
|
92
|
+
// If it's an array directly
|
|
93
|
+
if (!instructions && Array.isArray(recipeInstructionsData)) {
|
|
94
|
+
instructions = recipeInstructionsData
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If it's an object with nested data
|
|
98
|
+
if (!instructions && typeof recipeInstructionsData === 'object') {
|
|
99
|
+
// Try to find any array property
|
|
100
|
+
for (const key in recipeInstructionsData) {
|
|
101
|
+
if (Array.isArray(recipeInstructionsData[key])) {
|
|
102
|
+
instructions = recipeInstructionsData[key]
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (instructions && Array.isArray(instructions) && instructions.length > 0) {
|
|
109
|
+
// Convert instruction objects to strings
|
|
110
|
+
recipeSchemaData.recipeInstructions = instructions.map(instruction => {
|
|
111
|
+
if (typeof instruction === 'string') {
|
|
112
|
+
return instruction
|
|
113
|
+
} else if (instruction?.instruction) {
|
|
114
|
+
// Strip HTML tags from instruction text
|
|
115
|
+
return instruction.instruction.replace(/<[^>]*>/g, '')
|
|
116
|
+
} else if (instruction?.text) {
|
|
117
|
+
return instruction.text.replace(/<[^>]*>/g, '')
|
|
118
|
+
}
|
|
119
|
+
return String(instruction).replace(/<[^>]*>/g, '')
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add nutrition information - map to correct Schema.org property names
|
|
125
|
+
if (recipeNutritionData?.nutritionalInformation || recipeNutritionData) {
|
|
126
|
+
const nutritionData = recipeNutritionData?.nutritionalInformation || recipeNutritionData
|
|
127
|
+
|
|
128
|
+
const nutrition = {
|
|
129
|
+
'@type': 'NutritionInformation'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Map nutrition properties to Schema.org names
|
|
133
|
+
if (nutritionData.calories) nutrition.calories = nutritionData.calories
|
|
134
|
+
if (nutritionData.totalFat) nutrition.fatContent = nutritionData.totalFat
|
|
135
|
+
if (nutritionData.saturatedFat) nutrition.saturatedFatContent = nutritionData.saturatedFat
|
|
136
|
+
if (nutritionData.transFat) nutrition.transFatContent = nutritionData.transFat
|
|
137
|
+
if (nutritionData.cholesterol) nutrition.cholesterolContent = nutritionData.cholesterol
|
|
138
|
+
if (nutritionData.sodium) nutrition.sodiumContent = nutritionData.sodium
|
|
139
|
+
if (nutritionData.totalCarbohydrates) nutrition.carbohydrateContent = nutritionData.totalCarbohydrates
|
|
140
|
+
if (nutritionData.dietaryFiber) nutrition.fiberContent = nutritionData.dietaryFiber
|
|
141
|
+
if (nutritionData.sugar) nutrition.sugarContent = nutritionData.sugar
|
|
142
|
+
if (nutritionData.protein) nutrition.proteinContent = nutritionData.protein
|
|
143
|
+
|
|
144
|
+
recipeSchemaData.nutrition = nutrition
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add image if available
|
|
148
|
+
if (image?.mediaItemUrl) {
|
|
149
|
+
recipeSchemaData.image = image.mediaItemUrl
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add author if available
|
|
153
|
+
if (author?.name) {
|
|
154
|
+
recipeSchemaData.author = {
|
|
155
|
+
'@type': 'Person',
|
|
156
|
+
name: author.name
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const recipeSchema = {
|
|
161
|
+
'@context': 'https://schema.org',
|
|
162
|
+
'@type': 'Recipe',
|
|
163
|
+
...recipeSchemaData
|
|
164
|
+
}
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
hasRecipe && (
|
|
169
|
+
<script
|
|
170
|
+
is:inline
|
|
171
|
+
type='application/ld+json'
|
|
172
|
+
set:html={JSON.stringify(recipeSchema)}
|
|
173
|
+
/>
|
|
174
|
+
)
|
|
175
|
+
}
|