@getvitops/astro 0.1.1 → 0.3.0

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.
Files changed (37) hide show
  1. package/dist/index.d.mts +2 -120
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +3 -181
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +30 -9
  6. package/src/components/Cards.astro +32 -0
  7. package/src/components/Details.astro +76 -0
  8. package/src/components/Drawer.astro +65 -0
  9. package/src/components/NodeRenderer.astro +27 -0
  10. package/src/components/Popover.astro +90 -0
  11. package/src/components/Subgrid.astro +32 -0
  12. package/src/components/WebComponentLoader.astro +48 -0
  13. package/src/schemas/Article.astro +69 -0
  14. package/src/schemas/BookActions.astro +81 -0
  15. package/src/schemas/Breadcrumb.astro +26 -0
  16. package/src/schemas/Carousel.astro +34 -0
  17. package/src/schemas/CourseList.astro +122 -0
  18. package/src/schemas/Dataset.astro +127 -0
  19. package/src/schemas/DiscussionForum.astro +82 -0
  20. package/src/schemas/EducationQA.astro +63 -0
  21. package/src/schemas/EmployerAggregateRating.astro +54 -0
  22. package/src/schemas/Event.astro +115 -0
  23. package/src/schemas/FAQ.astro +34 -0
  24. package/src/schemas/FactCheck.astro +84 -0
  25. package/src/schemas/ImageMetadata.astro +56 -0
  26. package/src/schemas/JobPosting.astro +138 -0
  27. package/src/schemas/LocalBusiness.astro +155 -0
  28. package/src/schemas/MathSolver.astro +54 -0
  29. package/src/schemas/MovieCarousel.astro +92 -0
  30. package/src/schemas/Organization.astro +140 -0
  31. package/src/schemas/Product.astro +250 -0
  32. package/src/schemas/ProfilePage.astro +147 -0
  33. package/src/schemas/QAPage.astro +71 -0
  34. package/src/schemas/Recipe.astro +178 -0
  35. package/src/schemas/Review.astro +138 -0
  36. package/src/schemas/SoftwareApp.astro +172 -0
  37. /package/{Head.astro → src/components/Head.astro} +0 -0
