@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.
@@ -1,70 +1,382 @@
1
1
  ---
2
- import { stripHtml } from '@thg-altitude/utils'
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
- Astro.props.variants?.forEach((variant)=>{
10
- offersArray.push({
11
- "@type": "Offer",
12
- "sku": variant.sku.toString(),
13
- "url": Astro.props.variants.length > 1 ? `${url}${Astro.props.url}?variation=${variant?.sku}`: `${url}${Astro.props.url}`,
14
- "price": variant?.price?.price?.amount,
15
- "priceCurrency": Astro.props.currency,
16
- "itemCondition": "http://schema.org/NewCondition",
17
- "availability": variant?.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
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?.forEach((review)=>{
23
- reviewsArray.push({
24
- "@type": "Review",
25
- itemReviewed: {
26
- "@id": Astro.props.sku.toString(),
27
- name: Astro.props.name,
28
- },
29
- reviewRating: {
30
- "@type": "Rating",
31
- ratingValue: review?.elements?.[0]?.score,
32
- bestRating: "5",
33
- },
34
- author: {
35
- "@type": "Person",
36
- name: stripHtml(review?.authorName),
37
- },
38
- datePublished: review?.posted,
39
- reviewBody: stripHtml(review?.elements?.[1]?.value),
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 = Astro.props.reviewsCount > 0 ? {
44
- "@type": "AggregateRating",
45
- "ratingValue": Astro.props.reviewsAverage,
46
- "reviewCount": Astro.props.reviewsCount,
47
- bestRating: 5,
48
- worstRating: 1,
49
- } : undefined
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
- "@type": "Product",
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
- "sku": Astro.props.sku.toString(),
56
- "name": Astro.props.name,
57
- "description": Astro.props.description,
58
- "image": `${import.meta.env.IMAGE_PROXY_URL}?url=${Astro.props.image}&format=webp&width=1500&height=1500&fit=cover`,
59
- "brand": {
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
- "aggregateRating": aggregateRating,
64
- "review": reviewsArray.length ? reviewsArray : null,
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)}></script>
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
+ }