create-cartbase 0.1.19 → 0.1.21

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 (35) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +23 -4
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +288 -31
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/regions.md +269 -269
  16. package/template/app/docs/reviews.md +258 -223
  17. package/template/app/docs/search.md +227 -227
  18. package/template/app/docs/store.md +23 -1
  19. package/template/app/docs/subscriptions.md +148 -148
  20. package/template/app/docs/variables.md +331 -315
  21. package/template/app/package.json +1 -1
  22. package/template/app/postcss.config.cjs +11 -11
  23. package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
  25. package/template/app/src/app/checkout/error.tsx +23 -0
  26. package/template/app/src/app/checkout/page.tsx +16 -7
  27. package/template/app/src/app/globals.css +26 -26
  28. package/template/app/src/app/layout.tsx +126 -126
  29. package/template/app/src/app/page.tsx +37 -37
  30. package/template/app/src/app/products/[handle]/page.tsx +89 -89
  31. package/template/app/src/app/search/page.tsx +33 -33
  32. package/template/app/src/lib/browser-client.ts +35 -35
  33. package/template/app/src/lib/catalog.ts +120 -120
  34. package/template/app/src/lib/server-client.ts +25 -25
  35. package/template/app/src/lib/store-client.ts +16 -16
@@ -1,37 +1,37 @@
1
- import { Suspense } from "react"
2
- import { StoreTemplate } from "@cartbase/storefront/store/store-template"
3
- import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
4
- import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
5
- import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
6
- import { storeClient } from "@/lib/store-client"
7
-
8
- type ListingQuery = { sortBy?: string; page?: string }
9
-
10
- /**
11
- * Home = the all-products listing (runbook step 5: always pass a pricing
12
- * context or prices come back undecorated). The sort and the page number
13
- * are request data, so the listing renders behind a boundary and the shell
14
- * around it stays prerendered.
15
- */
16
- export default function HomePage({ searchParams }: { searchParams: Promise<ListingQuery> }) {
17
- return (
18
- <Suspense fallback={<SkeletonProductGrid />}>
19
- <Listing searchParams={searchParams} />
20
- </Suspense>
21
- )
22
- }
23
-
24
- async function Listing({ searchParams }: { searchParams: Promise<ListingQuery> }) {
25
- const { sortBy, page } = await searchParams
26
- return (
27
- <StoreTemplate
28
- client={storeClient}
29
- sortBy={sortBy as SortOptions | undefined}
30
- page={page}
31
- pricingContext={PRICING_CONTEXT}
32
- // Server-rendered: the locale provider cannot reach it, so the pack
33
- // comes as a prop, and the prop is required.
34
- labels={STORE_LOCALE.store}
35
- />
36
- )
37
- }
1
+ import { Suspense } from "react"
2
+ import { StoreTemplate } from "@cartbase/storefront/store/store-template"
3
+ import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
4
+ import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
5
+ import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
6
+ import { storeClient } from "@/lib/store-client"
7
+
8
+ type ListingQuery = { sortBy?: string; page?: string }
9
+
10
+ /**
11
+ * Home = the all-products listing (runbook step 5: always pass a pricing
12
+ * context or prices come back undecorated). The sort and the page number
13
+ * are request data, so the listing renders behind a boundary and the shell
14
+ * around it stays prerendered.
15
+ */
16
+ export default function HomePage({ searchParams }: { searchParams: Promise<ListingQuery> }) {
17
+ return (
18
+ <Suspense fallback={<SkeletonProductGrid />}>
19
+ <Listing searchParams={searchParams} />
20
+ </Suspense>
21
+ )
22
+ }
23
+
24
+ async function Listing({ searchParams }: { searchParams: Promise<ListingQuery> }) {
25
+ const { sortBy, page } = await searchParams
26
+ return (
27
+ <StoreTemplate
28
+ client={storeClient}
29
+ sortBy={sortBy as SortOptions | undefined}
30
+ page={page}
31
+ pricingContext={PRICING_CONTEXT}
32
+ // Server-rendered: the locale provider cannot reach it, so the pack
33
+ // comes as a prop, and the prop is required.
34
+ labels={STORE_LOCALE.store}
35
+ />
36
+ )
37
+ }
@@ -1,89 +1,89 @@
1
- import { notFound } from "next/navigation"
2
- import { ProductTemplate } from "@cartbase/storefront/products/product-template"
3
- import { getProduct, listProductHandles } from "@/lib/catalog"
4
- import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
5
- import { storeClient } from "@/lib/store-client"
6
-
7
- /**
8
- * Every product's page is prerendered at build, so it is a file on the CDN.
9
- * A product added after the last deploy renders on its first visit and is
10
- * kept from then on. Without this list the handle would be request data and
11
- * every product page would render per visit (Cache Components).
12
- */
13
- export async function generateStaticParams() {
14
- const handles = await listProductHandles()
15
- // Cache Components refuses an empty list; a store with no products yet
16
- // still builds, and the placeholder answers 404.
17
- return handles.length ? handles.map((handle) => ({ handle })) : [{ handle: "__none__" }]
18
- }
19
-
20
- /**
21
- * PDP (runbook step 5): the product is a cached read by handle WITH the
22
- * pricing context (lib/catalog.ts), and the template renders it. The buy
23
- * box re-reads the product live behind its own boundary, so price and stock
24
- * are this minute's while the rest of the page is prerendered.
25
- *
26
- * No `addToCart` is passed: the add goes through the cart drawer mounted in
27
- * `providers.tsx`, so the drawer opens at the click with the product in it
28
- * and no page render runs (the product page contract, rule 6). The drawer's
29
- * `onCartChange` owns the cart cookie.
30
- */
31
- export default async function ProductPage({
32
- params,
33
- searchParams,
34
- }: {
35
- params: Promise<{ handle: string }>
36
- searchParams: Promise<{ variant?: string }>
37
- }) {
38
- const { handle } = await params
39
- const product = await getProduct(handle)
40
- if (!product) notFound()
41
-
42
- return (
43
- <ProductTemplate
44
- client={storeClient}
45
- product={product}
46
- pricingContext={PRICING_CONTEXT}
47
- // The product page contract: the address names the variant
48
- // (`?variant=<digits>`, Shopify's parameter), and the FIRST paint of
49
- // the buy box is that variant's price, code and stock. The query is
50
- // request data, so it goes in as a promise and the live buy box reads
51
- // it behind its boundary. Switching a variant rewrites the address in
52
- // the browser without asking the server.
53
- initialVariantId={searchParams.then((query) => query.variant)}
54
- // NO `promises` here, deliberately. Until 0.13.0 the library carried
55
- // delivery, exchange and return promises as label DEFAULTS, so every
56
- // store scaffolded from this file told shoppers "your package will
57
- // arrive in 3-5 business days" and "we'll refund your money" without
58
- // anyone having decided that. Those are YOUR commitments, so you write
59
- // them:
60
- //
61
- // promises={[
62
- // { icon: "delivery", title: "Fast delivery",
63
- // body: "Your order arrives in 2 working days." },
64
- // ]}
65
- //
66
- // Pass nothing and the section does not exist, which is the right
67
- // default for a store that has not decided yet. The physical-facts
68
- // section appears on its own for products that HAVE facts, and is
69
- // skipped for those that do not.
70
- //
71
- // The pack has to be handed over here, not just mounted at the root:
72
- // the related-products strip is a server component and cannot read
73
- // the locale provider, so its heading would stay English. Required.
74
- labels={STORE_LOCALE.products}
75
- />
76
- )
77
- }
78
-
79
- export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }) {
80
- const { handle } = await params
81
- // The page's own read: one cache entry answers both.
82
- const product = await getProduct(handle).catch(() => null)
83
- if (!product) return {}
84
- // SEO fields with title/description fallbacks (runbook step 5).
85
- return {
86
- title: product.seo_title ?? product.title,
87
- description: product.seo_description ?? product.description ?? undefined,
88
- }
89
- }
1
+ import { notFound } from "next/navigation"
2
+ import { ProductTemplate } from "@cartbase/storefront/products/product-template"
3
+ import { getProduct, listProductHandles } from "@/lib/catalog"
4
+ import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
5
+ import { storeClient } from "@/lib/store-client"
6
+
7
+ /**
8
+ * Every product's page is prerendered at build, so it is a file on the CDN.
9
+ * A product added after the last deploy renders on its first visit and is
10
+ * kept from then on. Without this list the handle would be request data and
11
+ * every product page would render per visit (Cache Components).
12
+ */
13
+ export async function generateStaticParams() {
14
+ const handles = await listProductHandles()
15
+ // Cache Components refuses an empty list; a store with no products yet
16
+ // still builds, and the placeholder answers 404.
17
+ return handles.length ? handles.map((handle) => ({ handle })) : [{ handle: "__none__" }]
18
+ }
19
+
20
+ /**
21
+ * PDP (runbook step 5): the product is a cached read by handle WITH the
22
+ * pricing context (lib/catalog.ts), and the template renders it. The buy
23
+ * box re-reads the product live behind its own boundary, so price and stock
24
+ * are this minute's while the rest of the page is prerendered.
25
+ *
26
+ * No `addToCart` is passed: the add goes through the cart drawer mounted in
27
+ * `providers.tsx`, so the drawer opens at the click with the product in it
28
+ * and no page render runs (the product page contract, rule 6). The drawer's
29
+ * `onCartChange` owns the cart cookie.
30
+ */
31
+ export default async function ProductPage({
32
+ params,
33
+ searchParams,
34
+ }: {
35
+ params: Promise<{ handle: string }>
36
+ searchParams: Promise<{ variant?: string }>
37
+ }) {
38
+ const { handle } = await params
39
+ const product = await getProduct(handle)
40
+ if (!product) notFound()
41
+
42
+ return (
43
+ <ProductTemplate
44
+ client={storeClient}
45
+ product={product}
46
+ pricingContext={PRICING_CONTEXT}
47
+ // The product page contract: the address names the variant
48
+ // (`?variant=<digits>`, Shopify's parameter), and the FIRST paint of
49
+ // the buy box is that variant's price, code and stock. The query is
50
+ // request data, so it goes in as a promise and the live buy box reads
51
+ // it behind its boundary. Switching a variant rewrites the address in
52
+ // the browser without asking the server.
53
+ initialVariantId={searchParams.then((query) => query.variant)}
54
+ // NO `promises` here, deliberately. Until 0.13.0 the library carried
55
+ // delivery, exchange and return promises as label DEFAULTS, so every
56
+ // store scaffolded from this file told shoppers "your package will
57
+ // arrive in 3-5 business days" and "we'll refund your money" without
58
+ // anyone having decided that. Those are YOUR commitments, so you write
59
+ // them:
60
+ //
61
+ // promises={[
62
+ // { icon: "delivery", title: "Fast delivery",
63
+ // body: "Your order arrives in 2 working days." },
64
+ // ]}
65
+ //
66
+ // Pass nothing and the section does not exist, which is the right
67
+ // default for a store that has not decided yet. The physical-facts
68
+ // section appears on its own for products that HAVE facts, and is
69
+ // skipped for those that do not.
70
+ //
71
+ // The pack has to be handed over here, not just mounted at the root:
72
+ // the related-products strip is a server component and cannot read
73
+ // the locale provider, so its heading would stay English. Required.
74
+ labels={STORE_LOCALE.products}
75
+ />
76
+ )
77
+ }
78
+
79
+ export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }) {
80
+ const { handle } = await params
81
+ // The page's own read: one cache entry answers both.
82
+ const product = await getProduct(handle).catch(() => null)
83
+ if (!product) return {}
84
+ // SEO fields with title/description fallbacks (runbook step 5).
85
+ return {
86
+ title: product.seo_title ?? product.title,
87
+ description: product.seo_description ?? product.description ?? undefined,
88
+ }
89
+ }
@@ -1,33 +1,33 @@
1
- import { Suspense } from "react"
2
- import { SearchTemplate } from "@cartbase/storefront/store/search-template"
3
- import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
4
- import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
5
- import { storeClient } from "@/lib/store-client"
6
-
7
- type Query = Record<string, string | string[] | undefined>
8
-
9
- /**
10
- * Search page (runbook step 5) — fully URL-state driven search-template. The
11
- * query is request data, so the results render behind a boundary.
12
- */
13
- export default function SearchPage({ searchParams }: { searchParams: Promise<Query> }) {
14
- return (
15
- <Suspense fallback={<SkeletonProductGrid />}>
16
- <Results searchParams={searchParams} />
17
- </Suspense>
18
- )
19
- }
20
-
21
- async function Results({ searchParams }: { searchParams: Promise<Query> }) {
22
- const params = await searchParams
23
- return (
24
- <SearchTemplate
25
- client={storeClient}
26
- searchParams={params}
27
- pricingContext={PRICING_CONTEXT}
28
- // Server-rendered: the locale provider cannot reach it, so the pack
29
- // comes as a prop, and the prop is required.
30
- labels={STORE_LOCALE.store}
31
- />
32
- )
33
- }
1
+ import { Suspense } from "react"
2
+ import { SearchTemplate } from "@cartbase/storefront/store/search-template"
3
+ import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
4
+ import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
5
+ import { storeClient } from "@/lib/store-client"
6
+
7
+ type Query = Record<string, string | string[] | undefined>
8
+
9
+ /**
10
+ * Search page (runbook step 5) — fully URL-state driven search-template. The
11
+ * query is request data, so the results render behind a boundary.
12
+ */
13
+ export default function SearchPage({ searchParams }: { searchParams: Promise<Query> }) {
14
+ return (
15
+ <Suspense fallback={<SkeletonProductGrid />}>
16
+ <Results searchParams={searchParams} />
17
+ </Suspense>
18
+ )
19
+ }
20
+
21
+ async function Results({ searchParams }: { searchParams: Promise<Query> }) {
22
+ const params = await searchParams
23
+ return (
24
+ <SearchTemplate
25
+ client={storeClient}
26
+ searchParams={params}
27
+ pricingContext={PRICING_CONTEXT}
28
+ // Server-rendered: the locale provider cannot reach it, so the pack
29
+ // comes as a prop, and the prop is required.
30
+ labels={STORE_LOCALE.store}
31
+ />
32
+ )
33
+ }
@@ -1,35 +1,35 @@
1
- "use client"
2
-
3
- import { StorefrontClient } from "@cartbase/storefront/api/http"
4
- import {
5
- BARTER_CLIENT_ID,
6
- BARTER_PUBLISHABLE_KEY,
7
- BARTER_URL,
8
- LOCALE_COOKIE,
9
- } from "./config"
10
-
11
- function readCookie(name: string): string | null {
12
- if (typeof document === "undefined") return null
13
- const match = document.cookie
14
- .split("; ")
15
- .find((row) => row.startsWith(`${name}=`))
16
- return match ? decodeURIComponent(match.slice(name.length + 1)) : null
17
- }
18
-
19
- /**
20
- * Browser-scope StorefrontClient (runbook step 2): constructed once per
21
- * app. Guest-only reference app — no auth token store.
22
- *
23
- * baseUrl is the APP origin, not the API origin: the store API ships no
24
- * CORS headers, so browser calls ride the same-origin `/api/store/*`
25
- * rewrite (next.config.ts). During SSR of client components no fetches
26
- * run — the API origin stands in only to satisfy the constructor.
27
- */
28
- export const browserClient = new StorefrontClient({
29
- baseUrl:
30
- typeof window !== "undefined" ? window.location.origin : BARTER_URL,
31
- clientId: BARTER_CLIENT_ID,
32
- publishableKey: BARTER_PUBLISHABLE_KEY,
33
- getAuthToken: () => null,
34
- getLocale: () => readCookie(LOCALE_COOKIE),
35
- })
1
+ "use client"
2
+
3
+ import { StorefrontClient } from "@cartbase/storefront/api/http"
4
+ import {
5
+ BARTER_CLIENT_ID,
6
+ BARTER_PUBLISHABLE_KEY,
7
+ BARTER_URL,
8
+ LOCALE_COOKIE,
9
+ } from "./config"
10
+
11
+ function readCookie(name: string): string | null {
12
+ if (typeof document === "undefined") return null
13
+ const match = document.cookie
14
+ .split("; ")
15
+ .find((row) => row.startsWith(`${name}=`))
16
+ return match ? decodeURIComponent(match.slice(name.length + 1)) : null
17
+ }
18
+
19
+ /**
20
+ * Browser-scope StorefrontClient (runbook step 2): constructed once per
21
+ * app. Guest-only reference app — no auth token store.
22
+ *
23
+ * baseUrl is the APP origin, not the API origin: the store API ships no
24
+ * CORS headers, so browser calls ride the same-origin `/api/store/*`
25
+ * rewrite (next.config.ts). During SSR of client components no fetches
26
+ * run — the API origin stands in only to satisfy the constructor.
27
+ */
28
+ export const browserClient = new StorefrontClient({
29
+ baseUrl:
30
+ typeof window !== "undefined" ? window.location.origin : BARTER_URL,
31
+ clientId: BARTER_CLIENT_ID,
32
+ publishableKey: BARTER_PUBLISHABLE_KEY,
33
+ getAuthToken: () => null,
34
+ getLocale: () => readCookie(LOCALE_COOKIE),
35
+ })
@@ -1,120 +1,120 @@
1
- import { cacheLife, cacheTag } from "next/cache"
2
- import { getConsent, type ConsentConfig } from "@cartbase/storefront/api/consent"
3
- import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
4
- import { listProducts, retrieveProduct, type StoreProduct } from "@cartbase/storefront/api/products"
5
- import { getStore, type StoreIdentity } from "@cartbase/storefront/api/store"
6
- import { getTrackingConfig } from "@cartbase/storefront/tracking/get-tracking-config"
7
- import type { TrackingConfig } from "@cartbase/storefront/tracking/types"
8
- import { BARTER_CLIENT_ID, PRICING_CONTEXT } from "./config"
9
- import { storeClient } from "./store-client"
10
-
11
- /**
12
- * The store's cached reads (runbook step 3, Cache Components). Each is a
13
- * `use cache: remote` function on the `catalog` life (next.config.ts): the
14
- * platform is asked once per window, whichever instance renders, and every
15
- * page built from these reads is a prerendered file on the CDN.
16
- *
17
- * Failure contract: inside the cache a failed read THROWS, so a platform
18
- * blip is never stored as an empty answer for the whole window; the public
19
- * function decides what an error reads as. An unknown handle (404) is an
20
- * answer, null, and is kept.
21
- */
22
-
23
- function isNotFound(error: unknown): boolean {
24
- return (error as { status?: number } | null)?.status === 404
25
- }
26
-
27
- const FALLBACK_STORE: StoreIdentity = {
28
- id: BARTER_CLIENT_ID ?? "",
29
- name: "Store",
30
- slug: "",
31
- brand: {
32
- logo_url: null,
33
- logo_square_url: null,
34
- color_primary: null,
35
- color_secondary: null,
36
- slogan: null,
37
- },
38
- }
39
-
40
- async function readStore(): Promise<StoreIdentity> {
41
- "use cache: remote"
42
- cacheLife("catalog")
43
- cacheTag("catalog")
44
- const { store } = await getStore(storeClient)
45
- return store
46
- }
47
-
48
- /**
49
- * The store's own identity (store.md): name, slug, brand. A storefront never
50
- * hardcodes its name, and a transient API error never takes the layout down.
51
- */
52
- export async function getStoreIdentity(): Promise<StoreIdentity> {
53
- return readStore().catch(() => FALLBACK_STORE)
54
- }
55
-
56
- /** The store's consent configuration; the platform always answers a complete one. */
57
- export async function getConsentSettings(): Promise<ConsentConfig> {
58
- "use cache: remote"
59
- cacheLife("catalog")
60
- cacheTag("catalog")
61
- const { consent } = await getConsent(storeClient)
62
- return consent
63
- }
64
-
65
- async function readTracking(): Promise<TrackingConfig> {
66
- "use cache: remote"
67
- cacheLife("catalog")
68
- cacheTag("catalog")
69
- return getTrackingConfig(storeClient)
70
- }
71
-
72
- /** The store's marketing tags; null when the platform does not answer, so no tag mounts. */
73
- export async function getTracking(): Promise<TrackingConfig | null> {
74
- return readTracking().catch(() => null)
75
- }
76
-
77
- async function readMenu(handle: string): Promise<Menu | null> {
78
- "use cache: remote"
79
- cacheLife("catalog")
80
- cacheTag("catalog")
81
- try {
82
- const { menu } = await getMenu(storeClient, handle)
83
- return menu
84
- } catch (error) {
85
- if (isNotFound(error)) return null
86
- throw error
87
- }
88
- }
89
-
90
- /** One menu by handle, or null: a store with no menus 404s every handle (menus.md). */
91
- export async function getMenuSafe(handle: string): Promise<Menu | null> {
92
- return readMenu(handle).catch(() => null)
93
- }
94
-
95
- /** One product by handle, priced; null for a handle the store does not have. */
96
- export async function getProduct(handle: string): Promise<StoreProduct | null> {
97
- "use cache: remote"
98
- cacheLife("catalog")
99
- cacheTag("catalog")
100
- try {
101
- const { product } = await retrieveProduct(storeClient, handle, PRICING_CONTEXT)
102
- return product
103
- } catch (error) {
104
- if (isNotFound(error)) return null
105
- throw error
106
- }
107
- }
108
-
109
- /** The platform's largest page of products. */
110
- const PAGE = 200
111
-
112
- /** Every product handle, for the product pages the build prerenders. */
113
- export async function listProductHandles(): Promise<string[]> {
114
- const handles: string[] = []
115
- for (let offset = 0; ; offset += PAGE) {
116
- const { products, count } = await listProducts(storeClient, { limit: PAGE, offset })
117
- handles.push(...products.map((product) => product.handle).filter(Boolean))
118
- if (!products.length || offset + PAGE >= count) return handles
119
- }
120
- }
1
+ import { cacheLife, cacheTag } from "next/cache"
2
+ import { getConsent, type ConsentConfig } from "@cartbase/storefront/api/consent"
3
+ import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
4
+ import { listProducts, retrieveProduct, type StoreProduct } from "@cartbase/storefront/api/products"
5
+ import { getStore, type StoreIdentity } from "@cartbase/storefront/api/store"
6
+ import { getTrackingConfig } from "@cartbase/storefront/tracking/get-tracking-config"
7
+ import type { TrackingConfig } from "@cartbase/storefront/tracking/types"
8
+ import { BARTER_CLIENT_ID, PRICING_CONTEXT } from "./config"
9
+ import { storeClient } from "./store-client"
10
+
11
+ /**
12
+ * The store's cached reads (runbook step 3, Cache Components). Each is a
13
+ * `use cache: remote` function on the `catalog` life (next.config.ts): the
14
+ * platform is asked once per window, whichever instance renders, and every
15
+ * page built from these reads is a prerendered file on the CDN.
16
+ *
17
+ * Failure contract: inside the cache a failed read THROWS, so a platform
18
+ * blip is never stored as an empty answer for the whole window; the public
19
+ * function decides what an error reads as. An unknown handle (404) is an
20
+ * answer, null, and is kept.
21
+ */
22
+
23
+ function isNotFound(error: unknown): boolean {
24
+ return (error as { status?: number } | null)?.status === 404
25
+ }
26
+
27
+ const FALLBACK_STORE: StoreIdentity = {
28
+ id: BARTER_CLIENT_ID ?? "",
29
+ name: "Store",
30
+ slug: "",
31
+ brand: {
32
+ logo_url: null,
33
+ logo_square_url: null,
34
+ color_primary: null,
35
+ color_secondary: null,
36
+ slogan: null,
37
+ },
38
+ }
39
+
40
+ async function readStore(): Promise<StoreIdentity> {
41
+ "use cache: remote"
42
+ cacheLife("catalog")
43
+ cacheTag("catalog")
44
+ const { store } = await getStore(storeClient)
45
+ return store
46
+ }
47
+
48
+ /**
49
+ * The store's own identity (store.md): name, slug, brand. A storefront never
50
+ * hardcodes its name, and a transient API error never takes the layout down.
51
+ */
52
+ export async function getStoreIdentity(): Promise<StoreIdentity> {
53
+ return readStore().catch(() => FALLBACK_STORE)
54
+ }
55
+
56
+ /** The store's consent configuration; the platform always answers a complete one. */
57
+ export async function getConsentSettings(): Promise<ConsentConfig> {
58
+ "use cache: remote"
59
+ cacheLife("catalog")
60
+ cacheTag("catalog")
61
+ const { consent } = await getConsent(storeClient)
62
+ return consent
63
+ }
64
+
65
+ async function readTracking(): Promise<TrackingConfig> {
66
+ "use cache: remote"
67
+ cacheLife("catalog")
68
+ cacheTag("catalog")
69
+ return getTrackingConfig(storeClient)
70
+ }
71
+
72
+ /** The store's marketing tags; null when the platform does not answer, so no tag mounts. */
73
+ export async function getTracking(): Promise<TrackingConfig | null> {
74
+ return readTracking().catch(() => null)
75
+ }
76
+
77
+ async function readMenu(handle: string): Promise<Menu | null> {
78
+ "use cache: remote"
79
+ cacheLife("catalog")
80
+ cacheTag("catalog")
81
+ try {
82
+ const { menu } = await getMenu(storeClient, handle)
83
+ return menu
84
+ } catch (error) {
85
+ if (isNotFound(error)) return null
86
+ throw error
87
+ }
88
+ }
89
+
90
+ /** One menu by handle, or null: a store with no menus 404s every handle (menus.md). */
91
+ export async function getMenuSafe(handle: string): Promise<Menu | null> {
92
+ return readMenu(handle).catch(() => null)
93
+ }
94
+
95
+ /** One product by handle, priced; null for a handle the store does not have. */
96
+ export async function getProduct(handle: string): Promise<StoreProduct | null> {
97
+ "use cache: remote"
98
+ cacheLife("catalog")
99
+ cacheTag("catalog")
100
+ try {
101
+ const { product } = await retrieveProduct(storeClient, handle, PRICING_CONTEXT)
102
+ return product
103
+ } catch (error) {
104
+ if (isNotFound(error)) return null
105
+ throw error
106
+ }
107
+ }
108
+
109
+ /** The platform's largest page of products. */
110
+ const PAGE = 200
111
+
112
+ /** Every product handle, for the product pages the build prerenders. */
113
+ export async function listProductHandles(): Promise<string[]> {
114
+ const handles: string[] = []
115
+ for (let offset = 0; ; offset += PAGE) {
116
+ const { products, count } = await listProducts(storeClient, { limit: PAGE, offset })
117
+ handles.push(...products.map((product) => product.handle).filter(Boolean))
118
+ if (!products.length || offset + PAGE >= count) return handles
119
+ }
120
+ }