@@ -0,0 +1,155 @@
1
+ ---
2
+ export interface OpeningHoursSpecification {
3
+ dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];
4
+ opens: string;
5
+ closes: string;
6
+ validFrom?: string;
7
+ validThrough?: string;
8
+ }
9
+
10
+ export interface Props {
11
+ type?: 'LocalBusiness' | 'Restaurant' | 'Store' | 'MedicalBusiness' | 'FinancialService' | 'FoodEstablishment' | 'HealthAndBeautyBusiness' | 'HomeAndConstructionBusiness' | 'LegalService' | 'LodgingBusiness' | 'ProfessionalService' | 'SportsActivityLocation';
12
+ name: string;
13
+ description?: string;
14
+ url?: string;
15
+ telephone?: string;
16
+ email?: string;
17
+ image?: string | string[];
18
+ priceRange?: string;
19
+ address: {
20
+ streetAddress: string;
21
+ addressLocality: string;
22
+ addressRegion?: string;
23
+ postalCode: string;
24
+ addressCountry: string;
25
+ };
26
+ geo?: {
27
+ latitude: number;
28
+ longitude: number;
29
+ };
30
+ openingHoursSpecification?: OpeningHoursSpecification[];
31
+ aggregateRating?: {
32
+ ratingValue: number;
33
+ reviewCount?: number;
34
+ ratingCount?: number;
35
+ bestRating?: number;
36
+ worstRating?: number;
37
+ };
38
+ review?: {
39
+ author: string;
40
+ datePublished: string;
41
+ reviewBody: string;
42
+ reviewRating?: {
43
+ ratingValue: number;
44
+ bestRating?: number;
45
+ worstRating?: number;
46
+ };
47
+ }[];
48
+ servesCuisine?: string[];
49
+ menu?: string;
50
+ acceptsReservations?: boolean;
51
+ hasMenu?: string;
52
+ paymentAccepted?: string[];
53
+ currenciesAccepted?: string;
54
+ areaServed?: string[];
55
+ sameAs?: string[];
56
+ }
57
+
58
+ const {
59
+ type = 'LocalBusiness',
60
+ name,
61
+ description,
62
+ url,
63
+ telephone,
64
+ email,
65
+ image,
66
+ priceRange,
67
+ address,
68
+ geo,
69
+ openingHoursSpecification,
70
+ aggregateRating,
71
+ review,
72
+ servesCuisine,
73
+ menu,
74
+ acceptsReservations,
75
+ hasMenu,
76
+ paymentAccepted,
77
+ currenciesAccepted,
78
+ areaServed,
79
+ sameAs,
80
+ } = Astro.props;
81
+
82
+ const jsonLd = {
83
+ '@context': 'https://schema.org',
84
+ '@type': type,
85
+ name,
86
+ ...(description && { description }),
87
+ ...(url && { url }),
88
+ ...(telephone && { telephone }),
89
+ ...(email && { email }),
90
+ ...(image && { image: Array.isArray(image) ? image : [image] }),
91
+ ...(priceRange && { priceRange }),
92
+ address: {
93
+ '@type': 'PostalAddress',
94
+ streetAddress: address.streetAddress,
95
+ addressLocality: address.addressLocality,
96
+ ...(address.addressRegion && { addressRegion: address.addressRegion }),
97
+ postalCode: address.postalCode,
98
+ addressCountry: address.addressCountry,
99
+ },
100
+ ...(geo?.latitude != null && geo?.longitude != null && {
101
+ geo: {
102
+ '@type': 'GeoCoordinates',
103
+ latitude: geo.latitude,
104
+ longitude: geo.longitude,
105
+ },
106
+ }),
107
+ ...(openingHoursSpecification && {
108
+ openingHoursSpecification: openingHoursSpecification.map(spec => ({
109
+ '@type': 'OpeningHoursSpecification',
110
+ dayOfWeek: spec.dayOfWeek,
111
+ opens: spec.opens,
112
+ closes: spec.closes,
113
+ ...(spec.validFrom && { validFrom: spec.validFrom }),
114
+ ...(spec.validThrough && { validThrough: spec.validThrough }),
115
+ })),
116
+ }),
117
+ ...(aggregateRating && {
118
+ aggregateRating: {
119
+ '@type': 'AggregateRating',
120
+ ratingValue: aggregateRating.ratingValue,
121
+ ...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),
122
+ ...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),
123
+ bestRating: aggregateRating.bestRating ?? 5,
124
+ worstRating: aggregateRating.worstRating ?? 1,
125
+ },
126
+ }),
127
+ ...(review && {
128
+ review: review.map(r => ({
129
+ '@type': 'Review',
130
+ author: { '@type': 'Person', name: r.author },
131
+ datePublished: r.datePublished,
132
+ reviewBody: r.reviewBody,
133
+ ...(r.reviewRating && {
134
+ reviewRating: {
135
+ '@type': 'Rating',
136
+ ratingValue: r.reviewRating.ratingValue,
137
+ bestRating: r.reviewRating.bestRating ?? 5,
138
+ worstRating: r.reviewRating.worstRating ?? 1,
139
+ },
140
+ }),
141
+ })),
142
+ }),
143
+ ...(servesCuisine && { servesCuisine }),
144
+ ...(menu && { menu }),
145
+ ...(acceptsReservations !== undefined && { acceptsReservations }),
146
+ ...(hasMenu && { hasMenu }),
147
+ ...(paymentAccepted && { paymentAccepted }),
148
+ ...(currenciesAccepted && { currenciesAccepted }),
149
+ ...(areaServed && { areaServed }),
150
+ ...(sameAs && { sameAs }),
151
+ };
152
+ ---
153
+
154
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
155
+ <slot />
@@ -0,0 +1,54 @@
1
+ ---
2
+ export interface Props {
3
+ name: string;
4
+ url: string;
5
+ description?: string;
6
+ potentialAction: {
7
+ target: string;
8
+ mathExpression?: string;
9
+ eduQuestionType?: ('Polynomial' | 'Arithmetic' | 'Calculus' | 'Geometry' | 'Statistics' | 'Algebra' | 'Trigonometry')[];
10
+ };
11
+ applicationCategory?: string;
12
+ operatingSystem?: string;
13
+ offers?: {
14
+ price: number | string;
15
+ priceCurrency: string;
16
+ };
17
+ }
18
+
19
+ const {
20
+ name,
21
+ url,
22
+ description,
23
+ potentialAction,
24
+ applicationCategory,
25
+ operatingSystem,
26
+ offers,
27
+ } = Astro.props;
28
+
29
+ const jsonLd = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'WebApplication',
32
+ name,
33
+ url,
34
+ ...(description && { description }),
35
+ ...(applicationCategory && { applicationCategory }),
36
+ ...(operatingSystem && { operatingSystem }),
37
+ ...(offers && {
38
+ offers: {
39
+ '@type': 'Offer',
40
+ price: offers.price,
41
+ priceCurrency: offers.priceCurrency,
42
+ },
43
+ }),
44
+ potentialAction: {
45
+ '@type': 'SolveMathAction',
46
+ target: potentialAction.target,
47
+ ...(potentialAction.mathExpression && { 'mathExpression-input': `required name=${potentialAction.mathExpression}` }),
48
+ ...(potentialAction.eduQuestionType && { eduQuestionType: potentialAction.eduQuestionType }),
49
+ },
50
+ };
51
+ ---
52
+
53
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
54
+ <slot />
@@ -0,0 +1,92 @@
1
+ ---
2
+ export interface Movie {
3
+ name: string;
4
+ url: string;
5
+ image?: string;
6
+ dateCreated?: string;
7
+ director?: string | string[];
8
+ actor?: string[];
9
+ description?: string;
10
+ duration?: string;
11
+ aggregateRating?: {
12
+ ratingValue: number;
13
+ ratingCount?: number;
14
+ bestRating?: number;
15
+ worstRating?: number;
16
+ };
17
+ review?: {
18
+ author: string;
19
+ datePublished: string;
20
+ reviewBody: string;
21
+ reviewRating?: {
22
+ ratingValue: number;
23
+ bestRating?: number;
24
+ worstRating?: number;
25
+ };
26
+ }[];
27
+ }
28
+
29
+ export interface Props {
30
+ movies: Movie[];
31
+ }
32
+
33
+ const { movies } = Astro.props;
34
+
35
+ const jsonLd = {
36
+ '@context': 'https://schema.org',
37
+ '@type': 'ItemList',
38
+ itemListElement: movies.map((movie, index) => ({
39
+ '@type': 'ListItem',
40
+ position: index + 1,
41
+ item: {
42
+ '@type': 'Movie',
43
+ name: movie.name,
44
+ url: movie.url,
45
+ ...(movie.image && { image: movie.image }),
46
+ ...(movie.dateCreated && { dateCreated: movie.dateCreated }),
47
+ ...(movie.director && {
48
+ director: (Array.isArray(movie.director) ? movie.director : [movie.director]).map(d => ({
49
+ '@type': 'Person',
50
+ name: d,
51
+ })),
52
+ }),
53
+ ...(movie.actor && {
54
+ actor: movie.actor.map(a => ({
55
+ '@type': 'Person',
56
+ name: a,
57
+ })),
58
+ }),
59
+ ...(movie.description && { description: movie.description }),
60
+ ...(movie.duration && { duration: movie.duration }),
61
+ ...(movie.aggregateRating && {
62
+ aggregateRating: {
63
+ '@type': 'AggregateRating',
64
+ ratingValue: movie.aggregateRating.ratingValue,
65
+ ...(movie.aggregateRating.ratingCount && { ratingCount: movie.aggregateRating.ratingCount }),
66
+ bestRating: movie.aggregateRating.bestRating ?? 10,
67
+ worstRating: movie.aggregateRating.worstRating ?? 1,
68
+ },
69
+ }),
70
+ ...(movie.review && {
71
+ review: movie.review.map(r => ({
72
+ '@type': 'Review',
73
+ author: { '@type': 'Person', name: r.author },
74
+ datePublished: r.datePublished,
75
+ reviewBody: r.reviewBody,
76
+ ...(r.reviewRating && {
77
+ reviewRating: {
78
+ '@type': 'Rating',
79
+ ratingValue: r.reviewRating.ratingValue,
80
+ bestRating: r.reviewRating.bestRating ?? 10,
81
+ worstRating: r.reviewRating.worstRating ?? 1,
82
+ },
83
+ }),
84
+ })),
85
+ }),
86
+ },
87
+ })),
88
+ };
89
+ ---
90
+
91
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
92
+ <slot />
@@ -0,0 +1,140 @@
1
+ ---
2
+ export interface Props {
3
+ name: string;
4
+ url?: string;
5
+ logo?: string;
6
+ description?: string;
7
+ email?: string;
8
+ telephone?: string;
9
+ address?: {
10
+ streetAddress?: string;
11
+ addressLocality?: string;
12
+ addressRegion?: string;
13
+ postalCode?: string;
14
+ addressCountry?: string;
15
+ };
16
+ sameAs?: string[];
17
+ foundingDate?: string;
18
+ founders?: string[];
19
+ numberOfEmployees?: number | { minValue: number; maxValue: number };
20
+ parentOrganization?: {
21
+ name: string;
22
+ url?: string;
23
+ };
24
+ subOrganization?: {
25
+ name: string;
26
+ url?: string;
27
+ }[];
28
+ contactPoint?: {
29
+ type?: 'customer service' | 'technical support' | 'sales' | 'billing support' | 'reservations';
30
+ telephone: string;
31
+ email?: string;
32
+ contactType: string;
33
+ availableLanguage?: string[];
34
+ areaServed?: string[];
35
+ }[];
36
+ areaServed?: string[];
37
+ brand?: string;
38
+ award?: string[];
39
+ memberOf?: {
40
+ name: string;
41
+ url?: string;
42
+ }[];
43
+ }
44
+
45
+ const {
46
+ name,
47
+ url,
48
+ logo,
49
+ description,
50
+ email,
51
+ telephone,
52
+ address,
53
+ sameAs,
54
+ foundingDate,
55
+ founders,
56
+ numberOfEmployees,
57
+ parentOrganization,
58
+ subOrganization,
59
+ contactPoint,
60
+ areaServed,
61
+ brand,
62
+ award,
63
+ memberOf,
64
+ } = Astro.props;
65
+
66
+ const jsonLd = {
67
+ '@context': 'https://schema.org',
68
+ '@type': 'Organization',
69
+ name,
70
+ ...(url && { url }),
71
+ ...(logo && { logo }),
72
+ ...(description && { description }),
73
+ ...(email && { email }),
74
+ ...(telephone && { telephone }),
75
+ ...(address && {
76
+ address: {
77
+ '@type': 'PostalAddress',
78
+ ...(address.streetAddress && { streetAddress: address.streetAddress }),
79
+ ...(address.addressLocality && { addressLocality: address.addressLocality }),
80
+ ...(address.addressRegion && { addressRegion: address.addressRegion }),
81
+ ...(address.postalCode && { postalCode: address.postalCode }),
82
+ ...(address.addressCountry && { addressCountry: address.addressCountry }),
83
+ },
84
+ }),
85
+ ...(sameAs && { sameAs }),
86
+ ...(foundingDate && { foundingDate }),
87
+ ...(founders && {
88
+ founders: founders.map(f => ({
89
+ '@type': 'Person',
90
+ name: f,
91
+ })),
92
+ }),
93
+ ...(numberOfEmployees && {
94
+ numberOfEmployees: typeof numberOfEmployees === 'number'
95
+ ? { '@type': 'QuantitativeValue', value: numberOfEmployees }
96
+ : {
97
+ '@type': 'QuantitativeValue',
98
+ minValue: numberOfEmployees.minValue,
99
+ maxValue: numberOfEmployees.maxValue,
100
+ },
101
+ }),
102
+ ...(parentOrganization && {
103
+ parentOrganization: {
104
+ '@type': 'Organization',
105
+ name: parentOrganization.name,
106
+ ...(parentOrganization.url && { url: parentOrganization.url }),
107
+ },
108
+ }),
109
+ ...(subOrganization && {
110
+ subOrganization: subOrganization.map(sub => ({
111
+ '@type': 'Organization',
112
+ name: sub.name,
113
+ ...(sub.url && { url: sub.url }),
114
+ })),
115
+ }),
116
+ ...(contactPoint && {
117
+ contactPoint: contactPoint.map(cp => ({
118
+ '@type': 'ContactPoint',
119
+ telephone: cp.telephone,
120
+ contactType: cp.contactType,
121
+ ...(cp.email && { email: cp.email }),
122
+ ...(cp.availableLanguage && { availableLanguage: cp.availableLanguage }),
123
+ ...(cp.areaServed && { areaServed: cp.areaServed }),
124
+ })),
125
+ }),
126
+ ...(areaServed && { areaServed }),
127
+ ...(brand && { brand: { '@type': 'Brand', name: brand } }),
128
+ ...(award && { award }),
129
+ ...(memberOf && {
130
+ memberOf: memberOf.map(m => ({
131
+ '@type': 'Organization',
132
+ name: m.name,
133
+ ...(m.url && { url: m.url }),
134
+ })),
135
+ }),
136
+ };
137
+ ---
138
+
139
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
140
+ <slot />
@@ -0,0 +1,250 @@
1
+ ---
2
+ export interface Props {
3
+ name: string;
4
+ description?: string;
5
+ image?: string | string[];
6
+ sku?: string;
7
+ mpn?: string;
8
+ gtin?: string;
9
+ gtin8?: string;
10
+ gtin13?: string;
11
+ gtin14?: string;
12
+ brand?: {
13
+ name: string;
14
+ };
15
+ offers?: {
16
+ url?: string;
17
+ price: number | string;
18
+ priceCurrency: string;
19
+ priceValidUntil?: string;
20
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder' | 'SoldOut' | 'BackOrder' | 'Discontinued' | 'OnlineOnly' | 'InStoreOnly' | 'LimitedAvailability';
21
+ itemCondition?: 'NewCondition' | 'UsedCondition' | 'RefurbishedCondition' | 'DamagedCondition';
22
+ seller?: {
23
+ name: string;
24
+ };
25
+ shippingDetails?: {
26
+ shippingRate?: {
27
+ value: number;
28
+ currency: string;
29
+ };
30
+ shippingDestination?: {
31
+ addressCountry: string;
32
+ };
33
+ deliveryTime?: {
34
+ handlingTime?: { minValue: number; maxValue: number };
35
+ transitTime?: { minValue: number; maxValue: number };
36
+ };
37
+ };
38
+ hasMerchantReturnPolicy?: {
39
+ applicableCountry: string;
40
+ returnPolicyCategory: 'MerchantReturnFiniteReturnWindow' | 'MerchantReturnNotPermitted' | 'MerchantReturnUnlimitedWindow';
41
+ merchantReturnDays?: number;
42
+ returnMethod?: 'ReturnByMail' | 'ReturnInStore';
43
+ returnFees?: 'FreeReturn' | 'ReturnFeesCustomerResponsibility';
44
+ };
45
+ } | {
46
+ url?: string;
47
+ price: number | string;
48
+ priceCurrency: string;
49
+ priceValidUntil?: string;
50
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder' | 'SoldOut' | 'BackOrder' | 'Discontinued' | 'OnlineOnly' | 'InStoreOnly' | 'LimitedAvailability';
51
+ itemCondition?: 'NewCondition' | 'UsedCondition' | 'RefurbishedCondition' | 'DamagedCondition';
52
+ seller?: {
53
+ name: string;
54
+ };
55
+ }[];
56
+ aggregateRating?: {
57
+ ratingValue: number;
58
+ reviewCount?: number;
59
+ ratingCount?: number;
60
+ bestRating?: number;
61
+ worstRating?: number;
62
+ };
63
+ review?: {
64
+ author: {
65
+ name: string;
66
+ };
67
+ datePublished: string;
68
+ reviewBody?: string;
69
+ reviewRating?: {
70
+ ratingValue: number;
71
+ bestRating?: number;
72
+ worstRating?: number;
73
+ };
74
+ }[];
75
+ color?: string;
76
+ material?: string;
77
+ size?: string;
78
+ weight?: {
79
+ value: number;
80
+ unitCode: string;
81
+ };
82
+ category?: string;
83
+ productID?: string;
84
+ model?: string;
85
+ manufacturer?: {
86
+ name: string;
87
+ };
88
+ }
89
+
90
+ const {
91
+ name,
92
+ description,
93
+ image,
94
+ sku,
95
+ mpn,
96
+ gtin,
97
+ gtin8,
98
+ gtin13,
99
+ gtin14,
100
+ brand,
101
+ offers,
102
+ aggregateRating,
103
+ review,
104
+ color,
105
+ material,
106
+ size,
107
+ weight,
108
+ category,
109
+ productID,
110
+ model,
111
+ manufacturer,
112
+ } = Astro.props;
113
+
114
+ const formatOffer = (offer: any) => ({
115
+ '@type': 'Offer',
116
+ ...(offer.url && { url: offer.url }),
117
+ price: offer.price,
118
+ priceCurrency: offer.priceCurrency,
119
+ ...(offer.priceValidUntil && { priceValidUntil: offer.priceValidUntil }),
120
+ ...(offer.availability && { availability: `https://schema.org/${offer.availability}` }),
121
+ ...(offer.itemCondition && { itemCondition: `https://schema.org/${offer.itemCondition}` }),
122
+ ...(offer.seller && {
123
+ seller: {
124
+ '@type': 'Organization',
125
+ name: offer.seller.name,
126
+ },
127
+ }),
128
+ ...(offer.shippingDetails && {
129
+ shippingDetails: {
130
+ '@type': 'OfferShippingDetails',
131
+ ...(offer.shippingDetails.shippingRate && {
132
+ shippingRate: {
133
+ '@type': 'MonetaryAmount',
134
+ value: offer.shippingDetails.shippingRate.value,
135
+ currency: offer.shippingDetails.shippingRate.currency,
136
+ },
137
+ }),
138
+ ...(offer.shippingDetails.shippingDestination && {
139
+ shippingDestination: {
140
+ '@type': 'DefinedRegion',
141
+ addressCountry: offer.shippingDetails.shippingDestination.addressCountry,
142
+ },
143
+ }),
144
+ ...(offer.shippingDetails.deliveryTime && {
145
+ deliveryTime: {
146
+ '@type': 'ShippingDeliveryTime',
147
+ ...(offer.shippingDetails.deliveryTime.handlingTime && {
148
+ handlingTime: {
149
+ '@type': 'QuantitativeValue',
150
+ minValue: offer.shippingDetails.deliveryTime.handlingTime.minValue,
151
+ maxValue: offer.shippingDetails.deliveryTime.handlingTime.maxValue,
152
+ unitCode: 'DAY',
153
+ },
154
+ }),
155
+ ...(offer.shippingDetails.deliveryTime.transitTime && {
156
+ transitTime: {
157
+ '@type': 'QuantitativeValue',
158
+ minValue: offer.shippingDetails.deliveryTime.transitTime.minValue,
159
+ maxValue: offer.shippingDetails.deliveryTime.transitTime.maxValue,
160
+ unitCode: 'DAY',
161
+ },
162
+ }),
163
+ },
164
+ }),
165
+ },
166
+ }),
167
+ ...(offer.hasMerchantReturnPolicy && {
168
+ hasMerchantReturnPolicy: {
169
+ '@type': 'MerchantReturnPolicy',
170
+ applicableCountry: offer.hasMerchantReturnPolicy.applicableCountry,
171
+ returnPolicyCategory: `https://schema.org/${offer.hasMerchantReturnPolicy.returnPolicyCategory}`,
172
+ ...(offer.hasMerchantReturnPolicy.merchantReturnDays && {
173
+ merchantReturnDays: offer.hasMerchantReturnPolicy.merchantReturnDays,
174
+ }),
175
+ ...(offer.hasMerchantReturnPolicy.returnMethod && {
176
+ returnMethod: `https://schema.org/${offer.hasMerchantReturnPolicy.returnMethod}`,
177
+ }),
178
+ ...(offer.hasMerchantReturnPolicy.returnFees && {
179
+ returnFees: `https://schema.org/${offer.hasMerchantReturnPolicy.returnFees}`,
180
+ }),
181
+ },
182
+ }),
183
+ });
184
+
185
+ const jsonLd = {
186
+ '@context': 'https://schema.org',
187
+ '@type': 'Product',
188
+ name,
189
+ ...(description && { description }),
190
+ ...(image && { image: Array.isArray(image) ? image : [image] }),
191
+ ...(sku && { sku }),
192
+ ...(mpn && { mpn }),
193
+ ...(gtin && { gtin }),
194
+ ...(gtin8 && { gtin8 }),
195
+ ...(gtin13 && { gtin13 }),
196
+ ...(gtin14 && { gtin14 }),
197
+ ...(brand && { brand: { '@type': 'Brand', name: brand.name } }),
198
+ ...(offers && {
199
+ offers: Array.isArray(offers) ? offers.map(formatOffer) : formatOffer(offers),
200
+ }),
201
+ ...(aggregateRating && {
202
+ aggregateRating: {
203
+ '@type': 'AggregateRating',
204
+ ratingValue: aggregateRating.ratingValue,
205
+ ...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),
206
+ ...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),
207
+ bestRating: aggregateRating.bestRating ?? 5,
208
+ worstRating: aggregateRating.worstRating ?? 1,
209
+ },
210
+ }),
211
+ ...(review && {
212
+ review: review.map(r => ({
213
+ '@type': 'Review',
214
+ author: { '@type': 'Person', name: r.author.name },
215
+ datePublished: r.datePublished,
216
+ ...(r.reviewBody && { reviewBody: r.reviewBody }),
217
+ ...(r.reviewRating && {
218
+ reviewRating: {
219
+ '@type': 'Rating',
220
+ ratingValue: r.reviewRating.ratingValue,
221
+ bestRating: r.reviewRating.bestRating ?? 5,
222
+ worstRating: r.reviewRating.worstRating ?? 1,
223
+ },
224
+ }),
225
+ })),
226
+ }),
227
+ ...(color && { color }),
228
+ ...(material && { material }),
229
+ ...(size && { size }),
230
+ ...(weight && {
231
+ weight: {
232
+ '@type': 'QuantitativeValue',
233
+ value: weight.value,
234
+ unitCode: weight.unitCode,
235
+ },
236
+ }),
237
+ ...(category && { category }),
238
+ ...(productID && { productID }),
239
+ ...(model && { model }),
240
+ ...(manufacturer && {
241
+ manufacturer: {
242
+ '@type': 'Organization',
243
+ name: manufacturer.name,
244
+ },
245
+ }),
246
+ };
247
+ ---
248
+
249
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
250
+ <slot />