@cartbase/storefront 0.16.0 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartbase/storefront",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Storefront SDK + UI component library for Cartbase stores: typed API client, checkout orchestration, cart drawer, product/catalog components, tracking. Source-shipped TypeScript — add it to transpilePackages.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,6 +56,7 @@
56
56
  "./lib/hooks/use-intersection": "./src/lib/hooks/use-intersection.ts",
57
57
  "./lib/hooks/use-toggle-state": "./src/lib/hooks/use-toggle-state.ts",
58
58
  "./lib/cookie-names": "./src/lib/cookie-names.ts",
59
+ "./lib/media-image": "./src/lib/media-image.tsx",
59
60
  "./lib/platform": "./src/lib/platform.ts",
60
61
  "./lib/visitor": "./src/lib/visitor.ts",
61
62
  "./platform": "./src/platform/index.ts",
package/src/api/http.ts CHANGED
@@ -42,6 +42,42 @@ export interface StorefrontClientConfig {
42
42
  export type QueryValue = string | number | boolean | null | undefined
43
43
  export type Query = Record<string, QueryValue | QueryValue[]>
44
44
 
45
+ /**
46
+ * The reads the platform's CDN holds for anonymous shoppers (store-api.md
47
+ * § Caching). Such a request carries the store's publishable key in its
48
+ * address as `pk`, so the edge can tell one store's answers from another's,
49
+ * and the shopper's locale as `locale`. A request with a customer token
50
+ * carries neither: its prices are the shopper's own, and an address without
51
+ * the key is never held and never served from the hold. Carts, checkout,
52
+ * consent, customers and every write are never on this list.
53
+ */
54
+ const CACHEABLE_READ_PREFIXES = [
55
+ "/api/store/store",
56
+ "/api/store/menus",
57
+ "/api/store/regions",
58
+ "/api/store/collections",
59
+ "/api/store/products",
60
+ "/api/store/product-variants",
61
+ "/api/store/product-categories",
62
+ "/api/store/product-types",
63
+ "/api/store/product-tags",
64
+ "/api/store/reviews",
65
+ "/api/store/currencies",
66
+ "/api/store/countries",
67
+ "/api/store/locales",
68
+ "/api/store/pages",
69
+ "/api/store/blogs",
70
+ "/api/store/metaobjects",
71
+ "/api/store/url-redirects",
72
+ "/api/store/return-reasons",
73
+ ]
74
+
75
+ export function isCacheableStoreRead(pathname: string): boolean {
76
+ return CACHEABLE_READ_PREFIXES.some(
77
+ (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
78
+ )
79
+ }
80
+
45
81
  export interface RequestOptions {
46
82
  method?: "GET" | "POST" | "DELETE"
47
83
  query?: Query
@@ -92,9 +128,23 @@ export class StorefrontClient {
92
128
  const locale = this.config.getLocale ? await this.config.getLocale() : null
93
129
  if (locale && !headers["x-locale"]) headers["x-locale"] = locale
94
130
 
131
+ // An anonymous catalogue read carries its key (and locale) in the
132
+ // address, so the platform's CDN may hold the answer for this store.
133
+ const method = opts.method ?? "GET"
134
+ const anonymous = !headers.authorization
135
+ if (
136
+ method === "GET" &&
137
+ anonymous &&
138
+ this.config.publishableKey &&
139
+ isCacheableStoreRead(url.pathname)
140
+ ) {
141
+ url.searchParams.set("pk", this.config.publishableKey)
142
+ if (locale) url.searchParams.set("locale", locale)
143
+ }
144
+
95
145
  const doFetch = this.config.fetch ?? fetch
96
146
  const res = await doFetch(url.toString(), {
97
- method: opts.method ?? "GET",
147
+ method,
98
148
  headers,
99
149
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
100
150
  cache: opts.cache,
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import Image from "next/image"
3
+ import { StoreImage as Image } from "../lib/media-image"
4
4
  import Link from "next/link"
5
5
  import { useRef, useState } from "react"
6
6
 
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import Image from "next/image"
3
+ import { StoreImage as Image } from "../lib/media-image"
4
4
  import Link from "next/link"
5
5
 
6
6
  import type { StoreProduct } from "../api/products"
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import Image from "next/image"
3
+ import { StoreImage as Image } from "../lib/media-image"
4
4
 
5
5
  import { convertToLocale } from "../lib/money"
6
6
  import { useCartDrawer } from "./context"
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import Image from "next/image"
3
+ import { StoreImage as Image } from "../../lib/media-image"
4
4
  import Link from "next/link"
5
5
  import { type ReactNode } from "react"
6
6
 
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import Image from "next/image"
3
+ import { StoreImage as Image } from "../../lib/media-image"
4
4
  import Link from "next/link"
5
5
 
6
6
  import { convertToLocale } from "../../lib/money"
@@ -0,0 +1,39 @@
1
+ import NextImage, { type ImageProps } from "next/image"
2
+
3
+ /**
4
+ * The store's picture element (store-speed-law card, step 3, 2026-09-15).
5
+ *
6
+ * Product pictures live on the Cartbase media CDN, and Next resizes them on
7
+ * the store's own project, per device and in the modern formats, which the
8
+ * scaffold allows through `images.remotePatterns` (next.config.ts). The
9
+ * scaffold used to ship `images.unoptimized: true` instead, so no picture
10
+ * was ever resized: a phone downloaded the merchant's full upload.
11
+ *
12
+ * A picture from any other host (a catalogue not yet rehosted, a demo
13
+ * placeholder, a merchant's own asset host) renders as it is instead of
14
+ * failing the page: Next refuses to resize a host the config does not
15
+ * list, so this element switches resizing off for those. A merchant who
16
+ * adds their host to `remotePatterns` gets resizing there too by passing
17
+ * `unoptimized={false}` where they render it, or by using `next/image`
18
+ * directly.
19
+ */
20
+
21
+ /** The public base of the media CDN (REGISTRY.md, CDN domain). */
22
+ export const MEDIA_HOST = "cartbase.cloud"
23
+
24
+ /** Is this picture served by the media CDN (or by the app itself)? */
25
+ export function isResizableSrc(src: ImageProps["src"]): boolean {
26
+ if (typeof src !== "string") return true
27
+ if (src.startsWith("/")) return true
28
+ try {
29
+ const host = new URL(src).hostname
30
+ return host === MEDIA_HOST || host.endsWith(`.${MEDIA_HOST}`)
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ export function StoreImage(props: ImageProps) {
37
+ const unoptimized = props.unoptimized ?? !isResizableSrc(props.src)
38
+ return <NextImage {...props} unoptimized={unoptimized} />
39
+ }
@@ -1,4 +1,4 @@
1
- import Image from "next/image"
1
+ import { StoreImage as Image } from "../lib/media-image"
2
2
  import { ImageOff } from "lucide-react"
3
3
  import { Price } from "../lib/price"
4
4
  import { variantCaption } from "../lib/variant-caption"
@@ -5,7 +5,7 @@
5
5
  * (`@cartbase/storefront/api/products`; images arrive rank-ordered from the
6
6
  * API). Server-safe.
7
7
  */
8
- import Image from "next/image"
8
+ import { StoreImage as Image } from "../lib/media-image"
9
9
  import type { StoreProductImage } from "../api/products"
10
10
 
11
11
  type ImageGalleryProps = {
@@ -4,7 +4,7 @@
4
4
  * + `StoreProduct.images`). Server-safe (no "use client"): renders inside
5
5
  * RSC product grids.
6
6
  */
7
- import Image from "next/image"
7
+ import { StoreImage as Image } from "../lib/media-image"
8
8
  import React from "react"
9
9
  import { ImageOff } from "lucide-react"
10
10