@scayle/storefront-product-detail 0.1.0 → 0.1.1

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/dist/module.mjs CHANGED
@@ -16,6 +16,7 @@ const module = defineNuxtModule({
16
16
  nuxt.options.alias["#storefront-product-detail"] = resolve("./runtime");
17
17
  nuxt.options.build.transpile.push("#storefront-product-detail/runtime");
18
18
  if (options.autoImports) {
19
+ addImportsDir(resolve("./runtime/composables"));
19
20
  addImportsDir(resolve("./runtime/utils"));
20
21
  }
21
22
  }
@@ -0,0 +1,42 @@
1
+ import { type ComputedRef } from 'vue';
2
+ import type { BreadcrumbItem, Variant } from '@scayle/storefront-nuxt';
3
+ import type { WithContext, Product, BreadcrumbList } from 'schema-dts';
4
+ type CanonicalLink = Record<'rel' | 'key' | 'href', string>;
5
+ interface UrlParams {
6
+ baseUrl: string;
7
+ fullPath: string;
8
+ }
9
+ interface ProductInfo {
10
+ name: string;
11
+ brand: string;
12
+ productDescription: string;
13
+ variants: Variant[];
14
+ }
15
+ interface UseProductSeoDataReturn {
16
+ robots: string;
17
+ canonicalLink: ComputedRef<CanonicalLink[]>;
18
+ title: string;
19
+ productJsonLd: ComputedRef<WithContext<Product>>;
20
+ productBreadcrumbJsonLd: ComputedRef<WithContext<BreadcrumbList>>;
21
+ }
22
+ /**
23
+ * Composable function to generate SEO-related data for a product page.
24
+ *
25
+ * @param {BreadcrumbItem[]} breadcrumbs - Array of breadcrumb items representing the navigation path to the product.
26
+ * @param {UrlParams} urlParams - Object containing baseUrl and fullPath to build and sanitize canonicalUrl.
27
+ * @param {string[]} images - Array of image URLs for the product.
28
+ * @param {ProductInfo} productInfo - Object containing product information including name, brand, description, and variants.
29
+ * @property {string} name - The name of the product.
30
+ * @property {string} brand - The brand of the product.
31
+ * @property {string} productDescription - A description of the product.
32
+ * @property {Variant[]} variants - Array of product variants.
33
+ *
34
+ * @returns {UseProductSeoDataReturn} An object containing SEO data for the product page with following properties:
35
+ * @property {string} robots - Robots meta tag value, typically "index, follow".
36
+ * @property {ComputedRef<CanonicalLink[]>} canonicalLink - Computed ref for canonical link tag metadata.
37
+ * @property {string} title - The title of the product.
38
+ * @property {ComputedRef<WithContext<Product>>} productJsonLd - Computed ref containing JSON-LD structured data for the product.
39
+ * @property {ComputedRef<WithContext<BreadcrumbList>>} productBreadcrumbJsonLd - Computed ref containing JSON-LD structured data for product breadcrumbs.
40
+ */
41
+ export declare function useProductSeoData(breadcrumbs: BreadcrumbItem[], urlParams: UrlParams, images: string[], productInfo: ProductInfo): UseProductSeoDataReturn;
42
+ export {};
@@ -0,0 +1,36 @@
1
+ import { computed } from "vue";
2
+ import {
3
+ generateCategoryBreadcrumbSchema,
4
+ sanitizeCanonicalURL
5
+ } from "@scayle/storefront-nuxt";
6
+ import { generateProductSchema } from "~/src/runtime/utils/seo";
7
+ export function useProductSeoData(breadcrumbs, urlParams, images, productInfo) {
8
+ const canonicalUrl = computed(() => {
9
+ const url = `${urlParams.baseUrl}${urlParams.fullPath}`;
10
+ return sanitizeCanonicalURL(url, []);
11
+ });
12
+ const canonicalLink = computed(() => {
13
+ return [{ rel: "canonical", key: "canonical", href: canonicalUrl.value }];
14
+ });
15
+ const robots = "index, follow";
16
+ const productJsonLd = computed(() => {
17
+ return generateProductSchema({
18
+ productName: productInfo.name || "",
19
+ brandName: productInfo.brand || "",
20
+ url: canonicalUrl.value,
21
+ variants: productInfo.variants || [],
22
+ images,
23
+ description: productInfo.productDescription
24
+ });
25
+ });
26
+ const productBreadcrumbJsonLd = computed(() => {
27
+ return generateCategoryBreadcrumbSchema(breadcrumbs);
28
+ });
29
+ return {
30
+ robots,
31
+ canonicalLink,
32
+ title: productInfo.name,
33
+ productJsonLd,
34
+ productBreadcrumbJsonLd
35
+ };
36
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { useProductSeoData } from "~/src/runtime";
3
+ import { variantFactory } from "@scayle/storefront-nuxt/dist/test/factories";
4
+ describe("useProductSeoData", () => {
5
+ it("should return product seo data from product", () => {
6
+ const {
7
+ title,
8
+ productBreadcrumbJsonLd,
9
+ productJsonLd,
10
+ robots,
11
+ canonicalLink
12
+ } = useProductSeoData(
13
+ [{ value: "value", to: "/" }],
14
+ { baseUrl: "baseUrl", fullPath: "fullPath" },
15
+ [],
16
+ {
17
+ name: "name",
18
+ variants: [variantFactory.build({ id: 1 })],
19
+ brand: "brand",
20
+ productDescription: "description"
21
+ }
22
+ );
23
+ expect(title).toBe("name");
24
+ expect(productBreadcrumbJsonLd.value).toStrictEqual({
25
+ "@context": "https://schema.org",
26
+ "@type": "BreadcrumbList",
27
+ "itemListElement": [
28
+ {
29
+ "@type": "ListItem",
30
+ "item": "/",
31
+ "name": "value",
32
+ "position": 1
33
+ }
34
+ ]
35
+ });
36
+ expect(productJsonLd.value).toStrictEqual({
37
+ "@context": "https://schema.org",
38
+ "@type": "Product",
39
+ "brand": {
40
+ "@type": "Brand",
41
+ "name": "brand"
42
+ },
43
+ "description": "description",
44
+ "image": [],
45
+ "name": "name",
46
+ "offers": [
47
+ {
48
+ "@type": "Offer",
49
+ "availability": "https://schema.org/InStock",
50
+ "itemCondition": "https://schema.org/NewCondition",
51
+ "mpn": void 0,
52
+ "price": 1,
53
+ "priceCurrency": "USD",
54
+ "sku": "1"
55
+ }
56
+ ],
57
+ "url": "baseUrlfullPath"
58
+ });
59
+ expect(robots).toBe("index, follow");
60
+ expect(canonicalLink.value).toStrictEqual([
61
+ {
62
+ "href": "baseUrlfullPath",
63
+ "key": "canonical",
64
+ "rel": "canonical"
65
+ }
66
+ ]);
67
+ });
68
+ });
@@ -1 +1,3 @@
1
+ export * from './composables/useProductSeoData.js';
1
2
  export * from './utils/product.js';
3
+ export * from './utils/attribute.js';
@@ -1 +1,3 @@
1
+ export * from "./composables/useProductSeoData.js";
1
2
  export * from "./utils/product.js";
3
+ export * from "./utils/attribute.js";
@@ -0,0 +1,2 @@
1
+ import type { Attributes } from '@scayle/storefront-nuxt';
2
+ export declare const getFilteredAttributeGroups: (attributes: Attributes, filterTypes: string[]) => Map<string, string[]>;
@@ -0,0 +1,19 @@
1
+ const getFormattedLabel = (attributeGroup) => {
2
+ if (!attributeGroup) {
3
+ return [];
4
+ }
5
+ const attributeGroupValues = Array.isArray(attributeGroup.values) ? attributeGroup.values : [attributeGroup.values];
6
+ return attributeGroupValues.map(
7
+ (value) => attributeGroup.type === "extras" ? value.label : `${attributeGroup.label}: ${value.label}`
8
+ );
9
+ };
10
+ export const getFilteredAttributeGroups = (attributes, filterTypes) => {
11
+ return Object.values(attributes).filter(
12
+ (attribute) => attribute?.type && filterTypes.includes(attribute.type)
13
+ ).reduce((map, attributeValue) => {
14
+ const key = attributeValue?.type || "";
15
+ const value = getFormattedLabel(attributeValue);
16
+ map.set(key, (map.get(key) || []).concat(value));
17
+ return map;
18
+ }, /* @__PURE__ */ new Map());
19
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { it, describe, expect } from "vitest";
2
+ import { attributeGroupFactory } from "@scayle/storefront-nuxt/dist/test/factories";
3
+ import { getFilteredAttributeGroups } from "./attribute.js";
4
+ describe("attribute", () => {
5
+ describe("getFilteredAttributeGroups", () => {
6
+ it("should return correct filtered and formatted attributes", () => {
7
+ const attributes = {
8
+ design: attributeGroupFactory.build({
9
+ type: "design",
10
+ label: "Jackenart",
11
+ values: { id: 2534, label: "Bomberjacke", value: "bomberjacke" }
12
+ }),
13
+ extras: attributeGroupFactory.build({
14
+ label: "Extras",
15
+ type: "extras",
16
+ multiSelect: true,
17
+ values: [
18
+ { id: 2427, label: "Weicher Griff", value: "weicher_griff" },
19
+ { id: 2435, label: "Ton-in-Ton-N\xE4hte", value: "tonintonnhte" },
20
+ { id: 2438, label: "Label-Stickerei", value: "labelstickerei" }
21
+ ]
22
+ })
23
+ };
24
+ const formattedAttributes = getFilteredAttributeGroups(
25
+ attributes,
26
+ ["design", "extras"]
27
+ );
28
+ expect(formattedAttributes).toHaveLength(2);
29
+ expect(formattedAttributes.get("design")).toStrictEqual([
30
+ "Jackenart: Bomberjacke"
31
+ ]);
32
+ expect(formattedAttributes.get("extras")).toStrictEqual([
33
+ "Weicher Griff",
34
+ "Ton-in-Ton-N\xE4hte",
35
+ "Label-Stickerei"
36
+ ]);
37
+ });
38
+ });
39
+ });
@@ -1,6 +1,6 @@
1
1
  import { it, describe } from "vitest";
2
2
  import { getCombineWithProductIds } from "~/src/runtime/utils/product";
3
- import { advancedAttributeFactory } from "@scayle/storefront-nuxt";
3
+ import { advancedAttributeFactory } from "@scayle/storefront-nuxt/dist/test/factories";
4
4
  describe("getCombineWithProductIds", () => {
5
5
  it("should return productIds from data", ({ expect }) => {
6
6
  const combineWithAttribute = advancedAttributeFactory.build({
@@ -0,0 +1,10 @@
1
+ import type { Variant } from '@scayle/storefront-nuxt';
2
+ import type { Product, WithContext } from 'schema-dts';
3
+ export declare const generateProductSchema: ({ productName, brandName, variants, url, images, description, }: {
4
+ productName: string;
5
+ brandName: string;
6
+ variants: Variant[];
7
+ url: string;
8
+ description: string;
9
+ images?: string[];
10
+ }) => WithContext<Product>;
@@ -0,0 +1,38 @@
1
+ import { isInStock } from "@scayle/storefront-nuxt";
2
+ const generateSchemaProductOffers = ({
3
+ variants
4
+ }) => {
5
+ return variants.map((variant) => ({
6
+ "@type": "Offer",
7
+ mpn: variant.referenceKey,
8
+ sku: variant.id.toString(),
9
+ price: variant.price.withTax / 100,
10
+ priceCurrency: variant.price.currencyCode,
11
+ availability: isInStock(variant) ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
12
+ itemCondition: "https://schema.org/NewCondition"
13
+ }));
14
+ };
15
+ export const generateProductSchema = ({
16
+ productName,
17
+ brandName,
18
+ variants,
19
+ url,
20
+ images = [],
21
+ description
22
+ }) => {
23
+ const offers = generateSchemaProductOffers({ variants });
24
+ const brand = {
25
+ "@type": "Brand",
26
+ name: brandName
27
+ };
28
+ return {
29
+ "@context": "https://schema.org",
30
+ "@type": "Product",
31
+ url,
32
+ name: productName,
33
+ image: images,
34
+ description,
35
+ brand,
36
+ offers
37
+ };
38
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateProductSchema } from "~/src/runtime/utils/seo";
3
+ import { variantFactory } from "@scayle/storefront-nuxt/dist/test/factories";
4
+ describe("generateProductSchema", () => {
5
+ it("should return productSchema from product", () => {
6
+ const productSchema = generateProductSchema({
7
+ productName: "productName",
8
+ brandName: "brandName",
9
+ variants: [variantFactory.build({ id: 1 })],
10
+ url: "url",
11
+ images: [],
12
+ description: "description"
13
+ });
14
+ expect(productSchema).toStrictEqual({
15
+ "@context": "https://schema.org",
16
+ "@type": "Product",
17
+ "brand": {
18
+ "@type": "Brand",
19
+ "name": "brandName"
20
+ },
21
+ "description": "description",
22
+ "image": [],
23
+ "name": "productName",
24
+ "offers": [
25
+ {
26
+ "@type": "Offer",
27
+ "availability": "https://schema.org/InStock",
28
+ "itemCondition": "https://schema.org/NewCondition",
29
+ "mpn": void 0,
30
+ "price": 1,
31
+ "priceCurrency": "USD",
32
+ "sku": "1"
33
+ }
34
+ ],
35
+ "url": "url"
36
+ });
37
+ });
38
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Collection of essential composables and utilities to work with product detail",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -52,13 +52,13 @@
52
52
  "@nuxt/schema": "3.13.2",
53
53
  "@nuxt/test-utils": "3.14.4",
54
54
  "@scayle/eslint-config-storefront": "4.3.2",
55
- "@scayle/storefront-nuxt": "7.94.0",
55
+ "@scayle/storefront-nuxt": "7.95.0",
56
56
  "@types/node": "22.9.0",
57
57
  "@vue/test-utils": "2.4.6",
58
58
  "@vueuse/core": "11.2.0",
59
59
  "dprint": "0.47.5",
60
60
  "eslint": "9.14.0",
61
- "happy-dom": "15.11.0",
61
+ "happy-dom": "15.11.4",
62
62
  "eslint-formatter-gitlab": "5.1.0",
63
63
  "nuxt": "3.13.2",
64
64
  "publint": "0.2.12",