@cartbase/storefront 0.19.0 → 0.20.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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
@@ -1,130 +1,130 @@
1
- /**
2
- * @cartbase/storefront/api/collections — collections + membership listings.
3
- *
4
- * Collections are the merchant's curated product groupings
5
- * (collections-upgrade card): manual or smart (`conditions`/`match`), with a
6
- * per-collection `default_sort` the membership listing honors and an
7
- * optional per-request `order` override. Docs:
8
- * docs/storefront/collections.md.
9
- */
10
- import type { StorefrontClient } from "./http"
11
- import type { IsoDateString, ListEnvelope, PaginationQuery, PricingContextQuery } from "./types"
12
- import type { StoreProduct } from "./products"
13
-
14
- /** Sort modes accepted by `default_sort` and the `order` override. */
15
- export type CollectionSortMode =
16
- | "manual"
17
- | "title_asc"
18
- | "title_desc"
19
- | "price_asc"
20
- | "price_desc"
21
- | "newest"
22
- | "oldest"
23
- | "best_selling"
24
-
25
- export interface StoreCollection {
26
- id: string
27
- title: string
28
- handle: string
29
- /** "manual" | "smart" (smart membership is computed from `conditions`). */
30
- type: string
31
- description: string | null
32
- image_url: string | null
33
- /** The sort the membership listing uses when no `order` override is sent. */
34
- default_sort: string
35
- /** Smart-collection condition rows (admin-authored; opaque to render). */
36
- conditions: unknown
37
- /** Smart matching: "all" | "any". */
38
- match: string
39
- /** SEO override; null = fall back to `title`. */
40
- seo_title: string | null
41
- /** SEO override; null = fall back to `description`. */
42
- seo_description: string | null
43
- metadata: Record<string, unknown> | null
44
- created_at: IsoDateString
45
- updated_at: IsoDateString
46
- }
47
-
48
- export interface ListCollectionsQuery extends PaginationQuery {
49
- /** Case-insensitive substring match on title. */
50
- q?: string
51
- handle?: string
52
- /**
53
- * Channel scope: a collection with channel links is visible ONLY on those
54
- * channels (excluded from other channels' lists); a collection with no
55
- * links is visible everywhere.
56
- */
57
- sales_channel_id?: string
58
- }
59
-
60
- export interface CollectionListResponse extends ListEnvelope {
61
- collections: StoreCollection[]
62
- }
63
-
64
- export interface CollectionResponse {
65
- collection: StoreCollection
66
- }
67
-
68
- export interface ListCollectionProductsQuery extends PricingContextQuery, PaginationQuery {
69
- /** Sort override — any CollectionSortMode; unknown values are ignored. */
70
- order?: CollectionSortMode | string
71
- /** Channel scope: a collection scoped to OTHER channels 404s here. */
72
- sales_channel_id?: string
73
- }
74
-
75
- export interface CollectionProductsResponse extends ListEnvelope {
76
- products: StoreProduct[]
77
- }
78
-
79
- /**
80
- * List collections, ordered by title.
81
- *
82
- * Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
83
- * 400 `validation_failed`. Settings: collection channel links
84
- * (`sales_channel_id` scope), smart-collection conditions.
85
- */
86
- export async function listCollections(
87
- client: StorefrontClient,
88
- query?: ListCollectionsQuery
89
- ): Promise<CollectionListResponse> {
90
- return client.get("/api/store/collections", { query: { ...query } })
91
- }
92
-
93
- /**
94
- * Retrieve one collection by id. (Look up by handle via
95
- * `listCollections({ handle })`.)
96
- *
97
- * Auth: anon (`x-client-id`). Errors: 404 `not_found`. NOTE: the single read
98
- * takes no `sales_channel_id` — channel scope applies to the list and to the
99
- * membership listing, not here.
100
- */
101
- export async function retrieveCollection(
102
- client: StorefrontClient,
103
- collectionId: string
104
- ): Promise<CollectionResponse> {
105
- return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}`)
106
- }
107
-
108
- /**
109
- * List a collection's products — the MEMBERSHIP join (multi-collection
110
- * products appear in every collection they belong to), published products
111
- * only, ordered by the collection's `default_sort` unless `order` overrides
112
- * (manual position, title, EUR price asc/desc, newest/oldest, best_selling
113
- * 90-day aggregate). Products carry the full `StoreProduct` shape incl.
114
- * `calculated_price` when a pricing context is given.
115
- *
116
- * Auth: anon (`x-client-id`); optional Bearer JWT for group-aware pricing.
117
- * Errors: 404 `not_found` (unknown collection, or scoped away from
118
- * `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
119
- * `limit` 1–100 (default 50). Settings: collection default_sort + manual
120
- * order, channel links, price lists.
121
- */
122
- export async function listCollectionProducts(
123
- client: StorefrontClient,
124
- collectionId: string,
125
- query?: ListCollectionProductsQuery
126
- ): Promise<CollectionProductsResponse> {
127
- return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}/products`, {
128
- query: { ...query },
129
- })
130
- }
1
+ /**
2
+ * @cartbase/storefront/api/collections — collections + membership listings.
3
+ *
4
+ * Collections are the merchant's curated product groupings
5
+ * (collections-upgrade card): manual or smart (`conditions`/`match`), with a
6
+ * per-collection `default_sort` the membership listing honors and an
7
+ * optional per-request `order` override. Docs:
8
+ * docs/storefront/collections.md.
9
+ */
10
+ import type { StorefrontClient } from "./http"
11
+ import type { IsoDateString, ListEnvelope, PaginationQuery, PricingContextQuery } from "./types"
12
+ import type { StoreProduct } from "./products"
13
+
14
+ /** Sort modes accepted by `default_sort` and the `order` override. */
15
+ export type CollectionSortMode =
16
+ | "manual"
17
+ | "title_asc"
18
+ | "title_desc"
19
+ | "price_asc"
20
+ | "price_desc"
21
+ | "newest"
22
+ | "oldest"
23
+ | "best_selling"
24
+
25
+ export interface StoreCollection {
26
+ id: string
27
+ title: string
28
+ handle: string
29
+ /** "manual" | "smart" (smart membership is computed from `conditions`). */
30
+ type: string
31
+ description: string | null
32
+ image_url: string | null
33
+ /** The sort the membership listing uses when no `order` override is sent. */
34
+ default_sort: string
35
+ /** Smart-collection condition rows (admin-authored; opaque to render). */
36
+ conditions: unknown
37
+ /** Smart matching: "all" | "any". */
38
+ match: string
39
+ /** SEO override; null = fall back to `title`. */
40
+ seo_title: string | null
41
+ /** SEO override; null = fall back to `description`. */
42
+ seo_description: string | null
43
+ metadata: Record<string, unknown> | null
44
+ created_at: IsoDateString
45
+ updated_at: IsoDateString
46
+ }
47
+
48
+ export interface ListCollectionsQuery extends PaginationQuery {
49
+ /** Case-insensitive substring match on title. */
50
+ q?: string
51
+ handle?: string
52
+ /**
53
+ * Channel scope: a collection with channel links is visible ONLY on those
54
+ * channels (excluded from other channels' lists); a collection with no
55
+ * links is visible everywhere.
56
+ */
57
+ sales_channel_id?: string
58
+ }
59
+
60
+ export interface CollectionListResponse extends ListEnvelope {
61
+ collections: StoreCollection[]
62
+ }
63
+
64
+ export interface CollectionResponse {
65
+ collection: StoreCollection
66
+ }
67
+
68
+ export interface ListCollectionProductsQuery extends PricingContextQuery, PaginationQuery {
69
+ /** Sort override — any CollectionSortMode; unknown values are ignored. */
70
+ order?: CollectionSortMode | string
71
+ /** Channel scope: a collection scoped to OTHER channels 404s here. */
72
+ sales_channel_id?: string
73
+ }
74
+
75
+ export interface CollectionProductsResponse extends ListEnvelope {
76
+ products: StoreProduct[]
77
+ }
78
+
79
+ /**
80
+ * List collections, ordered by title.
81
+ *
82
+ * Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
83
+ * 400 `validation_failed`. Settings: collection channel links
84
+ * (`sales_channel_id` scope), smart-collection conditions.
85
+ */
86
+ export async function listCollections(
87
+ client: StorefrontClient,
88
+ query?: ListCollectionsQuery
89
+ ): Promise<CollectionListResponse> {
90
+ return client.get("/api/store/collections", { query: { ...query } })
91
+ }
92
+
93
+ /**
94
+ * Retrieve one collection by id. (Look up by handle via
95
+ * `listCollections({ handle })`.)
96
+ *
97
+ * Auth: anon (`x-client-id`). Errors: 404 `not_found`. NOTE: the single read
98
+ * takes no `sales_channel_id` — channel scope applies to the list and to the
99
+ * membership listing, not here.
100
+ */
101
+ export async function retrieveCollection(
102
+ client: StorefrontClient,
103
+ collectionId: string
104
+ ): Promise<CollectionResponse> {
105
+ return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}`)
106
+ }
107
+
108
+ /**
109
+ * List a collection's products — the MEMBERSHIP join (multi-collection
110
+ * products appear in every collection they belong to), published products
111
+ * only, ordered by the collection's `default_sort` unless `order` overrides
112
+ * (manual position, title, EUR price asc/desc, newest/oldest, best_selling
113
+ * 90-day aggregate). Products carry the full `StoreProduct` shape incl.
114
+ * `calculated_price` when a pricing context is given.
115
+ *
116
+ * Auth: anon (`x-client-id`); optional Bearer JWT for group-aware pricing.
117
+ * Errors: 404 `not_found` (unknown collection, or scoped away from
118
+ * `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
119
+ * `limit` 1–100 (default 50). Settings: collection default_sort + manual
120
+ * order, channel links, price lists.
121
+ */
122
+ export async function listCollectionProducts(
123
+ client: StorefrontClient,
124
+ collectionId: string,
125
+ query?: ListCollectionProductsQuery
126
+ ): Promise<CollectionProductsResponse> {
127
+ return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}/products`, {
128
+ query: { ...query },
129
+ })
130
+ }
@@ -1,75 +1,75 @@
1
- /**
2
- * @cartbase/storefront/api/consent — the store's CMP configuration for the
3
- * built-in Consent Mode v2 banner (consent-management card).
4
- *
5
- * Ground truth: src/app/api/store/consent/route.ts +
6
- * src/lib/consent/model.ts (publicConsentPayload — exact allowlist).
7
- *
8
- * WIRING TRAP (the one that matters): the synchronous Consent Mode v2
9
- * DEFAULT (`<ConsentInit>`, first child of <body>) must NEVER wait on this
10
- * fetch — resolve the config server-side (RSC) and inline it. Render the
11
- * built-in banner only when `enabled && mode === "builtin"`. In `external`
12
- * mode the merchant's CMP owns the UI and must write the same `_1c_consent`
13
- * cookie (or call `setConsent()`) — all Cartbase-side tag gating works off
14
- * that one seam. Choices persist 12 months.
15
- */
16
-
17
- import type { StorefrontClient } from "./http"
18
-
19
- /** Per-locale banner copy — every field present (defaults merged server-side). */
20
- export interface ConsentCopy {
21
- // Layer 1
22
- title: string
23
- body: string
24
- privacy_link_label: string
25
- accept_label: string
26
- settings_label: string
27
- reject_label: string
28
- // Layer 2 (preferences)
29
- settings_title: string
30
- accept_all_label: string
31
- save_label: string
32
- reject_all_label: string
33
- necessary_label: string
34
- necessary_description: string
35
- analytics_label: string
36
- analytics_description: string
37
- ads_label: string
38
- ads_description: string
39
- }
40
-
41
- /**
42
- * The EXACT public payload — an allowlist; nothing else will ever appear.
43
- * Defaults are ALWAYS applied server-side, so this is complete and
44
- * renderable even for an unconfigured store (BG + EN copy shipped).
45
- */
46
- export interface ConsentConfig {
47
- enabled: boolean
48
- /** `builtin` = Cartbase renders the banner; `external` = merchant CMP owns UI. */
49
- mode: "builtin" | "external"
50
- /** `modal` = blocking centered modal; `banner-bottom` = non-blocking bar. */
51
- layout: "modal" | "banner-bottom"
52
- /** Storefront-relative privacy/cookies page link. */
53
- privacy_href: string
54
- /** Show a reject-all button on the first layer. */
55
- reject_on_first_layer: boolean
56
- /** Keyed by locale code (at least `bg` + `en`). */
57
- copy: Record<string, ConsentCopy>
58
- }
59
-
60
- export interface ConsentResponse {
61
- consent: ConsentConfig
62
- }
63
-
64
- /**
65
- * GET /api/store/consent — the complete, renderable consent configuration.
66
- *
67
- * Auth: anon (x-client-id).
68
- * Errors: 400 missing_client_id only — the payload always succeeds
69
- * (corrupt/missing config degrades to defaults, never to a broken banner).
70
- * Settings: admin → Settings → Consent (enabled/mode/layout/copy);
71
- * `integrations.tracking.consent_required` mirrors `enabled`.
72
- */
73
- export async function getConsent(client: StorefrontClient): Promise<ConsentResponse> {
74
- return client.get("/api/store/consent")
75
- }
1
+ /**
2
+ * @cartbase/storefront/api/consent — the store's CMP configuration for the
3
+ * built-in Consent Mode v2 banner (consent-management card).
4
+ *
5
+ * Ground truth: src/app/api/store/consent/route.ts +
6
+ * src/lib/consent/model.ts (publicConsentPayload — exact allowlist).
7
+ *
8
+ * WIRING TRAP (the one that matters): the synchronous Consent Mode v2
9
+ * DEFAULT (`<ConsentInit>`, first child of <body>) must NEVER wait on this
10
+ * fetch — resolve the config server-side (RSC) and inline it. Render the
11
+ * built-in banner only when `enabled && mode === "builtin"`. In `external`
12
+ * mode the merchant's CMP owns the UI and must write the same `_1c_consent`
13
+ * cookie (or call `setConsent()`) — all Cartbase-side tag gating works off
14
+ * that one seam. Choices persist 12 months.
15
+ */
16
+
17
+ import type { StorefrontClient } from "./http"
18
+
19
+ /** Per-locale banner copy — every field present (defaults merged server-side). */
20
+ export interface ConsentCopy {
21
+ // Layer 1
22
+ title: string
23
+ body: string
24
+ privacy_link_label: string
25
+ accept_label: string
26
+ settings_label: string
27
+ reject_label: string
28
+ // Layer 2 (preferences)
29
+ settings_title: string
30
+ accept_all_label: string
31
+ save_label: string
32
+ reject_all_label: string
33
+ necessary_label: string
34
+ necessary_description: string
35
+ analytics_label: string
36
+ analytics_description: string
37
+ ads_label: string
38
+ ads_description: string
39
+ }
40
+
41
+ /**
42
+ * The EXACT public payload — an allowlist; nothing else will ever appear.
43
+ * Defaults are ALWAYS applied server-side, so this is complete and
44
+ * renderable even for an unconfigured store (BG + EN copy shipped).
45
+ */
46
+ export interface ConsentConfig {
47
+ enabled: boolean
48
+ /** `builtin` = Cartbase renders the banner; `external` = merchant CMP owns UI. */
49
+ mode: "builtin" | "external"
50
+ /** `modal` = blocking centered modal; `banner-bottom` = non-blocking bar. */
51
+ layout: "modal" | "banner-bottom"
52
+ /** Storefront-relative privacy/cookies page link. */
53
+ privacy_href: string
54
+ /** Show a reject-all button on the first layer. */
55
+ reject_on_first_layer: boolean
56
+ /** Keyed by locale code (at least `bg` + `en`). */
57
+ copy: Record<string, ConsentCopy>
58
+ }
59
+
60
+ export interface ConsentResponse {
61
+ consent: ConsentConfig
62
+ }
63
+
64
+ /**
65
+ * GET /api/store/consent — the complete, renderable consent configuration.
66
+ *
67
+ * Auth: anon (x-client-id).
68
+ * Errors: 400 missing_client_id only — the payload always succeeds
69
+ * (corrupt/missing config degrades to defaults, never to a broken banner).
70
+ * Settings: admin → Settings → Consent (enabled/mode/layout/copy);
71
+ * `integrations.tracking.consent_required` mirrors `enabled`.
72
+ */
73
+ export async function getConsent(client: StorefrontClient): Promise<ConsentResponse> {
74
+ return client.get("/api/store/consent")
75
+ }
@@ -1,125 +1,125 @@
1
- /**
2
- * @cartbase/storefront/api/content — pages + blogs (content-pages-blog card).
3
- *
4
- * Ground truth: src/app/api/store/pages/[handle]/route.ts,
5
- * src/app/api/store/blogs/[handle]/posts{,/[postHandle]}/route.ts,
6
- * src/lib/content/article-shape.ts.
7
- *
8
- * Shapes are Shopify Storefront `Page` / `Article` verbatim so storefront
9
- * code ports 1:1. PUBLISHED only — drafts and deleted rows 404 (lib filter
10
- * AND anon RLS). Stored HTML is server-sanitized on write — safe to render
11
- * raw (`dangerouslySetInnerHTML`).
12
- *
13
- * Path convention (src/lib/content/paths.ts, shared with menus/redirects):
14
- * `/pages/<handle>`, `/blogs/<handle>`, `/blogs/<blog>/<post>`.
15
- */
16
-
17
- import type { StorefrontClient } from "./http"
18
- import type { IsoDateString, PaginationQuery } from "./types"
19
-
20
- /** Shopify Storefront `Page`, verbatim. */
21
- export interface Page {
22
- /** `cpage_<hex>` */
23
- id: string
24
- handle: string
25
- title: string
26
- /** Sanitized HTML — safe to render raw. */
27
- body: string
28
- /** Stripped text, ~160-char word-boundary truncation. */
29
- bodySummary: string
30
- /** Fallbacks applied server-side: title / bodySummary. */
31
- seo: { title: string; description: string }
32
- publishedAt: IsoDateString
33
- createdAt: IsoDateString
34
- updatedAt: IsoDateString
35
- }
36
-
37
- export interface PageResponse {
38
- page: Page
39
- }
40
-
41
- /**
42
- * GET /api/store/pages/:handle — one published page.
43
- *
44
- * Auth: anon (x-client-id).
45
- * Errors: 404 not_found (unknown handle, draft, or deleted).
46
- * Settings: policy pages (privacy-policy, terms-of-service, refund-policy,
47
- * shipping-policy) are seeded as DRAFTS — they 404 until published.
48
- */
49
- export async function getPage(
50
- client: StorefrontClient,
51
- handle: string
52
- ): Promise<PageResponse> {
53
- return client.get(`/api/store/pages/${encodeURIComponent(handle)}`)
54
- }
55
-
56
- /** Shopify Storefront `Article`, verbatim. */
57
- export interface Article {
58
- /** `bpost_<hex>` */
59
- id: string
60
- handle: string
61
- title: string
62
- /** Sanitized HTML — safe to render raw. */
63
- contentHtml: string
64
- /** Plain-text version of contentHtml. */
65
- content: string
66
- /** Author-provided; falls back to truncated content. */
67
- excerpt: string
68
- /** null when the post has no hero image. altText is always null today. */
69
- image: { url: string; altText: null } | null
70
- tags: string[]
71
- /** null when the post has no author name set. */
72
- author: { name: string } | null
73
- publishedAt: IsoDateString
74
- /** Fallbacks applied server-side: title / excerpt. */
75
- seo: { title: string; description: string }
76
- blog: { handle: string; title: string }
77
- }
78
-
79
- export interface ArticleResponse {
80
- article: Article
81
- }
82
-
83
- export type ListBlogPostsQuery = PaginationQuery
84
-
85
- export interface BlogPostListResponse {
86
- blog: { handle: string; title: string }
87
- articles: Article[]
88
- count: number
89
- offset: number
90
- limit: number
91
- }
92
-
93
- /**
94
- * GET /api/store/blogs/:handle/posts — published posts of one blog,
95
- * `published_at` desc. `limit` ≤ 100 (default 20), `offset` ≥ 0.
96
- *
97
- * Auth: anon (x-client-id).
98
- * Errors: 404 not_found (unknown blog handle) · 400 validation_failed
99
- * (limit/offset out of range).
100
- */
101
- export async function listBlogPosts(
102
- client: StorefrontClient,
103
- blogHandle: string,
104
- query?: ListBlogPostsQuery
105
- ): Promise<BlogPostListResponse> {
106
- return client.get(`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts`, {
107
- query: { ...query },
108
- })
109
- }
110
-
111
- /**
112
- * GET /api/store/blogs/:handle/posts/:postHandle — one published article.
113
- *
114
- * Auth: anon (x-client-id).
115
- * Errors: 404 not_found (unknown blog OR unknown/draft post).
116
- */
117
- export async function getBlogPost(
118
- client: StorefrontClient,
119
- blogHandle: string,
120
- postHandle: string
121
- ): Promise<ArticleResponse> {
122
- return client.get(
123
- `/api/store/blogs/${encodeURIComponent(blogHandle)}/posts/${encodeURIComponent(postHandle)}`
124
- )
125
- }
1
+ /**
2
+ * @cartbase/storefront/api/content — pages + blogs (content-pages-blog card).
3
+ *
4
+ * Ground truth: src/app/api/store/pages/[handle]/route.ts,
5
+ * src/app/api/store/blogs/[handle]/posts{,/[postHandle]}/route.ts,
6
+ * src/lib/content/article-shape.ts.
7
+ *
8
+ * Shapes are Shopify Storefront `Page` / `Article` verbatim so storefront
9
+ * code ports 1:1. PUBLISHED only — drafts and deleted rows 404 (lib filter
10
+ * AND anon RLS). Stored HTML is server-sanitized on write — safe to render
11
+ * raw (`dangerouslySetInnerHTML`).
12
+ *
13
+ * Path convention (src/lib/content/paths.ts, shared with menus/redirects):
14
+ * `/pages/<handle>`, `/blogs/<handle>`, `/blogs/<blog>/<post>`.
15
+ */
16
+
17
+ import type { StorefrontClient } from "./http"
18
+ import type { IsoDateString, PaginationQuery } from "./types"
19
+
20
+ /** Shopify Storefront `Page`, verbatim. */
21
+ export interface Page {
22
+ /** `cpage_<hex>` */
23
+ id: string
24
+ handle: string
25
+ title: string
26
+ /** Sanitized HTML — safe to render raw. */
27
+ body: string
28
+ /** Stripped text, ~160-char word-boundary truncation. */
29
+ bodySummary: string
30
+ /** Fallbacks applied server-side: title / bodySummary. */
31
+ seo: { title: string; description: string }
32
+ publishedAt: IsoDateString
33
+ createdAt: IsoDateString
34
+ updatedAt: IsoDateString
35
+ }
36
+
37
+ export interface PageResponse {
38
+ page: Page
39
+ }
40
+
41
+ /**
42
+ * GET /api/store/pages/:handle — one published page.
43
+ *
44
+ * Auth: anon (x-client-id).
45
+ * Errors: 404 not_found (unknown handle, draft, or deleted).
46
+ * Settings: policy pages (privacy-policy, terms-of-service, refund-policy,
47
+ * shipping-policy) are seeded as DRAFTS — they 404 until published.
48
+ */
49
+ export async function getPage(
50
+ client: StorefrontClient,
51
+ handle: string
52
+ ): Promise<PageResponse> {
53
+ return client.get(`/api/store/pages/${encodeURIComponent(handle)}`)
54
+ }
55
+
56
+ /** Shopify Storefront `Article`, verbatim. */
57
+ export interface Article {
58
+ /** `bpost_<hex>` */
59
+ id: string
60
+ handle: string
61
+ title: string
62
+ /** Sanitized HTML — safe to render raw. */
63
+ contentHtml: string
64
+ /** Plain-text version of contentHtml. */
65
+ content: string
66
+ /** Author-provided; falls back to truncated content. */
67
+ excerpt: string
68
+ /** null when the post has no hero image. altText is always null today. */
69
+ image: { url: string; altText: null } | null
70
+ tags: string[]
71
+ /** null when the post has no author name set. */
72
+ author: { name: string } | null
73
+ publishedAt: IsoDateString
74
+ /** Fallbacks applied server-side: title / excerpt. */
75
+ seo: { title: string; description: string }
76
+ blog: { handle: string; title: string }
77
+ }
78
+
79
+ export interface ArticleResponse {
80
+ article: Article
81
+ }
82
+
83
+ export type ListBlogPostsQuery = PaginationQuery
84
+
85
+ export interface BlogPostListResponse {
86
+ blog: { handle: string; title: string }
87
+ articles: Article[]
88
+ count: number
89
+ offset: number
90
+ limit: number
91
+ }
92
+
93
+ /**
94
+ * GET /api/store/blogs/:handle/posts — published posts of one blog,
95
+ * `published_at` desc. `limit` ≤ 100 (default 20), `offset` ≥ 0.
96
+ *
97
+ * Auth: anon (x-client-id).
98
+ * Errors: 404 not_found (unknown blog handle) · 400 validation_failed
99
+ * (limit/offset out of range).
100
+ */
101
+ export async function listBlogPosts(
102
+ client: StorefrontClient,
103
+ blogHandle: string,
104
+ query?: ListBlogPostsQuery
105
+ ): Promise<BlogPostListResponse> {
106
+ return client.get(`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts`, {
107
+ query: { ...query },
108
+ })
109
+ }
110
+
111
+ /**
112
+ * GET /api/store/blogs/:handle/posts/:postHandle — one published article.
113
+ *
114
+ * Auth: anon (x-client-id).
115
+ * Errors: 404 not_found (unknown blog OR unknown/draft post).
116
+ */
117
+ export async function getBlogPost(
118
+ client: StorefrontClient,
119
+ blogHandle: string,
120
+ postHandle: string
121
+ ): Promise<ArticleResponse> {
122
+ return client.get(
123
+ `/api/store/blogs/${encodeURIComponent(blogHandle)}/posts/${encodeURIComponent(postHandle)}`
124
+ )
125
+ }