@pixelated-tech/components 3.13.14 → 3.13.16

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 (68) hide show
  1. package/dist/components/general/schema.functions.js +114 -0
  2. package/dist/components/general/schema.js +662 -0
  3. package/dist/components/general/sitemap.js +74 -31
  4. package/dist/components/general/utilities.js +38 -0
  5. package/dist/components/integrations/spotify.components.js +43 -0
  6. package/dist/components/integrations/spotify.functions.js +111 -0
  7. package/dist/components/integrations/wordpress.components.js +2 -2
  8. package/dist/config/pixelated.config.json.enc +1 -1
  9. package/dist/index.js +7 -13
  10. package/dist/index.server.js +3 -1
  11. package/dist/scripts/release.sh +9 -15
  12. package/dist/scripts/update.sh +45 -8
  13. package/dist/types/components/general/schema.d.ts +266 -0
  14. package/dist/types/components/general/schema.d.ts.map +1 -0
  15. package/dist/types/components/general/schema.functions.d.ts +77 -0
  16. package/dist/types/components/general/schema.functions.d.ts.map +1 -0
  17. package/dist/types/components/general/sitemap.d.ts +4 -4
  18. package/dist/types/components/general/sitemap.d.ts.map +1 -1
  19. package/dist/types/components/general/utilities.d.ts +2 -0
  20. package/dist/types/components/general/utilities.d.ts.map +1 -1
  21. package/dist/types/components/integrations/spotify.components.d.ts +27 -0
  22. package/dist/types/components/integrations/spotify.components.d.ts.map +1 -0
  23. package/dist/types/components/integrations/spotify.functions.d.ts +57 -0
  24. package/dist/types/components/integrations/spotify.functions.d.ts.map +1 -0
  25. package/dist/types/index.d.ts +7 -13
  26. package/dist/types/index.server.d.ts +3 -1
  27. package/dist/types/stories/general/schema.stories.d.ts +1 -1
  28. package/dist/types/stories/general/schema.stories.d.ts.map +1 -1
  29. package/dist/types/stories/integrations/loremipsum.stories.d.ts +1 -1
  30. package/dist/types/stories/integrations/schema-podcast.stories.d.ts +45 -0
  31. package/dist/types/stories/integrations/schema-podcast.stories.d.ts.map +1 -0
  32. package/dist/types/stories/integrations/spotify.stories.d.ts +19 -0
  33. package/dist/types/stories/integrations/spotify.stories.d.ts.map +1 -0
  34. package/dist/types/tests/schema-podcast.test.d.ts +2 -0
  35. package/dist/types/tests/schema-podcast.test.d.ts.map +1 -0
  36. package/dist/types/tests/spotify.test.d.ts +2 -0
  37. package/dist/types/tests/spotify.test.d.ts.map +1 -0
  38. package/package.json +46 -42
  39. package/dist/components/general/schema-blogposting.functions.js +0 -44
  40. package/dist/components/general/schema-blogposting.js +0 -18
  41. package/dist/components/general/schema-breadcrumb.js +0 -78
  42. package/dist/components/general/schema-faq.js +0 -38
  43. package/dist/components/general/schema-localbusiness.js +0 -125
  44. package/dist/components/general/schema-product.js +0 -51
  45. package/dist/components/general/schema-recipe.js +0 -58
  46. package/dist/components/general/schema-review.js +0 -47
  47. package/dist/components/general/schema-services.js +0 -79
  48. package/dist/components/general/schema-website.js +0 -148
  49. package/dist/types/components/general/schema-blogposting.d.ts +0 -10
  50. package/dist/types/components/general/schema-blogposting.d.ts.map +0 -1
  51. package/dist/types/components/general/schema-blogposting.functions.d.ts +0 -27
  52. package/dist/types/components/general/schema-blogposting.functions.d.ts.map +0 -1
  53. package/dist/types/components/general/schema-breadcrumb.d.ts +0 -14
  54. package/dist/types/components/general/schema-breadcrumb.d.ts.map +0 -1
  55. package/dist/types/components/general/schema-faq.d.ts +0 -10
  56. package/dist/types/components/general/schema-faq.d.ts.map +0 -1
  57. package/dist/types/components/general/schema-localbusiness.d.ts +0 -53
  58. package/dist/types/components/general/schema-localbusiness.d.ts.map +0 -1
  59. package/dist/types/components/general/schema-product.d.ts +0 -38
  60. package/dist/types/components/general/schema-product.d.ts.map +0 -1
  61. package/dist/types/components/general/schema-recipe.d.ts +0 -33
  62. package/dist/types/components/general/schema-recipe.d.ts.map +0 -1
  63. package/dist/types/components/general/schema-review.d.ts +0 -34
  64. package/dist/types/components/general/schema-review.d.ts.map +0 -1
  65. package/dist/types/components/general/schema-services.d.ts +0 -36
  66. package/dist/types/components/general/schema-services.d.ts.map +0 -1
  67. package/dist/types/components/general/schema-website.d.ts +0 -40
  68. package/dist/types/components/general/schema-website.d.ts.map +0 -1
