@pixelated-tech/components 3.13.10 → 3.13.12

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 (51) hide show
  1. package/dist/components/general/schema-blogposting.js +1 -1
  2. package/dist/components/general/schema-breadcrumb.js +78 -0
  3. package/dist/components/general/schema-faq.js +11 -0
  4. package/dist/components/general/schema-localbusiness.js +1 -0
  5. package/dist/components/general/schema-product.js +51 -0
  6. package/dist/components/general/schema-recipe.js +1 -1
  7. package/dist/components/general/schema-review.js +47 -0
  8. package/dist/components/general/schema-services.js +29 -5
  9. package/dist/components/general/schema-website.js +15 -2
  10. package/dist/components/general/well-known.js +0 -1
  11. package/dist/components/integrations/contentful.delivery.js +201 -0
  12. package/dist/components/integrations/wordpress.components.js +4 -2
  13. package/dist/components/integrations/wordpress.css +2 -0
  14. package/dist/components/shoppingcart/ebay.functions.js +69 -0
  15. package/dist/config/pixelated.config.json.enc +1 -1
  16. package/dist/index.js +3 -0
  17. package/dist/index.server.js +0 -6
  18. package/dist/types/components/general/schema-blogposting.d.ts +0 -1
  19. package/dist/types/components/general/schema-blogposting.d.ts.map +1 -1
  20. package/dist/types/components/general/schema-breadcrumb.d.ts +14 -0
  21. package/dist/types/components/general/schema-breadcrumb.d.ts.map +1 -0
  22. package/dist/types/components/general/schema-faq.d.ts +8 -4
  23. package/dist/types/components/general/schema-faq.d.ts.map +1 -1
  24. package/dist/types/components/general/schema-localbusiness.d.ts.map +1 -1
  25. package/dist/types/components/general/schema-product.d.ts +38 -0
  26. package/dist/types/components/general/schema-product.d.ts.map +1 -0
  27. package/dist/types/components/general/schema-recipe.d.ts +0 -1
  28. package/dist/types/components/general/schema-recipe.d.ts.map +1 -1
  29. package/dist/types/components/general/schema-review.d.ts +34 -0
  30. package/dist/types/components/general/schema-review.d.ts.map +1 -0
  31. package/dist/types/components/general/schema-services.d.ts +17 -35
  32. package/dist/types/components/general/schema-services.d.ts.map +1 -1
  33. package/dist/types/components/general/schema-website.d.ts +3 -38
  34. package/dist/types/components/general/schema-website.d.ts.map +1 -1
  35. package/dist/types/components/general/well-known.d.ts.map +1 -1
  36. package/dist/types/components/integrations/contentful.delivery.d.ts +57 -0
  37. package/dist/types/components/integrations/contentful.delivery.d.ts.map +1 -1
  38. package/dist/types/components/integrations/wordpress.components.d.ts.map +1 -1
  39. package/dist/types/components/shoppingcart/ebay.functions.d.ts +33 -0
  40. package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
  41. package/dist/types/index.d.ts +3 -0
  42. package/dist/types/index.server.d.ts +0 -6
  43. package/dist/types/stories/general/schema.stories.d.ts +40 -0
  44. package/dist/types/stories/general/schema.stories.d.ts.map +1 -1
  45. package/dist/types/tests/schema-breadcrumb.test.d.ts +2 -0
  46. package/dist/types/tests/schema-breadcrumb.test.d.ts.map +1 -0
  47. package/dist/types/tests/schema-product.test.d.ts +2 -0
  48. package/dist/types/tests/schema-product.test.d.ts.map +1 -0
  49. package/dist/types/tests/schema-review.test.d.ts +2 -0
  50. package/dist/types/tests/schema-review.test.d.ts.map +1 -0
  51. package/package.json +15 -12
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import PropTypes from 'prop-types';
3
4
  /**
@@ -15,4 +16,3 @@ export function SchemaBlogPosting(props) {
15
16
  __html: JSON.stringify(post),
16
17
  } }));
17
18
  }
18
- export default SchemaBlogPosting;
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ /**
5
+ * Build breadcrumb trail from root to current path.
6
+ * e.g., "/store/item-slug" produces ["/", "/store", "/store/item-slug"]
7
+ */
8
+ function buildPathSegments(currentPath) {
9
+ const segments = ['/'];
10
+ if (currentPath === '/')
11
+ return segments;
12
+ const parts = currentPath.split('/').filter(Boolean);
13
+ let accumulated = '';
14
+ for (const part of parts) {
15
+ accumulated += '/' + part;
16
+ segments.push(accumulated);
17
+ }
18
+ return segments;
19
+ }
20
+ /**
21
+ * Determine breadcrumb name for a path segment.
22
+ * Uses route name if exact match found, otherwise uses humanized path segment.
23
+ */
24
+ function getSegmentName(routes, path, segment) {
25
+ if (path === '/')
26
+ return 'Home';
27
+ // Only use exact route matches with valid paths to avoid duplicating parent breadcrumb names
28
+ const route = routes.find((r) => r.path && r.path === path);
29
+ if (route)
30
+ return route.name || segment;
31
+ // Fallback: humanize the path segment
32
+ return segment
33
+ .split('-')
34
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
35
+ .join(' ');
36
+ }
37
+ /**
38
+ * BreadcrumbListSchema — auto-generates a breadcrumb list as JSON-LD from routes.json data.
39
+ * Parses the current path, builds breadcrumb trail by matching path segments to routes array,
40
+ * and embeds as schema.org/BreadcrumbList for SEO rich snippets.
41
+ * Accepts flexible route objects from routes.json with any additional properties.
42
+ *
43
+ * @param {array} [props.routes] - Routes array from routes.json with name and optional path properties.
44
+ * @param {string} [props.currentPath] - Current page path (e.g. "/store/vintage-oakley"). Defaults to "/" if not provided.
45
+ * @param {string} [props.siteUrl] - Full domain URL from siteInfo.url. Defaults to https://example.com.
46
+ */
47
+ BreadcrumbListSchema.propTypes = {
48
+ /** Routes array from routes.json. Accepts routes with any properties; only uses name and path. */
49
+ routes: PropTypes.arrayOf(PropTypes.object).isRequired,
50
+ /** Current page path to generate breadcrumbs for (e.g. "/store/item-slug"). Defaults to "/". */
51
+ currentPath: PropTypes.string,
52
+ /** Site domain URL for constructing full breadcrumb URLs. Defaults to https://example.com. */
53
+ siteUrl: PropTypes.string,
54
+ };
55
+ export function BreadcrumbListSchema({ routes, currentPath = '/', siteUrl = 'https://example.com', }) {
56
+ // Type-safe conversion: routes prop is now flexible (accepts any object)
57
+ // Filter to ensure only valid Route objects with 'name' property
58
+ const validRoutes = (Array.isArray(routes)
59
+ ? routes.filter((r) => !!(r && typeof r === 'object' && 'name' in r))
60
+ : []);
61
+ const pathSegments = buildPathSegments(currentPath || '/');
62
+ const finalSiteUrl = siteUrl || 'https://example.com';
63
+ const itemListElement = pathSegments.map((path, index) => {
64
+ const segment = path.split('/').filter(Boolean).pop() || 'Home';
65
+ return {
66
+ '@type': 'ListItem',
67
+ 'position': index + 1,
68
+ 'name': getSegmentName(validRoutes, path, segment),
69
+ 'item': `${finalSiteUrl.replace(/\/$/, '')}${path}`,
70
+ };
71
+ });
72
+ const jsonLD = {
73
+ '@context': 'https://schema.org',
74
+ '@type': 'BreadcrumbList',
75
+ 'itemListElement': itemListElement,
76
+ };
77
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(jsonLD) } }));
78
+ }
@@ -1,4 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
2
4
  // normalizeFaqs turns a JSON-LD FAQPage payload into a form where each
