@asstio-dev/storefront 0.21.0 → 0.24.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.
- package/README.md +20 -7
- package/dist/analytics/facts.js +3 -2
- package/dist/analytics/ga4.d.ts +3 -1
- package/dist/analytics/ga4.js +3 -3
- package/dist/analytics/types.d.ts +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/schema.d.ts +5 -0
- package/package.json +2 -2
- package/src/schema.d.ts +5 -0
package/README.md
CHANGED
|
@@ -282,11 +282,16 @@ differ from what the order later freezes. `productFacts()` reads them from any s
|
|
|
282
282
|
const facts = productFacts(line); // card, product, { product, variant }, cart line, checkout cart line or product order line
|
|
283
283
|
render(facts.brand?.label); // the item's brand: the variant's own, never a fallback to the family's
|
|
284
284
|
render(facts.productBrand?.label); // the family brand
|
|
285
|
+
render(facts.primaryCategory?.pathNames.join(" > ")); // the one category to use for analytics, breadcrumbs and feeds
|
|
285
286
|
```
|
|
286
287
|
|
|
287
288
|
`brand` is the selected or purchased variant's brand, and `null` can mean the variant deliberately has none — do not
|
|
288
|
-
fall back to `productBrand`.
|
|
289
|
-
product-type node.
|
|
289
|
+
fall back to `productBrand`. **`categories` carry no primary meaning**: `categories[0]` may be a gender, a brand or a
|
|
290
|
+
product-type node. `primaryCategory` is the one to use: the storefront picks it under the site's product-taxonomy root, one
|
|
291
|
+
of `categories`, and it is `null` when the site has no root, the product has no category under it, or the line was frozen
|
|
292
|
+
before it existed. Identify a root by `pathIds[0]`, never by its translated name. `pathIds` and `primaryCategory` need
|
|
293
|
+
API v0.24.0 or later; on order lines and checkout lines frozen from a snapshot taken before v0.24.0, `pathIds` is `[]` and
|
|
294
|
+
`primaryCategory` is `null`.
|
|
290
295
|
|
|
291
296
|
`toGa4Item()` and `toGa4Ecommerce()` map the same sources to GA4 ecommerce data; `toGa4Ecommerce(checkout.cart)` is the
|
|
292
297
|
`begin_checkout` payload. They are pure: they send nothing and
|
|
@@ -306,20 +311,28 @@ know nothing about consent or a data layer.
|
|
|
306
311
|
25 % gives `price: 7.99`, `value: 23.97`). Pass `tax: "include"` to report gross on purpose.
|
|
307
312
|
- For `add_to_cart` and `remove_from_cart`, pass `quantity`: the positive number of units added or removed, never the
|
|
308
313
|
line's new total.
|
|
309
|
-
- `item_category`…`item_category5` come from
|
|
310
|
-
`
|
|
314
|
+
- `item_category`…`item_category5` come from `primaryCategory`'s `pathNames` (the full path, root included). When
|
|
315
|
+
`primaryCategory` is `null` the item has **no** category fields: null can mean "this product has no category under the
|
|
316
|
+
taxonomy root", and a fallback would put a brand or series root into GA4. Since v0.24.0 this is the default; before it
|
|
317
|
+
was the longest path, which `pickCategory: longestCategory` still gives. A `pickCategory` gets the categories and the
|
|
318
|
+
line's facts, `(categories, facts) => CategoryRefDto | undefined`; returning `undefined` means no category fields.
|
|
319
|
+
- Identify a root by id (`pathIds[0]`), never by its translated name.
|
|
311
320
|
- No `discount`: `compareAt` is a recommended price, not a discount applied.
|
|
312
321
|
- A source without a price throws `AnalyticsError`, as does a non-product order line; never `price: 0`.
|
|
313
322
|
- For an order, `transaction_id` is `order.id` (stable from completion; `orderNumber` is null until export), `tax` the
|
|
314
323
|
recorded VAT and `shipping` the shipping lines' net.
|
|
315
324
|
|
|
316
325
|
```ts
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
// The primary category is the default; this drops a top-level taxonomy root from the emitted path.
|
|
327
|
+
const belowRoot = (_: readonly CategoryRefDto[], f: ProductFacts) =>
|
|
328
|
+
f.primaryCategory ? { ...f.primaryCategory, pathNames: f.primaryCategory.pathNames.slice(1) } : undefined;
|
|
329
|
+
gtag("event", "view_item_list", { items: listing.items.map((card, index) => toGa4Item(card, { index, pickCategory: belowRoot })) });
|
|
319
330
|
gtag("event", "add_to_cart", { currency: cart.currency, items: [toGa4Item(line, { quantity: added })] });
|
|
320
|
-
gtag("event", "purchase", toGa4Ecommerce(order, { pickCategory:
|
|
331
|
+
gtag("event", "purchase", toGa4Ecommerce(order, { pickCategory: belowRoot }));
|
|
321
332
|
```
|
|
322
333
|
|
|
334
|
+
`slice(1)` is right only when the site's taxonomy root is a top-level category.
|
|
335
|
+
|
|
323
336
|
### Search-as-you-type and search-in-facet
|
|
324
337
|
|
|
325
338
|
`/v1/search/suggest` answers three short lists for what the shopper has typed so far: up to 5 product cards
|
package/dist/analytics/facts.js
CHANGED
|
@@ -8,6 +8,7 @@ export function productFacts(source) {
|
|
|
8
8
|
throw new AnalyticsError(`an order line of type "${line.type}" has no product facts`);
|
|
9
9
|
return {
|
|
10
10
|
brand: line.variant.brand, productBrand: line.product.brand, categories: line.product.categories,
|
|
11
|
+
primaryCategory: line.product.primaryCategory ?? null,
|
|
11
12
|
attributes: line.product.attributes, shortDescription: null,
|
|
12
13
|
};
|
|
13
14
|
}
|
|
@@ -17,7 +18,7 @@ export function productFacts(source) {
|
|
|
17
18
|
const { product, variant } = source;
|
|
18
19
|
const short = product.shortDescription;
|
|
19
20
|
return {
|
|
20
|
-
brand: variant.brand, productBrand: product.brand, categories: product.categories,
|
|
21
|
+
brand: variant.brand, productBrand: product.brand, categories: product.categories, primaryCategory: product.primaryCategory ?? null,
|
|
21
22
|
attributes: product.attributes, shortDescription: short ?? null,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
@@ -25,7 +26,7 @@ export function productFacts(source) {
|
|
|
25
26
|
if (!Array.isArray(flat.categories) || !Array.isArray(flat.attributes))
|
|
26
27
|
throw new AnalyticsError("not a product facts source");
|
|
27
28
|
return {
|
|
28
|
-
brand: flat.brand, productBrand: flat.brand, categories: flat.categories, attributes: flat.attributes,
|
|
29
|
+
brand: flat.brand, productBrand: flat.brand, categories: flat.categories, primaryCategory: flat.primaryCategory ?? null, attributes: flat.attributes,
|
|
29
30
|
shortDescription: flat.shortDescription ?? null,
|
|
30
31
|
};
|
|
31
32
|
}
|
package/dist/analytics/ga4.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CartDto, type CheckoutCartDto, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type OrderDto } from "./types.js";
|
|
1
|
+
import { type CartDto, type CategoryRefDto, type CheckoutCartDto, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type OrderDto } from "./types.js";
|
|
2
2
|
/** An item and its unit price in cents, so a payload's `value` sums exact cents rather than re-reading floats. */
|
|
3
3
|
export type MappedItem = {
|
|
4
4
|
item: Ga4Item;
|
|
@@ -20,3 +20,5 @@ export declare function mapItem(source: Ga4ItemSource, opts?: Ga4ItemOptions): M
|
|
|
20
20
|
export declare function toGa4Ecommerce(source: CartDto, opts?: Ga4SharedOptions): Ga4Ecommerce;
|
|
21
21
|
export declare function toGa4Ecommerce(source: CheckoutCartDto, opts?: Ga4SharedOptions): Ga4Ecommerce;
|
|
22
22
|
export declare function toGa4Ecommerce(source: OrderDto, opts?: Ga4SharedOptions): Ga4Purchase;
|
|
23
|
+
/** The longest path, the first of equals — the default before `primaryCategory` existed. Never mutates the array. */
|
|
24
|
+
export declare function longestCategory(categories: readonly CategoryRefDto[]): CategoryRefDto | undefined;
|
package/dist/analytics/ga4.js
CHANGED
|
@@ -20,7 +20,7 @@ export function mapItem(source, opts = {}) {
|
|
|
20
20
|
const variant = itemVariant(line.options);
|
|
21
21
|
if (variant !== undefined)
|
|
22
22
|
item.item_variant = variant;
|
|
23
|
-
const picked = opts.pickCategory ? opts.pickCategory(facts.categories) :
|
|
23
|
+
const picked = opts.pickCategory ? opts.pickCategory(facts.categories, facts) : facts.primaryCategory ?? undefined;
|
|
24
24
|
if (picked) {
|
|
25
25
|
const keys = ["item_category", "item_category2", "item_category3", "item_category4", "item_category5"];
|
|
26
26
|
picked.pathNames.slice(0, keys.length).forEach((name, i) => { item[keys[i]] = name; });
|
|
@@ -106,8 +106,8 @@ function itemVariant(options) {
|
|
|
106
106
|
const axes = Object.keys(options).sort();
|
|
107
107
|
return axes.length === 0 ? undefined : axes.map(a => `${a}: ${options[a]}`).join(", ");
|
|
108
108
|
}
|
|
109
|
-
/** The
|
|
110
|
-
function
|
|
109
|
+
/** The longest path, the first of equals — the default before `primaryCategory` existed. Never mutates the array. */
|
|
110
|
+
export function longestCategory(categories) {
|
|
111
111
|
let best;
|
|
112
112
|
for (const c of categories)
|
|
113
113
|
if (!best || c.pathNames.length > best.pathNames.length)
|
|
@@ -26,6 +26,10 @@ export type ProductFacts = {
|
|
|
26
26
|
productBrand: BrandDto | null;
|
|
27
27
|
/** Classification memberships with no primary meaning; `categories[0]` may be any root. */
|
|
28
28
|
categories: CategoryRefDto[];
|
|
29
|
+
/** The one category to use for analytics, breadcrumbs and feeds: one of `categories`, chosen by the storefront under the
|
|
30
|
+
* site's product-taxonomy root. `null` when the site has none, the product has no category under it, or the line was frozen
|
|
31
|
+
* before it existed. */
|
|
32
|
+
primaryCategory: CategoryRefDto | null;
|
|
29
33
|
/** As the source carries them: a `ProductDto` may include `html` attributes; cards, cart and order lines do not. */
|
|
30
34
|
attributes: AttributeDto[];
|
|
31
35
|
/** Null on order lines and checkout cart lines. */
|
|
@@ -61,9 +65,10 @@ export type Ga4SharedOptions = {
|
|
|
61
65
|
affiliation?: string;
|
|
62
66
|
itemListId?: string;
|
|
63
67
|
itemListName?: string;
|
|
64
|
-
/** Which category feeds `item_category`…`item_category5`. Default:
|
|
65
|
-
*
|
|
66
|
-
|
|
68
|
+
/** Which category feeds `item_category`…`item_category5`. Default: `facts.primaryCategory`, and no category fields when it is
|
|
69
|
+
* null (never a fallback: null can mean "this product has no category under the taxonomy root"). Pass `longestCategory` for
|
|
70
|
+
* the longest path. Returning `undefined` means no category fields. The second argument is the line's own facts. */
|
|
71
|
+
pickCategory?: (categories: readonly CategoryRefDto[], facts: ProductFacts) => CategoryRefDto | undefined;
|
|
67
72
|
};
|
|
68
73
|
/** One item's options: `quantity` overrides the source's (for add/remove events: the positive number of units added or
|
|
69
74
|
* removed), `itemId` its id (e.g. for family-level reporting), `index` its position in a list. */
|
package/dist/index.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export { gtinJsonLd } from "./gtin.js";
|
|
|
57
57
|
export { productJsonLd } from "./seo/product.js";
|
|
58
58
|
export { jsonLdScript } from "./seo/serialize.js";
|
|
59
59
|
export { productFacts } from "./analytics/facts.js";
|
|
60
|
-
export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
|
|
60
|
+
export { longestCategory, toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
|
|
61
61
|
export { trackSearchEvents, type SearchEvent, type SearchEventsResponse } from "./analytics/events.js";
|
|
62
62
|
export { AnalyticsError, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type ProductFacts, type SelectedVariant, } from "./analytics/types.js";
|
|
63
63
|
export { SeoError, type BreadcrumbLink, type CollectionHeadOptions, type HeadOptions, type JsonLd, type NextMetadataLike, type ProductJsonLdOptions, type SchemaProperty, type SeoContext, type SeoHead, type SeoMeta, type VariantAvailability, } from "./seo/types.js";
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ export { gtinJsonLd } from "./gtin.js";
|
|
|
46
46
|
export { productJsonLd } from "./seo/product.js";
|
|
47
47
|
export { jsonLdScript } from "./seo/serialize.js";
|
|
48
48
|
export { productFacts } from "./analytics/facts.js";
|
|
49
|
-
export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
|
|
49
|
+
export { longestCategory, toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
|
|
50
50
|
export { trackSearchEvents } from "./analytics/events.js";
|
|
51
51
|
export { AnalyticsError, } from "./analytics/types.js";
|
|
52
52
|
export { SeoError, } from "./seo/types.js";
|
package/dist/schema.d.ts
CHANGED
|
@@ -536,6 +536,7 @@ export interface components {
|
|
|
536
536
|
categories: components["schemas"]["CategoryRefDto"][];
|
|
537
537
|
id: string;
|
|
538
538
|
name: string;
|
|
539
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
539
540
|
shortDescription: null | string;
|
|
540
541
|
slug: string;
|
|
541
542
|
};
|
|
@@ -556,6 +557,7 @@ export interface components {
|
|
|
556
557
|
CategoryRefDto: {
|
|
557
558
|
id: string;
|
|
558
559
|
name: string;
|
|
560
|
+
pathIds: string[];
|
|
559
561
|
pathNames: string[];
|
|
560
562
|
};
|
|
561
563
|
CheckoutCartDto: {
|
|
@@ -917,6 +919,7 @@ export interface components {
|
|
|
917
919
|
attributes: components["schemas"]["AttributeDto"][];
|
|
918
920
|
brand: null | components["schemas"]["BrandDto"];
|
|
919
921
|
categories: components["schemas"]["CategoryRefDto"][];
|
|
922
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
920
923
|
};
|
|
921
924
|
OrderLineVariantDto: {
|
|
922
925
|
brand: null | components["schemas"]["BrandDto"];
|
|
@@ -977,6 +980,7 @@ export interface components {
|
|
|
977
980
|
name: string;
|
|
978
981
|
price: null | components["schemas"]["PriceDto"];
|
|
979
982
|
priceVaries: boolean;
|
|
983
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
980
984
|
shortDescription: null | string;
|
|
981
985
|
sku: string;
|
|
982
986
|
slug: string;
|
|
@@ -1003,6 +1007,7 @@ export interface components {
|
|
|
1003
1007
|
matchedVariantId: null | string;
|
|
1004
1008
|
name: string;
|
|
1005
1009
|
options: components["schemas"]["OptionDto"][];
|
|
1010
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
1006
1011
|
redirectTo: null | string;
|
|
1007
1012
|
related: components["schemas"]["RelatedGroupDto"][];
|
|
1008
1013
|
responsible: components["schemas"]["ResponsibleDto"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asstio-dev/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:browser": "playwright test -c browser/playwright.config.ts",
|
|
23
23
|
"check": "npm run generate && npm run typecheck-examples && npm test && git diff --exit-code -- src/schema.d.ts && test -z \"$(git status --porcelain -- src/schema.d.ts)\"",
|
|
24
|
-
"smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"liveShippingFor\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"gtinJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\", \\\"productFacts\\\", \\\"toGa4Item\\\", \\\"toGa4Ecommerce\\\", \\\"AnalyticsError\\\", \\\"trackSearchEvents\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
|
|
24
|
+
"smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"liveShippingFor\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"gtinJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\", \\\"productFacts\\\", \\\"toGa4Item\\\", \\\"toGa4Ecommerce\\\", \\\"longestCategory\\\", \\\"AnalyticsError\\\", \\\"trackSearchEvents\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"openapi-fetch": "0.17.0"
|
package/src/schema.d.ts
CHANGED
|
@@ -536,6 +536,7 @@ export interface components {
|
|
|
536
536
|
categories: components["schemas"]["CategoryRefDto"][];
|
|
537
537
|
id: string;
|
|
538
538
|
name: string;
|
|
539
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
539
540
|
shortDescription: null | string;
|
|
540
541
|
slug: string;
|
|
541
542
|
};
|
|
@@ -556,6 +557,7 @@ export interface components {
|
|
|
556
557
|
CategoryRefDto: {
|
|
557
558
|
id: string;
|
|
558
559
|
name: string;
|
|
560
|
+
pathIds: string[];
|
|
559
561
|
pathNames: string[];
|
|
560
562
|
};
|
|
561
563
|
CheckoutCartDto: {
|
|
@@ -917,6 +919,7 @@ export interface components {
|
|
|
917
919
|
attributes: components["schemas"]["AttributeDto"][];
|
|
918
920
|
brand: null | components["schemas"]["BrandDto"];
|
|
919
921
|
categories: components["schemas"]["CategoryRefDto"][];
|
|
922
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
920
923
|
};
|
|
921
924
|
OrderLineVariantDto: {
|
|
922
925
|
brand: null | components["schemas"]["BrandDto"];
|
|
@@ -977,6 +980,7 @@ export interface components {
|
|
|
977
980
|
name: string;
|
|
978
981
|
price: null | components["schemas"]["PriceDto"];
|
|
979
982
|
priceVaries: boolean;
|
|
983
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
980
984
|
shortDescription: null | string;
|
|
981
985
|
sku: string;
|
|
982
986
|
slug: string;
|
|
@@ -1003,6 +1007,7 @@ export interface components {
|
|
|
1003
1007
|
matchedVariantId: null | string;
|
|
1004
1008
|
name: string;
|
|
1005
1009
|
options: components["schemas"]["OptionDto"][];
|
|
1010
|
+
primaryCategory: null | components["schemas"]["CategoryRefDto"];
|
|
1006
1011
|
redirectTo: null | string;
|
|
1007
1012
|
related: components["schemas"]["RelatedGroupDto"][];
|
|
1008
1013
|
responsible: components["schemas"]["ResponsibleDto"];
|