@@ -0,0 +1,662 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ /* ========================================
5
+ SCHEMA HELPER COMPONENTS
6
+ ======================================== */
7
+ function SchemaScript({ schema }) {
8
+ return _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } });
9
+ }
10
+ /* ========================================
11
+ BLOG POSTING SCHEMA COMPONENTS
12
+ ======================================== */
13
+ /**
14
+ * SchemaBlogPosting — Inject a JSON-LD <script> tag containing a BlogPosting schema object.
15
+ *
16
+ * @param {object} [props.post] - Structured JSON-LD object representing a blog post (BlogPosting schema).
17
+ */
18
+ SchemaBlogPosting.propTypes = {
19
+ /** Structured BlogPosting JSON-LD object */
20
+ post: PropTypes.object.isRequired,
21
+ };
22
+ export function SchemaBlogPosting(props) {
23
+ const { post } = props;
24
+ return (_jsx(SchemaScript, { schema: post }));
25
+ }
26
+ /**
27
+ * Build breadcrumb trail from root to current path.
28
+ * e.g., "/store/item-slug" produces ["/", "/store", "/store/item-slug"]
29
+ */
30
+ function buildPathSegments(currentPath) {
31
+ const segments = ['/'];
32
+ if (currentPath === '/')
33
+ return segments;
34
+ const parts = currentPath.split('/').filter(Boolean);
35
+ let accumulated = '';
36
+ for (const part of parts) {
37
+ accumulated += '/' + part;
38
+ segments.push(accumulated);
39
+ }
40
+ return segments;
41
+ }
42
+ /**
43
+ * Determine breadcrumb name for a path segment.
44
+ * Uses route name if exact match found, otherwise uses humanized path segment.
45
+ */
46
+ function getSegmentName(routes, path, segment) {
47
+ if (path === '/')
48
+ return 'Home';
49
+ // Only use exact route matches with valid paths to avoid duplicating parent breadcrumb names
50
+ const route = routes.find((r) => r.path && r.path === path);
51
+ if (route)
52
+ return route.name || segment;
53
+ // Fallback: humanize the path segment
54
+ return segment
55
+ .split('-')
56
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
57
+ .join(' ');
58
+ }
59
+ /**
60
+ * BreadcrumbListSchema — auto-generates a breadcrumb list as JSON-LD from routes.json data.
61
+ * Parses the current path, builds breadcrumb trail by matching path segments to routes array,
62
+ * and embeds as schema.org/BreadcrumbList for SEO rich snippets.
63
+ * Accepts flexible route objects from routes.json with any additional properties.
64
+ *
65
+ * @param {array} [props.routes] - Routes array from routes.json with name and optional path properties.
66
+ * @param {string} [props.currentPath] - Current page path (e.g. "/store/vintage-oakley"). Defaults to "/" if not provided.
67
+ * @param {string} [props.siteUrl] - Full domain URL from siteInfo.url. Defaults to https://example.com.
68
+ */
69
+ BreadcrumbListSchema.propTypes = {
70
+ /** Routes array from routes.json. Accepts routes with any properties; only uses name and path. */
71
+ routes: PropTypes.arrayOf(PropTypes.object).isRequired,
72
+ /** Current page path to generate breadcrumbs for (e.g. "/store/item-slug"). Defaults to "/". */
73
+ currentPath: PropTypes.string,
74
+ /** Site domain URL for constructing full breadcrumb URLs. Defaults to https://example.com. */
75
+ siteUrl: PropTypes.string,
76
+ };
77
+ export function BreadcrumbListSchema({ routes, currentPath = '/', siteUrl = 'https://example.com', }) {
78
+ // Type-safe conversion: routes prop is now flexible (accepts any object)
79
+ // Filter to ensure only valid Route objects with 'name' property
80
+ const validRoutes = (Array.isArray(routes)
81
+ ? routes.filter((r) => !!(r && typeof r === 'object' && 'name' in r))
82
+ : []);
83
+ const pathSegments = buildPathSegments(currentPath || '/');
84
+ const finalSiteUrl = siteUrl || 'https://example.com';
85
+ const itemListElement = pathSegments.map((path, index) => {
86
+ const segment = path.split('/').filter(Boolean).pop() || 'Home';
87
+ return {
88
+ '@type': 'ListItem',
89
+ 'position': index + 1,
90
+ 'name': getSegmentName(validRoutes, path, segment),
91
+ 'item': `${finalSiteUrl.replace(/\/$/, '')}${path}`,
92
+ };
93
+ });
94
+ const jsonLD = {
95
+ '@context': 'https://schema.org',
96
+ '@type': 'BreadcrumbList',
97
+ 'itemListElement': itemListElement,
98
+ };
99
+ return (_jsx(SchemaScript, { schema: jsonLD }));
100
+ }
101
+ // normalizeFaqs turns a JSON-LD FAQPage payload into a form where each
102
+ // question has a single `acceptedAnswer.text` string. Some of our data
103
+ // sources (WordPress, CMS exports) allow multiple answer fragments; we
104
+ // merge them here so the final JSON remains valid for search engines.
105
+ function normalizeFaqs(data) {
106
+ if (!data || typeof data !== 'object')
107
+ return data;
108
+ const faqs = JSON.parse(JSON.stringify(data));
109
+ if (Array.isArray(faqs.mainEntity)) {
110
+ faqs.mainEntity.forEach((entry) => {
111
+ if (entry && entry.acceptedAnswer) {
112
+ const ans = entry.acceptedAnswer;
113
+ if (ans && Array.isArray(ans.text)) {
114
+ ans.text = ans.text.join(' ');
115
+ }
116
+ }
117
+ });
118
+ }
119
+ return faqs;
120
+ }
121
+ /**
122
+ * SchemaFAQ — Inject a JSON-LD <script> tag containing an FAQPage schema object.
123
+ *
124
+ * @param {object} [props.faqsData] - Structured JSON-LD object representing an FAQ page (FAQPage schema).
125
+ */
126
+ SchemaFAQ.propTypes = {
127
+ /** Structured FAQPage JSON-LD object */
128
+ faqsData: PropTypes.object.isRequired,
129
+ };
130
+ export function SchemaFAQ({ faqsData }) {
131
+ const normalized = normalizeFaqs(faqsData);
132
+ return (_jsx(SchemaScript, { schema: normalized }));
133
+ }
134
+ /**
135
+ * LocalBusiness Schema Component
136
+ * Generates JSON-LD structured data for SEO
137
+ * https://schema.org/LocalBusiness
138
+ *
139
+ * This component uses siteInfo passed as props to generate schema data.
140
+ * It does not use client-side hooks and can be rendered on the server.
141
+ */
142
+ /**
143
+ * LocalBusinessSchema — generates JSON-LD for a LocalBusiness using provided props or a fallback `siteInfo`.
144
+ *
145
+ * @param {string} [props.name] - Business name (overrides siteInfo.name).
146
+ * @param {object} [props.address] - Address object containing streetAddress, addressLocality, addressRegion, postalCode, and addressCountry.
147
+ * @param {string} [props.streetAddress] - Street address line.
148
+ * @param {string} [props.addressLocality] - City or locality.
149
+ * @param {string} [props.addressRegion] - State, region or province.
150
+ * @param {string} [props.postalCode] - Postal/ZIP code.
151
+ * @param {string} [props.addressCountry] - Country (defaults to 'United States' when missing).
152
+ * @param {string} [props.telephone] - Contact phone number.
153
+ * @param {string} [props.url] - Canonical website URL.
154
+ * @param {string} [props.logo] - Logo image URL.
155
+ * @param {string} [props.image] - Representative image URL.
156
+ * @param {oneOfType} [props.openingHours] - Opening hours string or array in schema.org format.
157
+ * @param {string} [props.description] - Short business description.
158
+ * @param {string} [props.email] - Contact email address.
159
+ * @param {string} [props.priceRange] - Price range (e.g. '$$', optional).
160
+ * @param {arrayOf} [props.sameAs] - Array of social/profile URLs for schema 'sameAs'.
161
+ * @param {object} [props.siteInfo] - Site-level fallback information object.
162
+ */
163
+ LocalBusinessSchema.propTypes = {
164
+ /** Business name to include in schema (falls back to siteInfo.name). */
165
+ name: PropTypes.string,
166
+ /** Address object for the business */
167
+ address: PropTypes.shape({
168
+ /** Street address for the business. */
169
+ streetAddress: PropTypes.string,
170
+ /** City or locality for the business address. */
171
+ addressLocality: PropTypes.string,
172
+ /** State/region for the business address. */
173
+ addressRegion: PropTypes.string,
174
+ /** Postal or ZIP code for the address. */
175
+ postalCode: PropTypes.string,
176
+ /** Country for the address (defaults to United States when absent). */
177
+ addressCountry: PropTypes.string,
178
+ }),
179
+ /** Street address for the business. */
180
+ streetAddress: PropTypes.string,
181
+ /** City or locality for the business address. */
182
+ addressLocality: PropTypes.string,
183
+ /** State/region for the business address. */
184
+ addressRegion: PropTypes.string,
185
+ /** Postal or ZIP code for the address. */
186
+ postalCode: PropTypes.string,
187
+ /** Country for the address (defaults to United States when absent). */
188
+ addressCountry: PropTypes.string,
189
+ /** Contact telephone number. */
190
+ telephone: PropTypes.string,
191
+ /** Canonical website URL. */
192
+ url: PropTypes.string,
193
+ /** Logo image URL for schema/logo property. */
194
+ logo: PropTypes.string,
195
+ /** Representative image URL. */
196
+ image: PropTypes.string,
197
+ /** Opening hours as a string or array in schema.org format (e.g., "Mo-Fr 09:00-17:00"). */
198
+ openingHours: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
199
+ /** Short description for schema. */
200
+ description: PropTypes.string,
201
+ /** Contact email address. */
202
+ email: PropTypes.string,
203
+ /** Price range string (e.g. '$$'). */
204
+ priceRange: PropTypes.string,
205
+ /** Array of profile/URL strings for sameAs (social links). */
206
+ sameAs: PropTypes.arrayOf(PropTypes.string), // Social media profiles
207
+ /** Site-level fallback information object (used when props omitted). */
208
+ siteInfo: PropTypes.object // Required siteinfo from parent component
209
+ };
210
+ export function LocalBusinessSchema(props) {
211
+ // const config = usePixelatedConfig();
212
+ const siteInfo = props.siteInfo;
213
+ // Use props if provided, otherwise fall back to siteInfo
214
+ const name = props.name || siteInfo?.name;
215
+ const address = props.address || siteInfo?.address;
216
+ const streetAddress = props.streetAddress || siteInfo?.address?.streetAddress;
217
+ const addressLocality = props.addressLocality || siteInfo?.address?.addressLocality;
218
+ const addressRegion = props.addressRegion || siteInfo?.address?.addressRegion;
219
+ const postalCode = props.postalCode || siteInfo?.address?.postalCode;
220
+ const addressCountry = props.addressCountry || siteInfo?.address?.addressCountry || 'United States';
221
+ const telephone = props.telephone || siteInfo?.telephone;
222
+ const url = props.url || siteInfo?.url;
223
+ const logo = props.logo || siteInfo?.image;
224
+ const image = props.image || siteInfo?.image || logo;
225
+ const openingHours = props.openingHours;
226
+ const description = props.description || siteInfo?.description;
227
+ const email = props.email || siteInfo?.email;
228
+ const priceRange = props.priceRange || siteInfo?.priceRange;
229
+ const sameAs = props.sameAs || siteInfo?.sameAs;
230
+ const schemaData = {
231
+ '@context': 'https://schema.org',
232
+ '@type': 'LocalBusiness',
233
+ name,
234
+ address: {
235
+ '@type': 'PostalAddress',
236
+ ...(address || {
237
+ streetAddress,
238
+ addressLocality,
239
+ addressRegion,
240
+ postalCode,
241
+ addressCountry
242
+ })
243
+ },
244
+ telephone,
245
+ url,
246
+ ...(logo && { logo }),
247
+ ...(image && { image }),
248
+ ...(openingHours && { openingHours }),
249
+ ...(description && { description }),
250
+ ...(email && { email }),
251
+ ...(priceRange && { priceRange }),
252
+ ...(sameAs && sameAs.length > 0 && { sameAs })
253
+ };
254
+ return (_jsx(SchemaScript, { schema: schemaData }));
255
+ }
256
+ /* ========================================
257
+ PODCAST SCHEMA COMPONENTS
258
+ ======================================== */
259
+ /**
260
+ * SchemaPodcastEpisode — Inject a JSON-LD <script> tag containing a PodcastEpisode schema object.
261
+ *
262
+ * @param {object} [props.episode] - Structured JSON-LD object representing a podcast episode (PodcastEpisode schema).
263
+ */
264
+ SchemaPodcastEpisode.propTypes = {
265
+ episode: PropTypes.object.isRequired,
266
+ };
267
+ export function SchemaPodcastEpisode(props) {
268
+ const { episode } = props;
269
+ return (_jsx(SchemaScript, { schema: episode }));
270
+ }
271
+ /**
272
+ * SchemaPodcastSeries — Inject a JSON-LD <script> tag containing a PodcastSeries schema object.
273
+ *
274
+ * @param {object} [props.series] - Structured JSON-LD object representing a podcast series (PodcastSeries schema).
275
+ */
276
+ SchemaPodcastSeries.propTypes = {
277
+ series: PropTypes.object.isRequired,
278
+ };
279
+ export function SchemaPodcastSeries(props) {
280
+ const { series } = props;
281
+ return (_jsx(SchemaScript, { schema: series }));
282
+ }
283
+ /* ========================================
284
+ PRODUCT SCHEMA COMPONENTS
285
+ ======================================== */
286
+ /**
287
+ * ProductSchema — embeds a product/offer as JSON-LD for SEO (schema.org/Product).
288
+ *
289
+ * @param {shape} [props.product] - Product object conforming to schema.org/Product; will be serialized as JSON-LD.
290
+ * @param {string} [props.product.name] - The product name.
291
+ * @param {string} [props.product.description] - Product description.
292
+ * @param {shape} [props.product.brand] - Brand information (name and @type).
293
+ * @param {shape} [props.product.offers] - Offer information including price, currency, URL, and availability.
294
+ */
295
+ ProductSchema.propTypes = {
296
+ /** Product information object to be serialized as JSON-LD. */
297
+ product: PropTypes.shape({
298
+ '@context': PropTypes.string.isRequired,
299
+ '@type': PropTypes.string.isRequired,
300
+ name: PropTypes.string.isRequired,
301
+ description: PropTypes.string,
302
+ image: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
303
+ brand: PropTypes.shape({
304
+ '@type': PropTypes.string.isRequired,
305
+ name: PropTypes.string.isRequired,
306
+ }),
307
+ offers: PropTypes.oneOfType([
308
+ PropTypes.shape({
309
+ '@type': PropTypes.string.isRequired,
310
+ url: PropTypes.string,
311
+ priceCurrency: PropTypes.string,
312
+ price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
313
+ availability: PropTypes.string,
314
+ }),
315
+ PropTypes.arrayOf(PropTypes.shape({
316
+ '@type': PropTypes.string.isRequired,
317
+ url: PropTypes.string,
318
+ priceCurrency: PropTypes.string,
319
+ price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
320
+ availability: PropTypes.string,
321
+ }))
322
+ ]),
323
+ aggregateRating: PropTypes.shape({
324
+ '@type': PropTypes.string,
325
+ ratingValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
326
+ reviewCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
327
+ }),
328
+ }).isRequired,
329
+ };
330
+ export function ProductSchema(props) {
331
+ const { product } = props;
332
+ return (_jsx(SchemaScript, { schema: product }));
333
+ }
334
+ /* ========================================
335
+ RECIPE SCHEMA COMPONENTS
336
+ ======================================== */
337
+ /**
338
+ * Recipe Schema Component
339
+ * Generates JSON-LD structured data for recipes
340
+ * https://schema.org/Recipe
341
+ */
342
+ /**
343
+ * RecipeSchema — embeds a recipe as JSON-LD for SEO (schema.org/Recipe).
344
+ *
345
+ * @param {shape} [props.recipe] - Recipe object conforming to schema.org/Recipe; will be serialized as JSON-LD.
346
+ * @param {string} [props.name] - Recipe title.
347
+ * @param {string} [props.description] - Short recipe description.
348
+ * @param {shape} [props.author] - Author information (name and @type).
349
+ * @param {string} [props.datePublished] - ISO date the recipe was published.
350
+ * @param {string} [props.image] - Primary image URL for the recipe.
351
+ * @param {string} [props.recipeYield] - Yield or serving size (e.g., '4 servings').
352
+ * @param {string} [props.prepTime] - Prep time in ISO 8601 duration (e.g. 'PT20M').
353
+ * @param {string} [props.cookTime] - Cook time in ISO 8601 duration.
354
+ * @param {string} [props.totalTime] - Total time in ISO 8601 duration.
355
+ * @param {string} [props.recipeCategory] - Category of the recipe (e.g., 'Dessert').
356
+ * @param {string} [props.recipeCuisine] - Cuisine (e.g., 'Italian').
357
+ * @param {arrayOf} [props.recipeIngredient] - List of ingredient strings.
358
+ * @param {arrayOf} [props.recipeInstructions] - Structured list of instruction steps or paragraphs.
359
+ * @param {string} [props.license] - License URL or short string for the recipe content.
360
+ */
361
+ RecipeSchema.propTypes = {
362
+ /** Recipe information object to be serialized as JSON-LD. */
363
+ recipe: PropTypes.shape({
364
+ '@context': PropTypes.string.isRequired,
365
+ '@type': PropTypes.string.isRequired,
366
+ name: PropTypes.string.isRequired,
367
+ description: PropTypes.string,
368
+ author: PropTypes.shape({
369
+ '@type': PropTypes.string.isRequired,
370
+ name: PropTypes.string.isRequired,
371
+ }),
372
+ datePublished: PropTypes.string,
373
+ image: PropTypes.string,
374
+ recipeYield: PropTypes.string,
375
+ prepTime: PropTypes.string,
376
+ cookTime: PropTypes.string,
377
+ totalTime: PropTypes.string,
378
+ recipeCategory: PropTypes.string,
379
+ recipeCuisine: PropTypes.string,
380
+ recipeIngredient: PropTypes.arrayOf(PropTypes.string),
381
+ recipeInstructions: PropTypes.arrayOf(PropTypes.shape({
382
+ '@type': PropTypes.string.isRequired,
383
+ text: PropTypes.string.isRequired,
384
+ })),
385
+ license: PropTypes.string,
386
+ }).isRequired,
387
+ };
388
+ export function RecipeSchema(props) {
389
+ const { recipe } = props;
390
+ return (_jsx(SchemaScript, { schema: recipe }));
391
+ }
392
+ /* ========================================
393
+ REVIEW SCHEMA COMPONENTS
394
+ ======================================== */
395
+ /**
396
+ * ReviewSchema — embeds a review as JSON-LD for SEO (schema.org/Review).
397
+ *
398
+ * @param {shape} [props.review] - Review object conforming to schema.org/Review; will be serialized as JSON-LD.
399
+ * @param {string} [props.review.name] - The headline or title of the review.
400
+ * @param {string} [props.review.reviewBody] - The body of the review content.
401
+ * @param {string} [props.review.datePublished] - ISO date the review was published.
402
+ * @param {shape} [props.review.author] - Author information (name and @type).
403
+ * @param {shape} [props.review.itemReviewed] - The item being reviewed (product, service, etc.).
404
+ * @param {shape} [props.review.reviewRating] - Rating information including ratingValue, bestRating, worstRating.
405
+ * @param {shape} [props.review.publisher] - Organization publishing the review.
406
+ */
407
+ ReviewSchema.propTypes = {
408
+ /** Review information object to be serialized as JSON-LD. */
409
+ review: PropTypes.shape({
410
+ '@context': PropTypes.string.isRequired,
411
+ '@type': PropTypes.string.isRequired,
412
+ name: PropTypes.string.isRequired,
413
+ reviewBody: PropTypes.string,
414
+ datePublished: PropTypes.string,
415
+ author: PropTypes.shape({
416
+ '@type': PropTypes.string.isRequired,
417
+ name: PropTypes.string.isRequired,
418
+ }),
419
+ itemReviewed: PropTypes.shape({
420
+ '@type': PropTypes.string.isRequired,
421
+ name: PropTypes.string,
422
+ }),
423
+ reviewRating: PropTypes.shape({
424
+ '@type': PropTypes.string.isRequired,
425
+ ratingValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
426
+ bestRating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
427
+ worstRating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
428
+ }),
429
+ publisher: PropTypes.shape({
430
+ '@type': PropTypes.string.isRequired,
431
+ name: PropTypes.string,
432
+ }),
433
+ }).isRequired,
434
+ };
435
+ export function ReviewSchema(props) {
436
+ const { review } = props;
437
+ return (_jsx(SchemaScript, { schema: review }));
438
+ }
439
+ /* ========================================
440
+ SERVICES SCHEMA COMPONENTS
441
+ ======================================== */
442
+ /**
443
+ * Services Schema Component
444
+ * Generates JSON-LD structured data for services
445
+ * https://schema.org/Service
446
+ */
447
+ /**
448
+ * ServicesSchema — Inject JSON-LD <script> tags for each service offered by the business, using schema.org/Service format.
449
+ *
450
+ * @param {object} [props.siteInfo] - Optional site information object containing business details and services array.
451
+ * @param {object} [props.provider] - Optional provider information object to override siteInfo for the service provider.
452
+ * @param {array} [props.services] - Optional array of service objects to override siteInfo.services.
453
+ */
454
+ ServicesSchema.propTypes = {
455
+ siteInfo: PropTypes.shape({
456
+ name: PropTypes.string,
457
+ url: PropTypes.string,
458
+ image: PropTypes.string,
459
+ telephone: PropTypes.string,
460
+ email: PropTypes.string,
461
+ services: PropTypes.arrayOf(PropTypes.shape({
462
+ name: PropTypes.string.isRequired,
463
+ description: PropTypes.string.isRequired,
464
+ url: PropTypes.string,
465
+ image: PropTypes.string,
466
+ areaServed: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
467
+ }))
468
+ }),
469
+ provider: PropTypes.shape({
470
+ name: PropTypes.string.isRequired,
471
+ url: PropTypes.string.isRequired,
472
+ logo: PropTypes.string,
473
+ telephone: PropTypes.string,
474
+ email: PropTypes.string,
475
+ }),
476
+ services: PropTypes.arrayOf(PropTypes.shape({
477
+ name: PropTypes.string.isRequired,
478
+ description: PropTypes.string.isRequired,
479
+ url: PropTypes.string,
480
+ image: PropTypes.string,
481
+ areaServed: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
482
+ })),
483
+ };
484
+ export function ServicesSchema(props) {
485
+ const siteInfo = props.siteInfo;
486
+ const services = (siteInfo?.services || props.services || []);
487
+ const provider = props.provider || {
488
+ name: siteInfo?.name || '',
489
+ url: siteInfo?.url || '',
490
+ logo: siteInfo?.image,
491
+ telephone: siteInfo?.telephone,
492
+ email: siteInfo?.email
493
+ };
494
+ if (!services.length || !provider.name) {
495
+ return null;
496
+ }
497
+ const serviceObjects = services.filter((service) => service != null).map((service) => ({
498
+ '@type': 'Service',
499
+ name: service.name,
500
+ description: service.description,
501
+ ...(service.url && { url: service.url }),
502
+ ...(service.image && { image: service.image }),
503
+ ...(service.areaServed && { areaServed: service.areaServed }),
504
+ provider: {
505
+ '@type': 'LocalBusiness',
506
+ name: provider.name,
507
+ url: provider.url,
508
+ ...(provider.logo && { logo: provider.logo }),
509
+ ...(provider.telephone && { telephone: provider.telephone }),
510
+ ...(provider.email && { email: provider.email })
511
+ }
512
+ }));
513
+ return (_jsx(_Fragment, { children: serviceObjects.map((service, idx) => (_jsx(SchemaScript, { schema: { '@context': 'https://schema.org', ...service } }, idx))) }));
514
+ }
515
+ /* ========================================
516
+ WEBSITE SCHEMA COMPONENTS
517
+ ======================================== */
518
+ /**
519
+ * Website Schema Component
520
+ * Generates JSON-LD structured data for websites
521
+ * https://schema.org/WebSite
522
+ */
523
+ /**
524
+ * WebsiteSchema — Inject a JSON-LD <script> tag containing a WebSite schema object, using provided props or siteInfo from config.
525
+ *
526
+ * @param {object} [props.siteInfo] - Optional site information object containing business details to populate the schema.
527
+ * @param {string} [props.name] - Name of the website (overrides siteInfo.name).
528
+ * @param {string} [props.url] - URL of the website (overrides siteInfo.url).
529
+ * @param {string} [props.description] - Description of the website (overrides siteInfo.description).
530
+ * @param {string} [props.keywords] - Comma-separated keywords for the website (overrides siteInfo.keywords).
531
+ * @param {string} [props.inLanguage] - Language of the website content (overrides siteInfo.default_locale).
532
+ * @param {array} [props.sameAs] - Array of URLs representing social profiles or related sites (overrides siteInfo.sameAs).
533
+ * @param {object} [props.potentialAction] - Object defining a potentialAction for the website, such as a SearchAction (overrides siteInfo.potentialAction).
534
+ * @param {object} [props.publisher] - Object defining the publisher of the website, including name, url, and logo (overrides siteInfo).
535
+ * @param {number} [props.copyrightYear] - Year of copyright for the website (overrides siteInfo.copyrightYear).
536
+ * @param {object} [props.copyrightHolder] - Object defining the copyright holder, including name and url (overrides siteInfo).
537
+ */
538
+ WebsiteSchema.propTypes = {
539
+ name: PropTypes.string,
540
+ url: PropTypes.string,
541
+ description: PropTypes.string,
542
+ keywords: PropTypes.string,
543
+ inLanguage: PropTypes.string,
544
+ sameAs: PropTypes.arrayOf(PropTypes.string),
545
+ potentialAction: PropTypes.shape({
546
+ '@type': PropTypes.string,
547
+ target: PropTypes.shape({
548
+ '@type': PropTypes.string,
549
+ urlTemplate: PropTypes.string
550
+ }).isRequired,
551
+ 'query-input': PropTypes.string
552
+ }),
553
+ publisher: PropTypes.shape({
554
+ '@type': PropTypes.string,
555
+ name: PropTypes.string.isRequired,
556
+ url: PropTypes.string,
557
+ logo: PropTypes.shape({
558
+ '@type': PropTypes.string,
559
+ url: PropTypes.string.isRequired,
560
+ width: PropTypes.number,
561
+ height: PropTypes.number
562
+ })
563
+ }),
564
+ copyrightYear: PropTypes.number,
565
+ copyrightHolder: PropTypes.shape({
566
+ '@type': PropTypes.string,
567
+ name: PropTypes.string.isRequired,
568
+ url: PropTypes.string
569
+ }),
570
+ siteInfo: PropTypes.object
571
+ };
572
+ export function WebsiteSchema(props) {
573
+ const siteInfo = props.siteInfo;
574
+ const name = props.name || siteInfo?.name;
575
+ const url = props.url || siteInfo?.url;
576
+ if (!name || !url) {
577
+ return null;
578
+ }
579
+ const description = props.description || siteInfo?.description;
580
+ const keywords = props.keywords || siteInfo?.keywords;
581
+ const inLanguage = props.inLanguage || siteInfo?.default_locale;
582
+ const sameAs = props.sameAs || siteInfo?.sameAs;
583
+ const publisher = props.publisher || buildPublisher(siteInfo);
584
+ const potentialAction = props.potentialAction || buildPotentialAction(siteInfo?.potentialAction);
585
+ const copyrightYear = props.copyrightYear ?? siteInfo?.copyrightYear;
586
+ const copyrightHolder = props.copyrightHolder || buildCopyrightHolder(siteInfo);
587
+ const schemaData = {
588
+ '@context': 'https://schema.org',
589
+ '@type': 'WebSite',
590
+ name,
591
+ url,
592
+ ...(description && { description }),
593
+ ...(keywords && { keywords }),
594
+ ...(inLanguage && { inLanguage }),
595
+ ...(sameAs && sameAs.length ? { sameAs } : {}),
596
+ ...(publisher && { publisher }),
597
+ ...(potentialAction && { potentialAction }),
598
+ ...(copyrightYear != null && { copyrightYear }),
599
+ ...(copyrightHolder && { copyrightHolder })
600
+ };
601
+ return (_jsx(SchemaScript, { schema: schemaData }));
602
+ }
603
+ function buildPublisher(siteInfo) {
604
+ if (!siteInfo) {
605
+ return undefined;
606
+ }
607
+ if (!siteInfo.name) {
608
+ return undefined;
609
+ }
610
+ const logoUrl = siteInfo.image;
611
+ const logoWidth = parseDimension(siteInfo.image_width);
612
+ const logoHeight = parseDimension(siteInfo.image_height);
613
+ const logo = logoUrl
614
+ ? {
615
+ '@type': 'ImageObject',
616
+ url: logoUrl,
617
+ ...(logoWidth !== undefined && { width: logoWidth }),
618
+ ...(logoHeight !== undefined && { height: logoHeight })
619
+ }
620
+ : undefined;
621
+ return {
622
+ '@type': siteInfo.publisherType || 'Organization',
623
+ name: siteInfo.name,
624
+ ...(siteInfo.url && { url: siteInfo.url }),
625
+ ...(logo && { logo })
626
+ };
627
+ }
628
+ function buildCopyrightHolder(siteInfo) {
629
+ if (!siteInfo?.name) {
630
+ return undefined;
631
+ }
632
+ const holderType = siteInfo.publisherType || 'Organization';
633
+ return {
634
+ '@type': holderType,
635
+ name: siteInfo.name,
636
+ ...(siteInfo.url && { url: siteInfo.url })
637
+ };
638
+ }
639
+ function buildPotentialAction(action) {
640
+ if (!action || !action.target) {
641
+ return undefined;
642
+ }
643
+ const queryInput = action['query-input'] ?? action.queryInput;
644
+ return {
645
+ '@type': action['@type'] ?? 'SearchAction',
646
+ target: {
647
+ '@type': 'EntryPoint',
648
+ urlTemplate: action.target
649
+ },
650
+ ...(queryInput && { 'query-input': queryInput })
651
+ };
652
+ }
653
+ function parseDimension(value) {
654
+ if (typeof value === 'number') {
655
+ return value;
656
+ }
657
+ if (!value) {
658
+ return undefined;
659
+ }
660
+ const parsed = Number(value);
661
+ return Number.isNaN(parsed) ? undefined : parsed;
662
+ }