3
5
  // question has a single `acceptedAnswer.text` string. Some of our data
4
6
  // sources (WordPress, CMS exports) allow multiple answer fragments; we
@@ -19,6 +21,15 @@ function normalizeFaqs(data) {
19
21
  }
20
22
  return faqs;
21
23
  }
24
+ /**
25
+ * SchemaFAQ — Inject a JSON-LD <script> tag containing an FAQPage schema object.
26
+ *
27
+ * @param {object} [props.faqsData] - Structured JSON-LD object representing an FAQ page (FAQPage schema).
28
+ */
29
+ SchemaFAQ.propTypes = {
30
+ /** Structured FAQPage JSON-LD object */
31
+ faqsData: PropTypes.object.isRequired,
32
+ };
22
33
  export function SchemaFAQ({ faqsData }) {
23
34
  const normalized = normalizeFaqs(faqsData);
24
35
  return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: {
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import PropTypes from "prop-types";
3
4
  /**
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ /**
5
+ * ProductSchema — embeds a product/offer as JSON-LD for SEO (schema.org/Product).
6
+ *
7
+ * @param {shape} [props.product] - Product object conforming to schema.org/Product; will be serialized as JSON-LD.
8
+ * @param {string} [props.product.name] - The product name.
9
+ * @param {string} [props.product.description] - Product description.
10
+ * @param {shape} [props.product.brand] - Brand information (name and @type).
11
+ * @param {shape} [props.product.offers] - Offer information including price, currency, URL, and availability.
12
+ */
13
+ ProductSchema.propTypes = {
14
+ /** Product information object to be serialized as JSON-LD. */
15
+ product: PropTypes.shape({
16
+ '@context': PropTypes.string.isRequired,
17
+ '@type': PropTypes.string.isRequired,
18
+ name: PropTypes.string.isRequired,
19
+ description: PropTypes.string,
20
+ image: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
21
+ brand: PropTypes.shape({
22
+ '@type': PropTypes.string.isRequired,
23
+ name: PropTypes.string.isRequired,
24
+ }),
25
+ offers: PropTypes.oneOfType([
26
+ PropTypes.shape({
27
+ '@type': PropTypes.string.isRequired,
28
+ url: PropTypes.string,
29
+ priceCurrency: PropTypes.string,
30
+ price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
31
+ availability: PropTypes.string,
32
+ }),
33
+ PropTypes.arrayOf(PropTypes.shape({
34
+ '@type': PropTypes.string.isRequired,
35
+ url: PropTypes.string,
36
+ priceCurrency: PropTypes.string,
37
+ price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
38
+ availability: PropTypes.string,
39
+ }))
40
+ ]),
41
+ aggregateRating: PropTypes.shape({
42
+ '@type': PropTypes.string,
43
+ ratingValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
44
+ reviewCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
45
+ }),
46
+ }).isRequired,
47
+ };
48
+ export function ProductSchema(props) {
49
+ const { product } = props;
50
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(product) } }));
51
+ }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import PropTypes from 'prop-types';
3
4
  /**
@@ -55,4 +56,3 @@ export function RecipeSchema(props) {
55
56
  const { recipe } = props;
56
57
  return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(recipe) } }));
57
58
  }
58
- export default RecipeSchema;
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ /**
5
+ * ReviewSchema — embeds a review as JSON-LD for SEO (schema.org/Review).
6
+ *
7
+ * @param {shape} [props.review] - Review object conforming to schema.org/Review; will be serialized as JSON-LD.
8
+ * @param {string} [props.review.name] - The headline or title of the review.
9
+ * @param {string} [props.review.reviewBody] - The body of the review content.
10
+ * @param {string} [props.review.datePublished] - ISO date the review was published.
11
+ * @param {shape} [props.review.author] - Author information (name and @type).
12
+ * @param {shape} [props.review.itemReviewed] - The item being reviewed (product, service, etc.).
13
+ * @param {shape} [props.review.reviewRating] - Rating information including ratingValue, bestRating, worstRating.
14
+ * @param {shape} [props.review.publisher] - Organization publishing the review.
15
+ */
16
+ ReviewSchema.propTypes = {
17
+ /** Review information object to be serialized as JSON-LD. */
18
+ review: PropTypes.shape({
19
+ '@context': PropTypes.string.isRequired,
20
+ '@type': PropTypes.string.isRequired,
21
+ name: PropTypes.string.isRequired,
22
+ reviewBody: PropTypes.string,
23
+ datePublished: PropTypes.string,
24
+ author: PropTypes.shape({
25
+ '@type': PropTypes.string.isRequired,
26
+ name: PropTypes.string.isRequired,
27
+ }),
28
+ itemReviewed: PropTypes.shape({
29
+ '@type': PropTypes.string.isRequired,
30
+ name: PropTypes.string,
31
+ }),
32
+ reviewRating: PropTypes.shape({
33
+ '@type': PropTypes.string.isRequired,
34
+ ratingValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
35
+ bestRating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
36
+ worstRating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
37
+ }),
38
+ publisher: PropTypes.shape({
39
+ '@type': PropTypes.string.isRequired,
40
+ name: PropTypes.string,
41
+ }),
42
+ }).isRequired,
43
+ };
44
+ export function ReviewSchema(props) {
45
+ const { review } = props;
46
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(review) } }));
47
+ }
@@ -1,7 +1,33 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import PropTypes from 'prop-types';
3
- const servicesSchemaPropTypes = {
4
- siteInfo: PropTypes.object,
4
+ /**
5
+ * Services Schema Component
6
+ * Generates JSON-LD structured data for services
7
+ * https://schema.org/Service
8
+ */
9
+ /**
10
+ * ServicesSchema — Inject JSON-LD <script> tags for each service offered by the business, using schema.org/Service format.
11
+ *
12
+ * @param {object} [props.siteInfo] - Optional site information object containing business details and services array.
13
+ * @param {object} [props.provider] - Optional provider information object to override siteInfo for the service provider.
14
+ * @param {array} [props.services] - Optional array of service objects to override siteInfo.services.
15
+ */
16
+ ServicesSchema.propTypes = {
17
+ siteInfo: PropTypes.shape({
18
+ name: PropTypes.string,
19
+ url: PropTypes.string,
20
+ image: PropTypes.string,
21
+ telephone: PropTypes.string,
22
+ email: PropTypes.string,
23
+ services: PropTypes.arrayOf(PropTypes.shape({
24
+ name: PropTypes.string.isRequired,
25
+ description: PropTypes.string.isRequired,
26
+ url: PropTypes.string,
27
+ image: PropTypes.string,
28
+ areaServed: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
29
+ }))
30
+ }),
5
31
  provider: PropTypes.shape({
6
32
  name: PropTypes.string.isRequired,
7
33
  url: PropTypes.string.isRequired,
@@ -30,7 +56,7 @@ export function ServicesSchema(props) {
30
56
  if (!services.length || !provider.name) {
31
57
  return null;
32
58
  }
33
- const serviceObjects = services.map((service) => ({
59
+ const serviceObjects = services.filter((service) => service != null).map((service) => ({
34
60
  '@type': 'Service',
35
61
  name: service.name,
36
62
  description: service.description,
@@ -51,5 +77,3 @@ export function ServicesSchema(props) {
51
77
  ...service
52
78
  }) } }, idx))) }));
53
79
  }
54
- ServicesSchema.propTypes = servicesSchemaPropTypes;
55
- export default ServicesSchema;
@@ -1,12 +1,25 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import PropTypes from "prop-types";
3
4
  /**
4
5
  * Website Schema Component
5
6
  * Generates JSON-LD structured data for websites
6
7
  * https://schema.org/WebSite
8
+ */
9
+ /**
10
+ * WebsiteSchema — Inject a JSON-LD <script> tag containing a WebSite schema object, using provided props or siteInfo from config.
7
11
  *
8
- * This component uses siteInfo passed as props to generate schema data.
9
- * It does not use client-side hooks and can be rendered on the server.
12
+ * @param {object} [props.siteInfo] - Optional site information object containing business details to populate the schema.
13
+ * @param {string} [props.name] - Name of the website (overrides siteInfo.name).
14
+ * @param {string} [props.url] - URL of the website (overrides siteInfo.url).
15
+ * @param {string} [props.description] - Description of the website (overrides siteInfo.description).
16
+ * @param {string} [props.keywords] - Comma-separated keywords for the website (overrides siteInfo.keywords).
17
+ * @param {string} [props.inLanguage] - Language of the website content (overrides siteInfo.default_locale).
18
+ * @param {array} [props.sameAs] - Array of URLs representing social profiles or related sites (overrides siteInfo.sameAs).
19
+ * @param {object} [props.potentialAction] - Object defining a potentialAction for the website, such as a SearchAction (overrides siteInfo.potentialAction).
20
+ * @param {object} [props.publisher] - Object defining the publisher of the website, including name, url, and logo (overrides siteInfo).
21
+ * @param {number} [props.copyrightYear] - Year of copyright for the website (overrides siteInfo.copyrightYear).
22
+ * @param {object} [props.copyrightHolder] - Object defining the copyright holder, including name and url (overrides siteInfo).
10
23
  */
11
24
  WebsiteSchema.propTypes = {
12
25
  name: PropTypes.string,
@@ -3,7 +3,6 @@ import { readFile } from 'fs/promises';
3
3
  import crypto from 'crypto';
4
4
  import { NextResponse } from 'next/server';
5
5
  import { flattenRoutes } from './sitemap';
6
- /* ===== Shared helpers for .well-known files ===== */
7
6
  /**
8
7
  * Read JSON from disk safely — returns null on error. Exported for testing.
9
8
  */
@@ -332,3 +332,204 @@ export async function getContentfulDiscountCodes(props) {
332
332
  return [];
333
333
  }
334
334
  }
335
+ /* ========== GET CONTENTFUL REVIEWS AS SCHEMA ========== */
336
+ /**
337
+ * getContentfulReviewsSchema — Retrieve review entries from Contentful and convert them to schema.org/Review JSON-LD format.
338
+ *
339
+ * @param {shape} [props.apiProps] - Contentful API configuration object (base_url, space_id, environment, access tokens).
340
+ * @param {string} [props.itemName] - Name of the item/product being reviewed (e.g., 'Epoxy Flooring Service').
341
+ * @param {string} [props.itemType] - Schema type of the item being reviewed (default: 'Product').
342
+ * @param {string} [props.publisherName] - Name of the organization publishing the reviews.
343
+ */
344
+ getContentfulReviewsSchema.propTypes = {
345
+ /** Contentful API configuration */
346
+ apiProps: PropTypes.shape({
347
+ proxyURL: PropTypes.string,
348
+ base_url: PropTypes.string.isRequired,
349
+ space_id: PropTypes.string.isRequired,
350
+ environment: PropTypes.string.isRequired,
351
+ delivery_access_token: PropTypes.string.isRequired,
352
+ }).isRequired,
353
+ /** Name of the item/product being reviewed */
354
+ itemName: PropTypes.string.isRequired,
355
+ /** Schema type of the item being reviewed */
356
+ itemType: PropTypes.string,
357
+ /** Name of the organization publishing the reviews */
358
+ publisherName: PropTypes.string,
359
+ };
360
+ export async function getContentfulReviewsSchema(props) {
361
+ const contentType = "reviews";
362
+ const itemName = props.itemName;
363
+ const itemType = props.itemType || "Product";
364
+ const publisherName = props.publisherName;
365
+ try {
366
+ if (debug)
367
+ console.log("Fetching Reviews and converting to schema");
368
+ const response = await getContentfulEntriesByType({
369
+ apiProps: props.apiProps,
370
+ contentType: contentType
371
+ });
372
+ if (!response || !response.items) {
373
+ console.error("No reviews found in Contentful");
374
+ return [];
375
+ }
376
+ const reviewSchemas = response.items.map((item) => {
377
+ // Extract reviewer name from "fields.reviewer" which comes as " - Name"
378
+ const reviewerNameRaw = item.fields.reviewer || "Anonymous";
379
+ const reviewerName = reviewerNameRaw.replace(/^\s*-\s*/, "").trim();
380
+ // Convert Contentful date to ISO format (remove quotes if present)
381
+ const publishDate = new Date(item.sys.createdAt).toISOString().split("T")[0];
382
+ // Create Review schema object
383
+ const reviewSchema = {
384
+ "@context": "https://schema.org/",
385
+ "@type": "Review",
386
+ "name": "", // Will be extracted or generated from review body
387
+ "reviewBody": item.fields.description?.replace(/^"|"$/g, "").trim() || "",
388
+ "datePublished": publishDate,
389
+ "author": {
390
+ "@type": "Person",
391
+ "name": reviewerName
392
+ },
393
+ "itemReviewed": {
394
+ "@type": itemType,
395
+ "name": itemName
396
+ },
397
+ "reviewRating": {
398
+ "@type": "Rating",
399
+ "ratingValue": "5",
400
+ "bestRating": "5",
401
+ "worstRating": "1"
402
+ }
403
+ };
404
+ // Generate name from first sentence of review or use generic title
405
+ if (reviewSchema.reviewBody) {
406
+ const sentences = reviewSchema.reviewBody.split(/[.!?]/);
407
+ reviewSchema.name = sentences[0].trim().substring(0, 100) || "Review";
408
+ }
409
+ // Add publisher if provided
410
+ if (publisherName) {
411
+ reviewSchema.publisher = {
412
+ "@type": "Organization",
413
+ "name": publisherName
414
+ };
415
+ }
416
+ return reviewSchema;
417
+ });
418
+ if (debug)
419
+ console.log("Review Schemas: ", reviewSchemas);
420
+ return reviewSchemas;
421
+ }
422
+ catch (error) {
423
+ console.error('Error fetching reviews:', error);
424
+ return [];
425
+ }
426
+ }
427
+ /* ========== GET CONTENTFUL PRODUCT AS SCHEMA ========== */
428
+ /**
429
+ * getContentfulProductSchema — Retrieve a product entry from Contentful and convert it to schema.org/Product JSON-LD format.
430
+ *
431
+ * @param {shape} [props.apiProps] - Contentful API configuration object (base_url, space_id, environment, access tokens).
432
+ * @param {string} [props.productId] - The product ID field value to search for (e.g., 'PIX-000779').
433
+ * @param {string} [props.siteUrl] - Optional base site URL for the offer URL.
434
+ * @param {function} [props.getAssetUrl] - Optional function to transform asset URLs.
435
+ */
436
+ getContentfulProductSchema.propTypes = {
437
+ /** Contentful API configuration */
438
+ apiProps: PropTypes.shape({
439
+ proxyURL: PropTypes.string,
440
+ base_url: PropTypes.string.isRequired,
441
+ space_id: PropTypes.string.isRequired,
442
+ environment: PropTypes.string.isRequired,
443
+ delivery_access_token: PropTypes.string.isRequired,
444
+ }).isRequired,
445
+ /** Product ID to search for */
446
+ productId: PropTypes.string.isRequired,
447
+ /** Optional site URL for offer */
448
+ siteUrl: PropTypes.string,
449
+ /** Optional function to transform asset URLs */
450
+ getAssetUrl: PropTypes.func,
451
+ };
452
+ export async function getContentfulProductSchema(props) {
453
+ const contentType = "item";
454
+ const productId = props.productId;
455
+ const siteUrl = props.siteUrl || '';
456
+ const getAssetUrl = props.getAssetUrl;
457
+ try {
458
+ if (debug)
459
+ console.log("Fetching Product and converting to schema");
460
+ const response = await getContentfulEntriesByType({
461
+ apiProps: props.apiProps,
462
+ contentType: contentType
463
+ });
464
+ if (!response || !response.items) {
465
+ console.error("No products found in Contentful");
466
+ return null;
467
+ }
468
+ // Use existing helper function to find product by ID field
469
+ const product = await getContentfulEntryByField({
470
+ cards: response,
471
+ searchField: 'id',
472
+ searchVal: productId
473
+ });
474
+ if (!product) {
475
+ console.error(`Product with ID ${productId} not found`);
476
+ return null;
477
+ }
478
+ const fields = product.fields;
479
+ // Create Product schema object
480
+ const productSchema = {
481
+ '@context': 'https://schema.org/',
482
+ '@type': 'Product',
483
+ name: fields.title || '',
484
+ description: fields.description || '',
485
+ image: [],
486
+ brand: {
487
+ '@type': 'Brand',
488
+ name: fields.brand || 'Pixelated'
489
+ },
490
+ offers: {
491
+ '@type': 'Offer',
492
+ url: siteUrl || '',
493
+ priceCurrency: 'USD',
494
+ price: String(fields.price || '0'),
495
+ availability: 'https://schema.org/InStock'
496
+ }
497
+ };
498
+ // Handle images
499
+ if (Array.isArray(fields.images) && fields.images.length > 0) {
500
+ // Note: Contentful images are links to assets
501
+ // In a real implementation, you'd need to resolve these asset URLs
502
+ // For now, we store them as asset IDs that can be looked up later
503
+ productSchema.image = fields.images.map((img) => {
504
+ if (img.sys?.id) {
505
+ return getAssetUrl ? getAssetUrl(img.sys.id) : `https://assets.ctfassets.net/${img.sys.id}`;
506
+ }
507
+ return '';
508
+ }).filter((url) => url);
509
+ }
510
+ // Add additional product attributes if available
511
+ if (fields.brand) {
512
+ productSchema.brand.name = fields.brand;
513
+ }
514
+ if (fields.model) {
515
+ productSchema.model = fields.model;
516
+ }
517
+ if (fields.color) {
518
+ productSchema.color = fields.color;
519
+ }
520
+ // Add rating based on availability/quantity
521
+ if (fields.quantity !== undefined) {
522
+ const availability = fields.quantity > 0
523
+ ? 'https://schema.org/InStock'
524
+ : 'https://schema.org/OutOfStock';
525
+ productSchema.offers.availability = availability;
526
+ }
527
+ if (debug)
528
+ console.log("Contentful Product Schema: ", productSchema);
529
+ return productSchema;
530
+ }
531
+ catch (error) {
532
+ console.error('Error fetching product:', error);
533
+ return null;
534
+ }
535
+ }
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { usePixelatedConfig } from "../config/config.client";
@@ -9,6 +9,8 @@ import { getWordPressItems, getWordPressLastModified } from './wordpress.functio
9
9
  import { Loading, ToggleLoading } from '../general/loading';
10
10
  import { CacheManager } from "../general/cache-manager";
11
11
  import "./wordpress.css";
12
+ import { SchemaBlogPosting } from '../general/schema-blogposting';
13
+ import { mapWordPressToBlogPosting } from '../general/schema-blogposting.functions';
12
14
  // https://microformats.org/wiki/h-entry
13
15
  function decodeString(str) {
14
16
  const textarea = document.createElement('textarea');
@@ -103,7 +105,7 @@ export function BlogPostList(props) {
103
105
  ToggleLoading({ show: false });
104
106
  })();
105
107
  }, [site, baseURL, count, cachedPosts]);
