create-brainerce-store 1.67.0 → 1.71.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/dist/index.js +22 -2
- package/messages/en.json +52 -2
- package/messages/he.json +52 -2
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +7 -0
- package/templates/nextjs/base/AGENTS.md.ejs +7 -0
- package/templates/nextjs/base/CLAUDE.md.ejs +7 -0
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +8 -2
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +16 -7
- package/templates/nextjs/base/src/app/checkout/page.tsx +1018 -1017
- package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +8 -2
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +17 -7
- package/templates/nextjs/base/src/app/register/page.tsx +67 -64
- package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
- package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
- package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
- package/templates/nextjs/base/src/core/lib/auth.ts +162 -154
- package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
- package/templates/nextjs/base/src/core/lib/site-url.ts +42 -9
- package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
- package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
- package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
- package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
- package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +127 -96
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.71.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -206,7 +206,27 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
206
206
|
// dashboard's "orders from ChatGPT/Google/…" reporting. No template code
|
|
207
207
|
// depends on it (fully automatic), but scaffolds should ship the SDK that
|
|
208
208
|
// actually reports attribution.
|
|
209
|
-
|
|
209
|
+
// 1.60 adds marketing.subscribe() — confirmed opt-in for a storefront
|
|
210
|
+
// newsletter signup. The scaffolded footer's signup form calls it, so an
|
|
211
|
+
// older SDK fails type-check at scaffold time.
|
|
212
|
+
// 1.61 adds stockAlerts.subscribe() plus two fields the scaffolded product
|
|
213
|
+
// page reads to decide whether to offer it at all —
|
|
214
|
+
// StoreInfo.stockAlertsEnabled (the merchant's switch) and
|
|
215
|
+
// InventoryInfo.backorderMode (an alert on a backorderable item is a
|
|
216
|
+
// no-op). All three are referenced by the generated back-in-stock form, so
|
|
217
|
+
// an older SDK fails type-check at scaffold time.
|
|
218
|
+
// 1.62 adds the availability clock. The scaffolded DATE/DATETIME checkout
|
|
219
|
+
// pickers pass `{ timezone }` as a third argument to isCalendarDateAllowed()/
|
|
220
|
+
// computeAvailableSlots()/getBusinessHoursForDate() — without it the
|
|
221
|
+
// leadTimeMinutes/cutoffTime/maxDaysAhead bounds are skipped and the picker
|
|
222
|
+
// offers days the API then rejects — and an older SDK types those helpers
|
|
223
|
+
// with two parameters, so the scaffold fails type-check outright.
|
|
224
|
+
// 1.63 adds customer photos on reviews. The scaffolded review form calls
|
|
225
|
+
// uploadReviewPhoto() and passes `imageKeys` on submit, reads `photos` off
|
|
226
|
+
// getMyProductReview() to decide whether to render a picker at all, and the
|
|
227
|
+
// reviews list renders `review.images`. None of those exist on an older SDK,
|
|
228
|
+
// so the scaffold fails type-check outright.
|
|
229
|
+
brainerce: "^1.63.0",
|
|
210
230
|
"isomorphic-dompurify": "^3.8.0"
|
|
211
231
|
});
|
|
212
232
|
|
package/messages/en.json
CHANGED
|
@@ -38,7 +38,28 @@
|
|
|
38
38
|
"allRightsReserved": "All rights reserved.",
|
|
39
39
|
"store": "Store",
|
|
40
40
|
"save": "Save",
|
|
41
|
-
"cancel": "Cancel"
|
|
41
|
+
"cancel": "Cancel",
|
|
42
|
+
"birthday": "Birthday",
|
|
43
|
+
"birthdayMonth": "Month",
|
|
44
|
+
"birthdayDay": "Day",
|
|
45
|
+
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
|
|
46
|
+
"birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
|
|
47
|
+
"birthdayRequired": "Choose your birthday month and day to continue.",
|
|
48
|
+
"birthdayPlaceholder": "Choose your birthday",
|
|
49
|
+
"birthdayClear": "Clear",
|
|
50
|
+
"birthdayDisplay": "{month} {day}",
|
|
51
|
+
"monthJanuary": "January",
|
|
52
|
+
"monthFebruary": "February",
|
|
53
|
+
"monthMarch": "March",
|
|
54
|
+
"monthApril": "April",
|
|
55
|
+
"monthMay": "May",
|
|
56
|
+
"monthJune": "June",
|
|
57
|
+
"monthJuly": "July",
|
|
58
|
+
"monthAugust": "August",
|
|
59
|
+
"monthSeptember": "September",
|
|
60
|
+
"monthOctober": "October",
|
|
61
|
+
"monthNovember": "November",
|
|
62
|
+
"monthDecember": "December"
|
|
42
63
|
},
|
|
43
64
|
"nav": {
|
|
44
65
|
"products": "Products",
|
|
@@ -130,7 +151,13 @@
|
|
|
130
151
|
"saving": "Saving…",
|
|
131
152
|
"submittingBtn": "Submitting…",
|
|
132
153
|
"saveChanges": "Save changes",
|
|
133
|
-
"submitReview": "Submit review"
|
|
154
|
+
"submitReview": "Submit review",
|
|
155
|
+
"addPhotos": "Add photos",
|
|
156
|
+
"removePhoto": "Remove photo",
|
|
157
|
+
"photoUploading": "Uploading…",
|
|
158
|
+
"photoTooLarge": "That photo is too large. Please pick a smaller one.",
|
|
159
|
+
"photoUploadFailed": "Could not upload that photo. Please try another.",
|
|
160
|
+
"photosNeedApproval": "Photos are checked before they appear, so yours may not show right away."
|
|
134
161
|
},
|
|
135
162
|
"customization": {
|
|
136
163
|
"title": "Personalize this product",
|
|
@@ -258,6 +285,8 @@
|
|
|
258
285
|
"customFieldsDateNextMonth": "Next month",
|
|
259
286
|
"customFieldsTimeLabel": "Time",
|
|
260
287
|
"customFieldsTimeClosed": "No delivery times are available on this date.",
|
|
288
|
+
"customFieldsTimeNoneLeft": "Every time on this date has already passed. Please pick a later date.",
|
|
289
|
+
"customFieldsTimeTo": "to",
|
|
261
290
|
"changeOptions": "Change options",
|
|
262
291
|
"surcharges": "Additional charges"
|
|
263
292
|
},
|
|
@@ -496,6 +525,27 @@
|
|
|
496
525
|
"genericError": "Something went wrong. Please try again.",
|
|
497
526
|
"fieldRequired": "{label} is required"
|
|
498
527
|
},
|
|
528
|
+
"backInStock": {
|
|
529
|
+
"title": "Email me when it's back",
|
|
530
|
+
"subtitle": "One message about this item, nothing else. You can stop it any time.",
|
|
531
|
+
"placeholder": "your@email.com",
|
|
532
|
+
"submit": "Notify me",
|
|
533
|
+
"submitting": "Sending...",
|
|
534
|
+
"doneTitle": "We'll email you when it's back",
|
|
535
|
+
"doneBody": "Stock is limited, so it may go quickly once it returns.",
|
|
536
|
+
"genericError": "Something went wrong. Please try again."
|
|
537
|
+
},
|
|
538
|
+
"newsletter": {
|
|
539
|
+
"title": "Join our newsletter",
|
|
540
|
+
"subtitle": "Offers and new arrivals, straight to your inbox.",
|
|
541
|
+
"placeholder": "your@email.com",
|
|
542
|
+
"submit": "Subscribe",
|
|
543
|
+
"submitting": "Sending...",
|
|
544
|
+
"checkEmailTitle": "Almost there — check your email",
|
|
545
|
+
"checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
|
|
546
|
+
"discountCode": "Your code: {code}",
|
|
547
|
+
"genericError": "Something went wrong. Please try again."
|
|
548
|
+
},
|
|
499
549
|
"content": {
|
|
500
550
|
"faqTitle": "Frequently Asked Questions",
|
|
501
551
|
"faqEmptyTitle": "FAQ coming soon",
|
package/messages/he.json
CHANGED
|
@@ -38,7 +38,28 @@
|
|
|
38
38
|
"allRightsReserved": "כל הזכויות שמורות.",
|
|
39
39
|
"store": "חנות",
|
|
40
40
|
"save": "שמירה",
|
|
41
|
-
"cancel": "ביטול"
|
|
41
|
+
"cancel": "ביטול",
|
|
42
|
+
"birthday": "יום הולדת",
|
|
43
|
+
"birthdayMonth": "חודש",
|
|
44
|
+
"birthdayDay": "יום",
|
|
45
|
+
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו.",
|
|
46
|
+
"birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
|
|
47
|
+
"birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
|
|
48
|
+
"birthdayPlaceholder": "בחרו את יום ההולדת",
|
|
49
|
+
"birthdayClear": "ניקוי",
|
|
50
|
+
"birthdayDisplay": "{day} ב{month}",
|
|
51
|
+
"monthJanuary": "ינואר",
|
|
52
|
+
"monthFebruary": "פברואר",
|
|
53
|
+
"monthMarch": "מרץ",
|
|
54
|
+
"monthApril": "אפריל",
|
|
55
|
+
"monthMay": "מאי",
|
|
56
|
+
"monthJune": "יוני",
|
|
57
|
+
"monthJuly": "יולי",
|
|
58
|
+
"monthAugust": "אוגוסט",
|
|
59
|
+
"monthSeptember": "ספטמבר",
|
|
60
|
+
"monthOctober": "אוקטובר",
|
|
61
|
+
"monthNovember": "נובמבר",
|
|
62
|
+
"monthDecember": "דצמבר"
|
|
42
63
|
},
|
|
43
64
|
"nav": {
|
|
44
65
|
"products": "מוצרים",
|
|
@@ -130,7 +151,13 @@
|
|
|
130
151
|
"saving": "שומר…",
|
|
131
152
|
"submittingBtn": "שולח…",
|
|
132
153
|
"saveChanges": "שמירת שינויים",
|
|
133
|
-
"submitReview": "שליחת ביקורת"
|
|
154
|
+
"submitReview": "שליחת ביקורת",
|
|
155
|
+
"addPhotos": "הוספת תמונות",
|
|
156
|
+
"removePhoto": "הסרת תמונה",
|
|
157
|
+
"photoUploading": "מעלה...",
|
|
158
|
+
"photoTooLarge": "התמונה הזו גדולה מדי. בחרו תמונה קטנה יותר.",
|
|
159
|
+
"photoUploadFailed": "לא הצלחנו להעלות את התמונה. נסו תמונה אחרת.",
|
|
160
|
+
"photosNeedApproval": "התמונות נבדקות לפני שהן מופיעות, כך שייתכן שהתמונה שלכם לא תוצג מיד."
|
|
134
161
|
},
|
|
135
162
|
"customization": {
|
|
136
163
|
"title": "התאמה אישית של המוצר",
|
|
@@ -258,6 +285,8 @@
|
|
|
258
285
|
"customFieldsDateNextMonth": "חודש הבא",
|
|
259
286
|
"customFieldsTimeLabel": "שעה",
|
|
260
287
|
"customFieldsTimeClosed": "אין שעות משלוח זמינות בתאריך הזה.",
|
|
288
|
+
"customFieldsTimeNoneLeft": "כל השעות בתאריך הזה כבר עברו. בחרו תאריך מאוחר יותר.",
|
|
289
|
+
"customFieldsTimeTo": "עד",
|
|
261
290
|
"changeOptions": "שנה אפשרויות",
|
|
262
291
|
"surcharges": "תוספות"
|
|
263
292
|
},
|
|
@@ -496,6 +525,27 @@
|
|
|
496
525
|
"genericError": "משהו השתבש. אנא נסה שוב.",
|
|
497
526
|
"fieldRequired": "השדה {label} הוא חובה"
|
|
498
527
|
},
|
|
528
|
+
"backInStock": {
|
|
529
|
+
"title": "עדכנו אותי כשחוזר למלאי",
|
|
530
|
+
"subtitle": "הודעה אחת על הפריט הזה, ותו לא. אפשר לבטל בכל רגע.",
|
|
531
|
+
"placeholder": "your@email.com",
|
|
532
|
+
"submit": "עדכנו אותי",
|
|
533
|
+
"submitting": "שולח...",
|
|
534
|
+
"doneTitle": "נעדכן אותך כשהפריט יחזור",
|
|
535
|
+
"doneBody": "הכמות מוגבלת, אז ייתכן שהפריט ייגמר מהר.",
|
|
536
|
+
"genericError": "משהו השתבש. אפשר לנסות שוב."
|
|
537
|
+
},
|
|
538
|
+
"newsletter": {
|
|
539
|
+
"title": "הצטרפו לניוזלטר",
|
|
540
|
+
"subtitle": "מבצעים ומוצרים חדשים, ישירות למייל.",
|
|
541
|
+
"placeholder": "your@email.com",
|
|
542
|
+
"submit": "הרשמה",
|
|
543
|
+
"submitting": "שולח...",
|
|
544
|
+
"checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
|
|
545
|
+
"checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
|
|
546
|
+
"discountCode": "הקוד שלכם: {code}",
|
|
547
|
+
"genericError": "משהו השתבש. נסו שוב."
|
|
548
|
+
},
|
|
499
549
|
"content": {
|
|
500
550
|
"faqTitle": "שאלות נפוצות",
|
|
501
551
|
"faqEmptyTitle": "השאלות הנפוצות בדרך",
|
package/package.json
CHANGED
|
@@ -38,6 +38,13 @@ NEXT_PUBLIC_BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
|
|
|
38
38
|
# Set it in your hosting provider's environment variables, not in this file:
|
|
39
39
|
# .env.local is gitignored and never travels with a deploy.
|
|
40
40
|
# `NEXT_PUBLIC_SITE_URL` is still read as a backwards-compatible alias.
|
|
41
|
+
#
|
|
42
|
+
# On some hosts SITE_URL is NOT optional. Platforms whose proxy forwards
|
|
43
|
+
# requests to the app as `Host: localhost:<port>` (OpenAI Sites /
|
|
44
|
+
# *.chatgpt.site, among others) hide the real hostname from the server, so
|
|
45
|
+
# nothing can be resolved from the request. Set SITE_URL there, or server-side
|
|
46
|
+
# API calls send `Origin: http://localhost:3000` and a sales channel with a
|
|
47
|
+
# configured Domain rejects them with 403.
|
|
41
48
|
|
|
42
49
|
# Optional: floating WhatsApp contact button (used by some designs, e.g.
|
|
43
50
|
# passepartout). Digits only, international format without "+"
|
|
@@ -81,6 +81,13 @@ development and on any host with nothing configured.
|
|
|
81
81
|
`getRequestOrigin()` (for the `Origin` header sent to Brainerce). Hand-rolled
|
|
82
82
|
versions have shipped `http://` on HTTPS hosts and internal container
|
|
83
83
|
hostnames.
|
|
84
|
+
- **Some hosts hide the real hostname from the app.** Platforms whose proxy
|
|
85
|
+
forwards requests as `Host: localhost:<port>` with no `x-forwarded-host`
|
|
86
|
+
(OpenAI Sites / `*.chatgpt.site` does this) leave the resolver blind — the
|
|
87
|
+
request looks like it arrived on localhost. On such platforms `SITE_URL` is
|
|
88
|
+
not optional: set it, and the resolver prefers it over any internal host it
|
|
89
|
+
sees. Without it, server-side API calls send `Origin: http://localhost:3000`
|
|
90
|
+
and a channel with a configured Domain rejects them with 403.
|
|
84
91
|
- **When the merchant gives you a real domain**, set `SITE_URL` in the hosting
|
|
85
92
|
provider's environment variables — not in `.env.local`, which is gitignored
|
|
86
93
|
and never travels with a deploy.
|
|
@@ -92,6 +92,13 @@ development and on any host with nothing configured.
|
|
|
92
92
|
`getRequestOrigin()` (for the `Origin` header sent to Brainerce). Hand-rolled
|
|
93
93
|
versions have shipped `http://` on HTTPS hosts and internal container
|
|
94
94
|
hostnames.
|
|
95
|
+
- **Some hosts hide the real hostname from the app.** Platforms whose proxy
|
|
96
|
+
forwards requests as `Host: localhost:<port>` with no `x-forwarded-host`
|
|
97
|
+
(OpenAI Sites / `*.chatgpt.site` does this) leave the resolver blind — the
|
|
98
|
+
request looks like it arrived on localhost. On such platforms `SITE_URL` is
|
|
99
|
+
not optional: set it, and the resolver prefers it over any internal host it
|
|
100
|
+
sees. Without it, server-side API calls send `Origin: http://localhost:3000`
|
|
101
|
+
and a channel with a configured Domain rejects them with 403.
|
|
95
102
|
- **When the merchant gives you a real domain**, set `SITE_URL` in the hosting
|
|
96
103
|
provider's environment variables — not in `.env.local`, which is gitignored
|
|
97
104
|
and never travels with a deploy.
|
|
@@ -83,7 +83,10 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|
|
83
83
|
const slug = decodeSlug(rawSlug);
|
|
84
84
|
const client = await getServerClient(locale);
|
|
85
85
|
const [post, storeInfo] = await Promise.all([
|
|
86
|
-
|
|
86
|
+
// getPost returns null only on a genuine 404; any other API failure (a
|
|
87
|
+
// 403 from the channel's origin check, a network error) throws and
|
|
88
|
+
// surfaces as the error it is instead of masquerading as a missing post.
|
|
89
|
+
client.blog.getPost(slug),
|
|
87
90
|
client.getStoreInfo().catch(() => null),
|
|
88
91
|
]);
|
|
89
92
|
if (!post) {
|
|
@@ -220,7 +223,10 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|
|
220
223
|
const slug = decodeSlug(rawSlug);
|
|
221
224
|
const client = await getServerClient();
|
|
222
225
|
const [post, storeInfo] = await Promise.all([
|
|
223
|
-
|
|
226
|
+
// getPost returns null only on a genuine 404; any other API failure (a
|
|
227
|
+
// 403 from the channel's origin check, a network error) throws and
|
|
228
|
+
// surfaces as the error it is instead of masquerading as a missing post.
|
|
229
|
+
client.blog.getPost(slug),
|
|
224
230
|
client.getStoreInfo().catch(() => null),
|
|
225
231
|
]);
|
|
226
232
|
if (!post) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
|
-
import
|
|
3
|
+
import { BrainerceError } from 'brainerce';
|
|
4
4
|
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
5
5
|
import { buildMetaDescription } from '@/core/lib/seo';
|
|
6
6
|
import { sanitizeHtml } from '@/core/lib/sanitize';
|
|
@@ -77,8 +77,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
77
77
|
images: category.image ? [category.image] : [],
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
|
-
} catch {
|
|
81
|
-
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof BrainerceError && err.statusCode === 404) {
|
|
82
|
+
return { title: 'Category not found' };
|
|
83
|
+
}
|
|
84
|
+
// Anything else (channel origin rejection, network failure) hits the page
|
|
85
|
+
// render below too and surfaces there — don't stamp a misleading title.
|
|
86
|
+
return {};
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -87,14 +92,18 @@ export default async function CategoryPage({ params }: Props) {
|
|
|
87
92
|
const slug = decodeSlug(rawSlug);
|
|
88
93
|
const client = await getServerClient(locale);
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
// Only a genuine 404 becomes a storefront 404; a 403 from the channel's
|
|
96
|
+
// origin check or a network failure must surface as the error it is, not
|
|
97
|
+
// masquerade as a missing category (or an empty one).
|
|
98
|
+
const category = await client.getCategoryBySlug(slug).catch((err: unknown) => {
|
|
99
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
100
|
+
throw err;
|
|
101
|
+
});
|
|
91
102
|
if (!category) notFound();
|
|
92
103
|
|
|
93
104
|
// The listing endpoint owns pagination/FX/publish gating — reuse it rather
|
|
94
105
|
// than duplicating a product query here.
|
|
95
|
-
const { data: products } = await client
|
|
96
|
-
.getProducts({ categories: [category.id], limit: 48 })
|
|
97
|
-
.catch(() => ({ data: [] as Product[] }));
|
|
106
|
+
const { data: products } = await client.getProducts({ categories: [category.id], limit: 48 });
|
|
98
107
|
|
|
99
108
|
const baseUrl = await getCanonicalSiteUrl();
|
|
100
109
|
const url = `${baseUrl}/category/${category.slug || slug}`;
|