@thg-altitude/schemaorg 1.0.37 → 1.0.38
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/package.json +1 -1
- package/src/components/EnhancedProduct.astro +134 -44
- package/src/components/Product.astro +133 -23
package/package.json
CHANGED
|
@@ -29,7 +29,10 @@ const {
|
|
|
29
29
|
suggestedGender,
|
|
30
30
|
beauty_targetAge,
|
|
31
31
|
gender,
|
|
32
|
-
weight
|
|
32
|
+
weight,
|
|
33
|
+
colorVariantPages, // Array of {url, sku, color} for sibling color pages
|
|
34
|
+
currentColor, // The color of the current page (used to filter variants)
|
|
35
|
+
baseProductGroupID, // Optional: explicit base product group ID without color suffix
|
|
33
36
|
} = Astro.props;
|
|
34
37
|
|
|
35
38
|
// Use the enhanced mapping function to extract data from the full product data
|
|
@@ -126,20 +129,64 @@ function convertToISO8601(dateString) {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
function generateProductSchema() {
|
|
129
|
-
|
|
132
|
+
// Helper: extract the color value from a variant's choices
|
|
133
|
+
function getVariantColor(variant) {
|
|
134
|
+
if (!variant?.choices) return null;
|
|
135
|
+
const colorChoice = variant.choices.find(c =>
|
|
136
|
+
c.optionKey === 'Color' || c.optionKey === 'Colour' || c.optionKey === 'color' ||
|
|
137
|
+
c.optionKey === 'colour' || c.optionKey === 'Shade' || c.optionKey === 'shade' ||
|
|
138
|
+
c.colour
|
|
139
|
+
);
|
|
140
|
+
if (colorChoice) {
|
|
141
|
+
return colorChoice.title || colorChoice.key || colorChoice.colour || null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Filter variants to only include those matching the current page's color
|
|
147
|
+
// The currentColor prop must be explicitly passed by the calling code
|
|
148
|
+
// e.g. currentColor="Blush" to filter to only Blush variants
|
|
149
|
+
// If not provided, all variants are included (backwards compatible)
|
|
150
|
+
let filteredVariants = variants;
|
|
151
|
+
let excludedVariants = []; // Variants filtered out (other colors) - used for URL-only references
|
|
152
|
+
if (currentColor && variants && variants.length > 0) {
|
|
153
|
+
const filtered = variants.filter(v => {
|
|
154
|
+
const variantColor = getVariantColor(v);
|
|
155
|
+
if (!variantColor) return true; // Include variants without color info
|
|
156
|
+
return variantColor.toLowerCase() === currentColor.toLowerCase();
|
|
157
|
+
});
|
|
158
|
+
// Collect excluded variants (other colors) for URL-only hasVariant entries
|
|
159
|
+
excludedVariants = variants.filter(v => {
|
|
160
|
+
const variantColor = getVariantColor(v);
|
|
161
|
+
if (!variantColor) return false; // Don't include variants without color info
|
|
162
|
+
return variantColor.toLowerCase() !== currentColor.toLowerCase();
|
|
163
|
+
});
|
|
164
|
+
// Fallback: if filtering removed everything, use all variants
|
|
165
|
+
if (filtered.length > 0) {
|
|
166
|
+
filteredVariants = filtered;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hasMultipleVariants = filteredVariants && filteredVariants.length > 1;
|
|
130
171
|
let variantsArray = [];
|
|
131
172
|
|
|
132
|
-
if (
|
|
173
|
+
if (filteredVariants && filteredVariants.length > 0 && currency) {
|
|
133
174
|
if (hasMultipleVariants) {
|
|
134
175
|
// Create individual Product objects for each variant
|
|
135
|
-
variantsArray =
|
|
176
|
+
variantsArray = filteredVariants
|
|
136
177
|
.filter((variant) => variant?.sku && variant?.price?.price?.amount)
|
|
137
178
|
.map((variant) => {
|
|
138
|
-
// Build variant name
|
|
139
|
-
|
|
179
|
+
// Build variant name from base name (without color suffix) + variant's color + size
|
|
180
|
+
// e.g. "MP Women's Tempo Twist Front Sports Bra" + "Blush" + "XL"
|
|
181
|
+
// = "MP Women's Tempo Twist Front Sports Bra Blush XL"
|
|
182
|
+
const hasColorData = filteredVariants.some(v => getVariantColor(v));
|
|
183
|
+
const baseName = hasColorData && name
|
|
184
|
+
? name.replace(/\s*-\s*[^-]+$/, '')
|
|
185
|
+
: name;
|
|
186
|
+
let variantName = baseName;
|
|
140
187
|
const attributes = [];
|
|
141
188
|
|
|
142
|
-
// Extract properties in preferred order: Amount + Weight + Flavor +
|
|
189
|
+
// Extract properties in preferred order: Amount + Weight + Flavor + Color + Size + Material
|
|
143
190
|
if (variant.choices && variant.choices.length > 0) {
|
|
144
191
|
// Amount (only if explicitly marked as Amount, not weight-related)
|
|
145
192
|
const amountChoice = variant.choices.find(c =>
|
|
@@ -165,6 +212,10 @@ function generateProductSchema() {
|
|
|
165
212
|
);
|
|
166
213
|
if (flavorChoice && flavorChoice.title) attributes.push(flavorChoice.title);
|
|
167
214
|
|
|
215
|
+
// Color - add from variant's actual color data
|
|
216
|
+
const variantColor = getVariantColor(variant);
|
|
217
|
+
if (variantColor) attributes.push(variantColor);
|
|
218
|
+
|
|
168
219
|
// Size (excluding weight-related sizes and already processed weight)
|
|
169
220
|
const sizeChoice = variant.choices.find(c =>
|
|
170
221
|
c.optionKey === 'Size' &&
|
|
@@ -176,20 +227,13 @@ function generateProductSchema() {
|
|
|
176
227
|
);
|
|
177
228
|
if (sizeChoice && sizeChoice.title) attributes.push(sizeChoice.title);
|
|
178
229
|
|
|
179
|
-
// Color
|
|
180
|
-
const colorChoice = variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour);
|
|
181
|
-
if (colorChoice) {
|
|
182
|
-
if (colorChoice.title) attributes.push(colorChoice.title);
|
|
183
|
-
else if (colorChoice.colour) attributes.push(colorChoice.colour);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
230
|
// Material
|
|
187
231
|
const materialChoice = variant.choices.find(c => c.optionKey === 'Material');
|
|
188
232
|
if (materialChoice && materialChoice.title) attributes.push(materialChoice.title);
|
|
189
233
|
}
|
|
190
234
|
|
|
191
235
|
if (attributes.length > 0) {
|
|
192
|
-
variantName = `${
|
|
236
|
+
variantName = `${baseName} ${attributes.join(' ')}`;
|
|
193
237
|
}
|
|
194
238
|
|
|
195
239
|
return {
|
|
@@ -347,14 +391,18 @@ function generateProductSchema() {
|
|
|
347
391
|
});
|
|
348
392
|
} else {
|
|
349
393
|
// Single variant - create offers array as before
|
|
350
|
-
offersArray =
|
|
394
|
+
offersArray = filteredVariants
|
|
351
395
|
.filter((variant) => variant?.sku && variant?.price?.price?.amount)
|
|
352
396
|
.map((variant) => {
|
|
353
|
-
// Build variant name
|
|
354
|
-
|
|
397
|
+
// Build variant name from base name + variant's color + size
|
|
398
|
+
const hasColorData = filteredVariants.some(v => getVariantColor(v));
|
|
399
|
+
const baseName = hasColorData && name
|
|
400
|
+
? name.replace(/\s*-\s*[^-]+$/, '')
|
|
401
|
+
: name;
|
|
402
|
+
let variantName = baseName;
|
|
355
403
|
const attributes = [];
|
|
356
404
|
|
|
357
|
-
// Extract properties in preferred order: Amount + Weight + Flavor +
|
|
405
|
+
// Extract properties in preferred order: Amount + Weight + Flavor + Color + Size + Material
|
|
358
406
|
if (variant.choices && variant.choices.length > 0) {
|
|
359
407
|
// Amount (only if explicitly marked as Amount, not weight-related)
|
|
360
408
|
const amountChoice = variant.choices.find(c =>
|
|
@@ -380,6 +428,10 @@ function generateProductSchema() {
|
|
|
380
428
|
);
|
|
381
429
|
if (flavorChoice && flavorChoice.title) attributes.push(flavorChoice.title);
|
|
382
430
|
|
|
431
|
+
// Color - add from variant's actual color data
|
|
432
|
+
const variantColor = getVariantColor(variant);
|
|
433
|
+
if (variantColor) attributes.push(variantColor);
|
|
434
|
+
|
|
383
435
|
// Size (excluding weight-related sizes and already processed weight)
|
|
384
436
|
const sizeChoice = variant.choices.find(c =>
|
|
385
437
|
c.optionKey === 'Size' &&
|
|
@@ -391,20 +443,13 @@ function generateProductSchema() {
|
|
|
391
443
|
);
|
|
392
444
|
if (sizeChoice && sizeChoice.title) attributes.push(sizeChoice.title);
|
|
393
445
|
|
|
394
|
-
// Color
|
|
395
|
-
const colorChoice = variant.choices.find(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour);
|
|
396
|
-
if (colorChoice) {
|
|
397
|
-
if (colorChoice.title) attributes.push(colorChoice.title);
|
|
398
|
-
else if (colorChoice.colour) attributes.push(colorChoice.colour);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
446
|
// Material
|
|
402
447
|
const materialChoice = variant.choices.find(c => c.optionKey === 'Material');
|
|
403
448
|
if (materialChoice && materialChoice.title) attributes.push(materialChoice.title);
|
|
404
449
|
}
|
|
405
450
|
|
|
406
451
|
if (attributes.length > 0) {
|
|
407
|
-
variantName = `${
|
|
452
|
+
variantName = `${baseName} ${attributes.join(' ')}`;
|
|
408
453
|
}
|
|
409
454
|
|
|
410
455
|
return {
|
|
@@ -457,33 +502,70 @@ function generateProductSchema() {
|
|
|
457
502
|
const reviewsArray = generateReviewsSchema();
|
|
458
503
|
|
|
459
504
|
if (name && sku) {
|
|
460
|
-
const hasMultipleVariants =
|
|
505
|
+
const hasMultipleVariants = filteredVariants && filteredVariants.length > 1;
|
|
506
|
+
|
|
507
|
+
// Compute the base product group ID by stripping color suffix from SKU
|
|
508
|
+
// e.g. "16919878-fudge" -> "16919878", "16919878-rose-red" -> "16919878"
|
|
509
|
+
// If baseProductGroupID prop is provided, use that instead
|
|
510
|
+
const computedProductGroupID = baseProductGroupID || sku.toString().replace(/-[a-zA-Z].*$/, '');
|
|
461
511
|
|
|
462
512
|
if (hasMultipleVariants) {
|
|
463
|
-
//
|
|
513
|
+
// Build URL-only entries for other color variants (from excluded variants or colorVariantPages)
|
|
514
|
+
let otherColorUrlEntries = [];
|
|
515
|
+
if (colorVariantPages && colorVariantPages.length > 0) {
|
|
516
|
+
// Use explicitly provided color variant page URLs
|
|
517
|
+
otherColorUrlEntries = colorVariantPages
|
|
518
|
+
.filter(p => p?.url && (!currentColor || !p.color || p.color.toLowerCase() !== currentColor.toLowerCase()))
|
|
519
|
+
.map(p => ({
|
|
520
|
+
"url": p.url.startsWith('http') ? p.url : `${url}${p.url}`
|
|
521
|
+
}));
|
|
522
|
+
} else if (excludedVariants.length > 0) {
|
|
523
|
+
// Auto-generate URL-only entries from filtered-out variants (other colors)
|
|
524
|
+
// Use /p/{sku} format which redirects to the correct color page
|
|
525
|
+
// Deduplicate by color to get one URL per color (using first variant of each color)
|
|
526
|
+
const seenColors = new Set();
|
|
527
|
+
otherColorUrlEntries = excludedVariants
|
|
528
|
+
.filter(v => {
|
|
529
|
+
if (!v?.sku) return false;
|
|
530
|
+
const color = getVariantColor(v);
|
|
531
|
+
if (color && seenColors.has(color.toLowerCase())) return false;
|
|
532
|
+
if (color) seenColors.add(color.toLowerCase());
|
|
533
|
+
return true;
|
|
534
|
+
})
|
|
535
|
+
.map(v => ({
|
|
536
|
+
"url": `${url}/p/${v.sku}`
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Determine what properties vary between the filtered variants (this page's color only)
|
|
464
541
|
const variesBy = [];
|
|
465
542
|
|
|
466
|
-
// Check what attributes exist across variants
|
|
467
|
-
if (
|
|
543
|
+
// Check what attributes exist across filtered variants using correct schema.org properties
|
|
544
|
+
if (filteredVariants.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)))))) {
|
|
468
545
|
variesBy.push("https://schema.org/weight");
|
|
469
546
|
}
|
|
470
|
-
if (
|
|
547
|
+
if (filteredVariants.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')))) {
|
|
471
548
|
variesBy.push("https://schema.org/size");
|
|
472
549
|
}
|
|
473
|
-
|
|
550
|
+
// Add color to variesBy if we have other color variants (from colorVariantPages or excluded variants)
|
|
551
|
+
if (colorVariantPages && colorVariantPages.length > 0) {
|
|
552
|
+
variesBy.push("https://schema.org/color");
|
|
553
|
+
} else if (excludedVariants.length > 0) {
|
|
554
|
+
variesBy.push("https://schema.org/color");
|
|
555
|
+
} else if (filteredVariants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Color' || c.optionKey === 'Colour' || c.colour))) {
|
|
474
556
|
variesBy.push("https://schema.org/color");
|
|
475
557
|
}
|
|
476
|
-
if (
|
|
558
|
+
if (filteredVariants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Material'))) {
|
|
477
559
|
variesBy.push("https://schema.org/material");
|
|
478
560
|
}
|
|
479
561
|
// For non-standard properties like flavour, use additionalProperty
|
|
480
|
-
if (
|
|
562
|
+
if (filteredVariants.some(v => v.choices && v.choices.some(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste'))) {
|
|
481
563
|
if (!variesBy.includes("https://schema.org/additionalProperty")) {
|
|
482
564
|
variesBy.push("https://schema.org/additionalProperty");
|
|
483
565
|
}
|
|
484
566
|
}
|
|
485
567
|
// Check for Shade/Colour variations (Look Fantastic style) - now uses color property
|
|
486
|
-
if (
|
|
568
|
+
if (filteredVariants.some(v => v.choices && v.choices.some(c =>
|
|
487
569
|
c.optionKey === 'Shade' || c.optionKey === 'shade' ||
|
|
488
570
|
c.optionKey === 'Colour' || c.optionKey === 'colour' ||
|
|
489
571
|
c.optionKey === 'Color' || c.optionKey === 'color'))) {
|
|
@@ -492,12 +574,17 @@ function generateProductSchema() {
|
|
|
492
574
|
}
|
|
493
575
|
}
|
|
494
576
|
|
|
577
|
+
// Strip color suffix from ProductGroup name (e.g. "Sports Bra - Blush" -> "Sports Bra")
|
|
578
|
+
// Only strip when variants have color data, indicating the name suffix is a color
|
|
579
|
+
const hasColorVariants = filteredVariants.some(v => getVariantColor(v));
|
|
580
|
+
const productGroupName = hasColorVariants && name
|
|
581
|
+
? name.replace(/\s*-\s*[^-]+$/, '')
|
|
582
|
+
: name;
|
|
583
|
+
|
|
495
584
|
const productGroupSchema = {
|
|
496
585
|
"@type": "ProductGroup",
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
name: name,
|
|
500
|
-
productGroupID: sku.toString(),
|
|
586
|
+
name: productGroupName,
|
|
587
|
+
productGroupID: computedProductGroupID,
|
|
501
588
|
...(gtin13 && { gtin13: gtin13.toString() }),
|
|
502
589
|
...(description && { description }),
|
|
503
590
|
...(imageUrl && { image: imageUrl }),
|
|
@@ -508,7 +595,12 @@ function generateProductSchema() {
|
|
|
508
595
|
},
|
|
509
596
|
}),
|
|
510
597
|
...(variesBy.length > 0 && { variesBy }),
|
|
511
|
-
hasVariant:
|
|
598
|
+
hasVariant: [
|
|
599
|
+
...variantsArray,
|
|
600
|
+
// Add URL-only references to other color variants as hasVariant entries
|
|
601
|
+
// Uses colorVariantPages if provided, otherwise auto-generates from excluded variants
|
|
602
|
+
...otherColorUrlEntries
|
|
603
|
+
],
|
|
512
604
|
...(reviewsCount > 0 &&
|
|
513
605
|
reviewsAverage && {
|
|
514
606
|
aggregateRating: {
|
|
@@ -555,7 +647,7 @@ function generateProductSchema() {
|
|
|
555
647
|
}
|
|
556
648
|
|
|
557
649
|
// Add flavour as additionalProperty for ProductGroup level
|
|
558
|
-
const flavourValues =
|
|
650
|
+
const flavourValues = filteredVariants?.map(v =>
|
|
559
651
|
v.choices?.find(c => c.optionKey === 'Flavour' || c.optionKey === 'Flavor' || c.optionKey === 'Taste')?.title
|
|
560
652
|
).filter(Boolean);
|
|
561
653
|
|
|
@@ -578,8 +670,6 @@ function generateProductSchema() {
|
|
|
578
670
|
// Single product schema as before
|
|
579
671
|
const productSchema = {
|
|
580
672
|
"@type": "Product",
|
|
581
|
-
"@id": pageUrl,
|
|
582
|
-
url: pageUrl,
|
|
583
673
|
sku: sku.toString(),
|
|
584
674
|
name: name,
|
|
585
675
|
...(gtin13 && { gtin13: gtin13.toString() }),
|
|
@@ -26,8 +26,59 @@ let url = import.meta.env.DEV
|
|
|
26
26
|
: `${Astro.request.headers.get(
|
|
27
27
|
'X-forwarded-Proto'
|
|
28
28
|
)}://${Astro.request.headers.get('X-forwarded-Host')?.split(', ')[0]}`
|
|
29
|
+
// New props for multi-page color variant support
|
|
30
|
+
const colorVariantPages = Astro.props.colorVariantPages; // Array of {url, sku, color} for sibling color pages
|
|
31
|
+
const currentColor = Astro.props.currentColor; // The color of the current page
|
|
32
|
+
const baseProductGroupID = Astro.props.baseProductGroupID; // Optional: explicit base product group ID
|
|
33
|
+
|
|
34
|
+
// Helper: extract the color value from a variant
|
|
35
|
+
function getVariantColor(variant) {
|
|
36
|
+
if (!variant) return null;
|
|
37
|
+
// Check direct color property
|
|
38
|
+
if (variant.color) return variant.color;
|
|
39
|
+
// Check choices array
|
|
40
|
+
if (variant.choices && Array.isArray(variant.choices)) {
|
|
41
|
+
const colorChoice = variant.choices.find(c =>
|
|
42
|
+
c.optionKey === 'Color' || c.optionKey === 'Colour' || c.optionKey === 'color' ||
|
|
43
|
+
c.optionKey === 'colour' || c.optionKey === 'Shade' || c.optionKey === 'shade'
|
|
44
|
+
);
|
|
45
|
+
if (colorChoice) {
|
|
46
|
+
return colorChoice.title || colorChoice.key || colorChoice.colour || null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Filter variants to only include those matching the current page's color
|
|
53
|
+
// The currentColor prop must be explicitly passed by the calling code
|
|
54
|
+
// e.g. currentColor="Blush" to filter to only Blush variants
|
|
55
|
+
// If not provided, all variants are included (backwards compatible)
|
|
56
|
+
let filteredVariants = Astro.props.variants;
|
|
57
|
+
let excludedVariants = []; // Variants filtered out (other colors) - used for URL-only references
|
|
58
|
+
if (currentColor && Array.isArray(Astro.props.variants) && Astro.props.variants.length > 0) {
|
|
59
|
+
const filtered = Astro.props.variants.filter(v => {
|
|
60
|
+
const variantColor = getVariantColor(v);
|
|
61
|
+
if (!variantColor) return true; // Include variants without color info
|
|
62
|
+
return variantColor.toLowerCase() === currentColor.toLowerCase();
|
|
63
|
+
});
|
|
64
|
+
// Collect excluded variants (other colors) for URL-only hasVariant entries
|
|
65
|
+
excludedVariants = Astro.props.variants.filter(v => {
|
|
66
|
+
const variantColor = getVariantColor(v);
|
|
67
|
+
if (!variantColor) return false; // Don't include variants without color info
|
|
68
|
+
return variantColor.toLowerCase() !== currentColor.toLowerCase();
|
|
69
|
+
});
|
|
70
|
+
// Fallback: if filtering removed everything, use all variants
|
|
71
|
+
if (filtered.length > 0) {
|
|
72
|
+
filteredVariants = filtered;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Compute the base product group ID by stripping color suffix from SKU
|
|
77
|
+
// e.g. "16919878-fudge" -> "16919878", "16919878-rose-red" -> "16919878"
|
|
78
|
+
const computedProductGroupID = baseProductGroupID || (Astro.props.sku ? Astro.props.sku.toString().replace(/-[a-zA-Z].*$/, '') : '');
|
|
79
|
+
|
|
29
80
|
// Check if we have multiple variants to determine if we need ProductGroup
|
|
30
|
-
const hasMultipleVariants =
|
|
81
|
+
const hasMultipleVariants = filteredVariants && filteredVariants.length > 1;
|
|
31
82
|
|
|
32
83
|
let reviewsArray: {}[] = []
|
|
33
84
|
Astro.props.reviews?.forEach((review)=>{
|
|
@@ -68,12 +119,18 @@ let offersArray = []
|
|
|
68
119
|
let schema;
|
|
69
120
|
|
|
70
121
|
if (hasMultipleVariants) {
|
|
71
|
-
// Create individual Product objects for each variant
|
|
72
|
-
if (Array.isArray(
|
|
73
|
-
|
|
122
|
+
// Create individual Product objects for each variant (filtered to current color only)
|
|
123
|
+
if (Array.isArray(filteredVariants)) {
|
|
124
|
+
// Strip color from base name for variant name construction
|
|
125
|
+
const hasColorData = filteredVariants.some(v => getVariantColor(v));
|
|
126
|
+
const baseName = hasColorData && Astro.props.name
|
|
127
|
+
? Astro.props.name.replace(/\s*-\s*[^-]+$/, '')
|
|
128
|
+
: (Astro.props.name || "");
|
|
129
|
+
|
|
130
|
+
filteredVariants.forEach((variant) => {
|
|
74
131
|
if (variant && typeof variant === 'object' && variant.sku) {
|
|
75
|
-
// Build variant name
|
|
76
|
-
let variantName =
|
|
132
|
+
// Build variant name from base name + variant's color + size
|
|
133
|
+
let variantName = baseName;
|
|
77
134
|
const attributes = [];
|
|
78
135
|
|
|
79
136
|
// Check for amount/quantity first
|
|
@@ -91,13 +148,16 @@ if (hasMultipleVariants) {
|
|
|
91
148
|
if (variantSpecificName) attributes.push(variantSpecificName);
|
|
92
149
|
}
|
|
93
150
|
|
|
151
|
+
// Add color from variant's actual data
|
|
152
|
+
const variantColor = getVariantColor(variant);
|
|
153
|
+
if (variantColor) attributes.push(variantColor);
|
|
154
|
+
|
|
94
155
|
// Add other attributes
|
|
95
156
|
if (variant.size && !variant.size.includes('kg') && !variant.size.includes('g')) attributes.push(variant.size);
|
|
96
|
-
if (variant.color) attributes.push(variant.color);
|
|
97
157
|
if (variant.material) attributes.push(variant.material);
|
|
98
158
|
|
|
99
|
-
if (attributes.length > 0 &&
|
|
100
|
-
variantName = `${
|
|
159
|
+
if (attributes.length > 0 && baseName) {
|
|
160
|
+
variantName = `${baseName} ${attributes.join(' ')}`;
|
|
101
161
|
}
|
|
102
162
|
|
|
103
163
|
const variantObj = {
|
|
@@ -199,29 +259,41 @@ if (hasMultipleVariants) {
|
|
|
199
259
|
});
|
|
200
260
|
}
|
|
201
261
|
|
|
202
|
-
// Determine what properties vary between variants
|
|
262
|
+
// Determine what properties vary between filtered variants
|
|
203
263
|
const propertyChecks = [
|
|
204
264
|
{ prop: 'weight', url: 'https://schema.org/weight' },
|
|
205
265
|
{ prop: 'size', url: 'https://schema.org/size' },
|
|
206
|
-
{ prop: 'color', url: 'https://schema.org/color' },
|
|
207
266
|
{ prop: 'material', url: 'https://schema.org/material' },
|
|
208
267
|
];
|
|
209
268
|
|
|
210
|
-
// Check what attributes exist across
|
|
269
|
+
// Check what attributes exist across filtered variants
|
|
211
270
|
// For non-standard properties like amount and flavour, we use additionalProperty
|
|
212
271
|
const variesBy = propertyChecks
|
|
213
|
-
.filter(({ prop }) =>
|
|
272
|
+
.filter(({ prop }) => filteredVariants?.some(v => v?.[prop]))
|
|
214
273
|
.map(({ url }) => url);
|
|
215
274
|
|
|
275
|
+
// Add color to variesBy if we have other color variants (from colorVariantPages or excluded variants)
|
|
276
|
+
if (colorVariantPages && colorVariantPages.length > 0) {
|
|
277
|
+
if (!variesBy.includes("https://schema.org/color")) {
|
|
278
|
+
variesBy.push("https://schema.org/color");
|
|
279
|
+
}
|
|
280
|
+
} else if (excludedVariants.length > 0) {
|
|
281
|
+
if (!variesBy.includes("https://schema.org/color")) {
|
|
282
|
+
variesBy.push("https://schema.org/color");
|
|
283
|
+
}
|
|
284
|
+
} else if (filteredVariants?.some(v => v?.color)) {
|
|
285
|
+
variesBy.push("https://schema.org/color");
|
|
286
|
+
}
|
|
287
|
+
|
|
216
288
|
// Check for flavor with multiple possible property names - treat as additionalProperty
|
|
217
|
-
if (
|
|
289
|
+
if (filteredVariants?.some(v => v && (v.flavor || v.flavour || v.taste))) {
|
|
218
290
|
if (!variesBy.includes("https://schema.org/additionalProperty")) {
|
|
219
291
|
variesBy.push("https://schema.org/additionalProperty");
|
|
220
292
|
}
|
|
221
293
|
}
|
|
222
294
|
|
|
223
295
|
// Check for shade/colour in variant.choices (Look Fantastic structure) - now uses color property
|
|
224
|
-
if (
|
|
296
|
+
if (filteredVariants?.some(v => v && v.choices && Array.isArray(v.choices) &&
|
|
225
297
|
v.choices.some(c => c.optionKey === 'Shade' || c.optionKey === 'shade' ||
|
|
226
298
|
c.optionKey === 'Colour' || c.optionKey === 'colour' ||
|
|
227
299
|
c.optionKey === 'Color' || c.optionKey === 'color'))) {
|
|
@@ -230,21 +302,51 @@ if (hasMultipleVariants) {
|
|
|
230
302
|
}
|
|
231
303
|
}
|
|
232
304
|
|
|
305
|
+
// Build URL-only entries for other color variants (from excluded variants or colorVariantPages)
|
|
306
|
+
let otherColorUrlEntries = [];
|
|
307
|
+
if (colorVariantPages && colorVariantPages.length > 0) {
|
|
308
|
+
// Use explicitly provided color variant page URLs
|
|
309
|
+
otherColorUrlEntries = colorVariantPages
|
|
310
|
+
.filter(p => p?.url && (!currentColor || !p.color || p.color.toLowerCase() !== currentColor.toLowerCase()))
|
|
311
|
+
.map(p => ({
|
|
312
|
+
"url": p.url.startsWith('http') ? p.url : `${url}${p.url}`
|
|
313
|
+
}));
|
|
314
|
+
} else if (excludedVariants.length > 0) {
|
|
315
|
+
// Auto-generate URL-only entries from filtered-out variants (other colors)
|
|
316
|
+
// Use /p/{sku} format which redirects to the correct color page
|
|
317
|
+
// Deduplicate by color to get one URL per color (using first variant of each color)
|
|
318
|
+
const seenColors = new Set();
|
|
319
|
+
otherColorUrlEntries = excludedVariants
|
|
320
|
+
.filter(v => {
|
|
321
|
+
if (!v?.sku) return false;
|
|
322
|
+
const color = getVariantColor(v);
|
|
323
|
+
if (color && seenColors.has(color.toLowerCase())) return false;
|
|
324
|
+
if (color) seenColors.add(color.toLowerCase());
|
|
325
|
+
return true;
|
|
326
|
+
})
|
|
327
|
+
.map(v => ({
|
|
328
|
+
"url": `${url}/p/${v.sku}`
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
233
332
|
schema = {
|
|
234
333
|
"@type": "ProductGroup",
|
|
235
334
|
"@context": "https://schema.org",
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
335
|
+
"productGroupID": computedProductGroupID,
|
|
336
|
+
"hasVariant": [
|
|
337
|
+
...variantsArray,
|
|
338
|
+
// Add URL-only references to other color variants as hasVariant entries
|
|
339
|
+
// Uses colorVariantPages if provided, otherwise auto-generates from excluded variants
|
|
340
|
+
...otherColorUrlEntries
|
|
341
|
+
]
|
|
240
342
|
};
|
|
241
343
|
|
|
242
344
|
if (variesBy.length > 0) schema.variesBy = variesBy;
|
|
243
345
|
|
|
244
346
|
} else {
|
|
245
347
|
// Single variant - create offers array as before
|
|
246
|
-
if (Array.isArray(
|
|
247
|
-
|
|
348
|
+
if (Array.isArray(filteredVariants)) {
|
|
349
|
+
filteredVariants.forEach((variant) => {
|
|
248
350
|
if (variant && typeof variant === 'object' && variant.sku) {
|
|
249
351
|
const offerObj = {
|
|
250
352
|
"@type": "Offer",
|
|
@@ -266,14 +368,22 @@ if (hasMultipleVariants) {
|
|
|
266
368
|
schema = {
|
|
267
369
|
"@type": "Product",
|
|
268
370
|
"@context": "https://schema.org",
|
|
269
|
-
"@id": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
270
371
|
"sku": Astro.props.sku ? Astro.props.sku.toString() : "",
|
|
271
372
|
"offers": offersArray
|
|
272
373
|
};
|
|
273
374
|
}
|
|
274
375
|
|
|
275
376
|
// Extract duplicate code
|
|
276
|
-
if (Astro.props.name)
|
|
377
|
+
if (Astro.props.name) {
|
|
378
|
+
// Strip color suffix from ProductGroup name (e.g. "Sports Bra - Blush" -> "Sports Bra")
|
|
379
|
+
// Only strip when it's a ProductGroup and variants have color data
|
|
380
|
+
const hasColorVariants = hasMultipleVariants && filteredVariants?.some(v => getVariantColor(v));
|
|
381
|
+
if (hasColorVariants) {
|
|
382
|
+
schema.name = Astro.props.name.replace(/\s*-\s*[^-]+$/, '');
|
|
383
|
+
} else {
|
|
384
|
+
schema.name = Astro.props.name;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
277
387
|
if (Astro.props.description) schema.description = Astro.props.description;
|
|
278
388
|
if (Astro.props.image) {
|
|
279
389
|
schema.image = import.meta.env.IMAGE_PROXY_URL
|