106
- return (_jsxs(_Fragment, { children: [_jsx(Loading, {}), posts.map((post) => (_jsx(PageGridItem, { children: _jsx(BlogPostSummary, { ID: post.ID, title: post.title, date: post.date, excerpt: post.excerpt, URL: post.URL, categories: post.categories, featured_image: post.featured_image, showCategories: showCategories }) }, post.ID)))] }));
108
+ return (_jsxs(_Fragment, { children: [_jsx(Loading, {}), posts.map((post) => (_jsxs(PageGridItem, { children: [_jsx(SchemaBlogPosting, { post: mapWordPressToBlogPosting(post, false) }, post.ID), _jsx(BlogPostSummary, { ID: post.ID, title: post.title, date: post.date, excerpt: post.excerpt, URL: post.URL, categories: post.categories, featured_image: post.featured_image, showCategories: showCategories })] }, post.ID)))] }));
107
109
  }
108
110
  /**
109
111
  * BlogPostSummary — Render a compact summary card for a single WordPress post.
@@ -46,6 +46,8 @@
46
46
 
47
47
  .article-featured-image img {
48
48
  /* border-radius: 20px; */
49
+ min-width: 100%;
50
+ min-height: 100%;
49
51
  width: 100%;
50
52
  height: 100%;
51
53
  max-height: 150px;
@@ -392,3 +392,72 @@ export function getEbayItemsSearch(props) {
392
392
  };
393
393
  return fetchData(props.token);
394
394
  }
395
+ /* ========== PRODUCT SCHEMA ========== */
396
+ /**
397
+ * getEbayProductSchema — Convert an eBay item into schema.org/Product JSON-LD format.
398
+ *
399
+ * @param {object} [props.item] - eBay item object from the Browse API.
400
+ * @param {string} [props.brandName] - Optional brand name to include in the schema.
401
+ * @param {string} [props.siteUrl] - Optional site URL for the offer URL.
402
+ */
403
+ getEbayProductSchema.propTypes = {
404
+ /** eBay item object */
405
+ item: PropTypes.any.isRequired,
406
+ /** Optional brand name */
407
+ brandName: PropTypes.string,
408
+ /** Optional site URL for offer */
409
+ siteUrl: PropTypes.string,
410
+ };
411
+ export function getEbayProductSchema(props) {
412
+ const item = props.item;
413
+ const brandName = props.brandName || 'eBay';
414
+ const siteUrl = props.siteUrl || item.itemWebUrl || '';
415
+ if (!item || !item.title || !item.price) {
416
+ return null;
417
+ }
418
+ // Get the primary image
419
+ const primaryImage = item.image?.imageUrl || item.thumbnailImages?.[0]?.imageUrl || '';
420
+ // Collect all images
421
+ const allImages = [];
422
+ if (primaryImage)
423
+ allImages.push(primaryImage);
424
+ if (Array.isArray(item.additionalImages)) {
425
+ item.additionalImages.forEach((img) => {
426
+ if (img.imageUrl)
427
+ allImages.push(img.imageUrl);
428
+ });
429
+ }
430
+ const productSchema = {
431
+ '@context': 'https://schema.org/',
432
+ '@type': 'Product',
433
+ name: item.title,
434
+ description: item.title,
435
+ image: allImages.length > 1 ? allImages : (allImages[0] || ''),
436
+ brand: {
437
+ '@type': 'Brand',
438
+ name: brandName
439
+ },
440
+ offers: {
441
+ '@type': 'Offer',
442
+ url: siteUrl,
443
+ priceCurrency: item.price?.currency || 'USD',
444
+ price: String(item.price?.value || '0'),
445
+ availability: 'https://schema.org/InStock',
446
+ seller: {
447
+ '@type': 'Organization',
448
+ name: item.seller?.username || 'eBay Seller'
449
+ }
450
+ }
451
+ };
452
+ // Add seller rating if available
453
+ if (item.seller?.feedbackPercentage || item.seller?.feedbackScore) {
454
+ productSchema.aggregateRating = {
455
+ '@type': 'AggregateRating',
456
+ ratingValue: item.seller.feedbackPercentage ? String(item.seller.feedbackPercentage) : '0',
457
+ reviewCount: item.seller.feedbackScore ? String(item.seller.feedbackScore) : '0'
458
+ };
459
+ }
460
+ if (debug)
461
+ console.log("eBay Product Schema:", productSchema);
462
+ return productSchema;
463
+ }