create-brainerce-store 1.65.0 → 1.67.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.
Files changed (49) hide show
  1. package/dist/index.js +2 -2
  2. package/messages/en.json +505 -502
  3. package/messages/he.json +505 -502
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.env.local.ejs +45 -24
  6. package/templates/nextjs/base/.eslintrc.json +51 -57
  7. package/templates/nextjs/base/AGENTS.md.ejs +107 -81
  8. package/templates/nextjs/base/CLAUDE.md.ejs +118 -92
  9. package/templates/nextjs/base/next.config.ts +103 -86
  10. package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
  11. package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
  12. package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
  13. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
  14. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +311 -308
  15. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
  16. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
  17. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +6 -5
  18. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
  19. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
  20. package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
  21. package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
  22. package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
  23. package/templates/nextjs/base/src/app/page.tsx +65 -64
  24. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +92 -92
  25. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +10 -5
  26. package/templates/nextjs/base/src/app/robots.ts +3 -2
  27. package/templates/nextjs/base/src/app/sitemap.ts +4 -3
  28. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
  29. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
  30. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
  31. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +16 -2
  32. package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
  33. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
  34. package/templates/nextjs/base/src/core/lib/site-url.ts +197 -0
  35. package/templates/nextjs/base/src/ui/product/faq-section.tsx.ejs +46 -0
  36. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  37. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  38. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
  39. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
  40. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
  41. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
  42. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
  43. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  44. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
  45. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
  46. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
  47. package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
  48. package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
  49. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
@@ -1,59 +1,60 @@
1
- import type { BlogPost } from 'brainerce';
2
- import { buildArticleJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
- import { getNonce } from '@/core/lib/nonce';
4
-
5
- interface ArticleJsonLdProps {
6
- post: BlogPost;
7
- url: string;
8
- /** Store display name — feeds Article.publisher (and the author fallback). */
9
- organizationName?: string;
10
- }
11
-
12
- /**
13
- * Article + BreadcrumbList structured data for a blog post page.
14
- *
15
- * Uses the SDK builders for the schema shape, but renders the <script> here so
16
- * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
- */
18
- export async function ArticleJsonLd({ post, url, organizationName }: ArticleJsonLdProps) {
19
- const nonce = await getNonce();
20
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
-
22
- const articleJsonLd = buildArticleJsonLd(post, {
23
- siteUrl: baseUrl,
24
- path: url,
25
- organizationName,
26
- });
27
-
28
- const breadcrumbJsonLd = buildBreadcrumbJsonLd([
29
- { name: 'Home', url: baseUrl || '/' },
30
- { name: 'Blog', url: `${baseUrl}/blog` },
31
- { name: post.title, url },
32
- ]);
33
-
34
- return (
35
- <>
36
- <script
37
- type="application/ld+json"
38
- nonce={nonce}
39
- suppressHydrationWarning
40
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(articleJsonLd) }}
41
- />
42
- <script
43
- type="application/ld+json"
44
- nonce={nonce}
45
- suppressHydrationWarning
46
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
47
- />
48
- </>
49
- );
50
- }
51
-
52
- // Escape `<`, `>`, `&` so AI-generated article copy can't break out of the
53
- // <script> element or inject HTML.
54
- function serializeJsonLd(value: unknown): string {
55
- return JSON.stringify(value)
56
- .replace(/</g, '\\u003c')
57
- .replace(/>/g, '\\u003e')
58
- .replace(/&/g, '\\u0026');
59
- }
1
+ import type { BlogPost } from 'brainerce';
2
+ import { buildArticleJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
+ import { getNonce } from '@/core/lib/nonce';
4
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
5
+
6
+ interface ArticleJsonLdProps {
7
+ post: BlogPost;
8
+ url: string;
9
+ /** Store display name — feeds Article.publisher (and the author fallback). */
10
+ organizationName?: string;
11
+ }
12
+
13
+ /**
14
+ * Article + BreadcrumbList structured data for a blog post page.
15
+ *
16
+ * Uses the SDK builders for the schema shape, but renders the <script> here so
17
+ * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
18
+ */
19
+ export async function ArticleJsonLd({ post, url, organizationName }: ArticleJsonLdProps) {
20
+ const nonce = await getNonce();
21
+ const baseUrl = await getCanonicalSiteUrl();
22
+
23
+ const articleJsonLd = buildArticleJsonLd(post, {
24
+ siteUrl: baseUrl,
25
+ path: url,
26
+ organizationName,
27
+ });
28
+
29
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd([
30
+ { name: 'Home', url: baseUrl || '/' },
31
+ { name: 'Blog', url: `${baseUrl}/blog` },
32
+ { name: post.title, url },
33
+ ]);
34
+
35
+ return (
36
+ <>
37
+ <script
38
+ type="application/ld+json"
39
+ nonce={nonce}
40
+ suppressHydrationWarning
41
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(articleJsonLd) }}
42
+ />
43
+ <script
44
+ type="application/ld+json"
45
+ nonce={nonce}
46
+ suppressHydrationWarning
47
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
48
+ />
49
+ </>
50
+ );
51
+ }
52
+
53
+ // Escape `<`, `>`, `&` so AI-generated article copy can't break out of the
54
+ // <script> element or inject HTML.
55
+ function serializeJsonLd(value: unknown): string {
56
+ return JSON.stringify(value)
57
+ .replace(/</g, '\\u003c')
58
+ .replace(/>/g, '\\u003e')
59
+ .replace(/&/g, '\\u0026');
60
+ }
@@ -1,61 +1,60 @@
1
- import type { CategoryDetail } from 'brainerce';
2
- import { buildCollectionPageJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
- import { getNonce } from '@/core/lib/nonce';
4
-
5
- interface CategoryJsonLdProps {
6
- category: CategoryDetail;
7
- url: string;
8
- }
9
-
10
- /**
11
- * CollectionPage + BreadcrumbList structured data for a category (collection)
12
- * landing page. NEVER emit Product markup on a listing page — Google's Product
13
- * rich results are single-product only (see ProductJsonLd for PDPs).
14
- *
15
- * Uses the SDK builders for the schema shape, but renders the <script> here so
16
- * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
- */
18
- export async function CategoryJsonLd({ category, url }: CategoryJsonLdProps) {
19
- const nonce = await getNonce();
20
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
-
22
- const collectionJsonLd = buildCollectionPageJsonLd(category, { siteUrl: baseUrl, path: url });
23
-
24
- // Breadcrumb: Home → ancestor categories → this category.
25
- const crumbs: Array<{ name: string; url: string }> = [
26
- { name: 'Home', url: baseUrl || '/' },
27
- ];
28
- for (const ancestor of category.breadcrumb) {
29
- if (ancestor.slug) {
30
- crumbs.push({ name: ancestor.name, url: `${baseUrl}/category/${ancestor.slug}` });
31
- }
32
- }
33
- crumbs.push({ name: category.name, url });
34
- const breadcrumbJsonLd = buildBreadcrumbJsonLd(crumbs);
35
-
36
- return (
37
- <>
38
- <script
39
- type="application/ld+json"
40
- nonce={nonce}
41
- suppressHydrationWarning
42
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(collectionJsonLd) }}
43
- />
44
- <script
45
- type="application/ld+json"
46
- nonce={nonce}
47
- suppressHydrationWarning
48
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
49
- />
50
- </>
51
- );
52
- }
53
-
54
- // Escape `<`, `>`, `&` so merchant-authored category copy can't break out of
55
- // the <script> element or inject HTML.
56
- function serializeJsonLd(value: unknown): string {
57
- return JSON.stringify(value)
58
- .replace(/</g, '\\u003c')
59
- .replace(/>/g, '\\u003e')
60
- .replace(/&/g, '\\u0026');
61
- }
1
+ import type { CategoryDetail } from 'brainerce';
2
+ import { buildCollectionPageJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
+ import { getNonce } from '@/core/lib/nonce';
4
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
5
+
6
+ interface CategoryJsonLdProps {
7
+ category: CategoryDetail;
8
+ url: string;
9
+ }
10
+
11
+ /**
12
+ * CollectionPage + BreadcrumbList structured data for a category (collection)
13
+ * landing page. NEVER emit Product markup on a listing page — Google's Product
14
+ * rich results are single-product only (see ProductJsonLd for PDPs).
15
+ *
16
+ * Uses the SDK builders for the schema shape, but renders the <script> here so
17
+ * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
18
+ */
19
+ export async function CategoryJsonLd({ category, url }: CategoryJsonLdProps) {
20
+ const nonce = await getNonce();
21
+ const baseUrl = await getCanonicalSiteUrl();
22
+
23
+ const collectionJsonLd = buildCollectionPageJsonLd(category, { siteUrl: baseUrl, path: url });
24
+
25
+ // Breadcrumb: Home ancestor categories this category.
26
+ const crumbs: Array<{ name: string; url: string }> = [{ name: 'Home', url: baseUrl || '/' }];
27
+ for (const ancestor of category.breadcrumb) {
28
+ if (ancestor.slug) {
29
+ crumbs.push({ name: ancestor.name, url: `${baseUrl}/category/${ancestor.slug}` });
30
+ }
31
+ }
32
+ crumbs.push({ name: category.name, url });
33
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd(crumbs);
34
+
35
+ return (
36
+ <>
37
+ <script
38
+ type="application/ld+json"
39
+ nonce={nonce}
40
+ suppressHydrationWarning
41
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(collectionJsonLd) }}
42
+ />
43
+ <script
44
+ type="application/ld+json"
45
+ nonce={nonce}
46
+ suppressHydrationWarning
47
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
48
+ />
49
+ </>
50
+ );
51
+ }
52
+
53
+ // Escape `<`, `>`, `&` so merchant-authored category copy can't break out of
54
+ // the <script> element or inject HTML.
55
+ function serializeJsonLd(value: unknown): string {
56
+ return JSON.stringify(value)
57
+ .replace(/</g, '\\u003c')
58
+ .replace(/>/g, '\\u003e')
59
+ .replace(/&/g, '\\u0026');
60
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Product, ShippingSummaryEntry } from 'brainerce';
2
- import { buildProductJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
2
+ import { buildProductJsonLd, buildBreadcrumbJsonLd, buildProductFaqJsonLd } from 'brainerce';
3
3
  import { getNonce } from '@/core/lib/nonce';
4
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
4
5
  import { resolveCurrency } from '@/core/lib/resolve-currency';
5
6
 
6
7
  interface ProductJsonLdProps {
@@ -27,7 +28,7 @@ export async function ProductJsonLd({ product, url, currency, shipping }: Produc
27
28
  // lives in exactly one place across all server-side currency reads.
28
29
  const resolvedCurrency = resolveCurrency(null, currency);
29
30
  const nonce = await getNonce();
30
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
31
+ const baseUrl = await getCanonicalSiteUrl();
31
32
 
32
33
  const productJsonLd = buildProductJsonLd(product, {
33
34
  siteUrl: baseUrl,
@@ -42,6 +43,11 @@ export async function ProductJsonLd({ product, url, currency, shipping }: Produc
42
43
  { name: product.name, url },
43
44
  ]);
44
45
 
46
+ // FAQPage from the product's Q&A — null (no script) when the product has
47
+ // none. The visible FAQ section on the PDP renders the SAME pairs; this is
48
+ // corroboration for engines, never a substitute for visible text.
49
+ const faqJsonLd = buildProductFaqJsonLd(product);
50
+
45
51
  return (
46
52
  <>
47
53
  <script
@@ -56,6 +62,14 @@ export async function ProductJsonLd({ product, url, currency, shipping }: Produc
56
62
  suppressHydrationWarning
57
63
  dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
58
64
  />
65
+ {faqJsonLd ? (
66
+ <script
67
+ type="application/ld+json"
68
+ nonce={nonce}
69
+ suppressHydrationWarning
70
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
71
+ />
72
+ ) : null}
59
73
  </>
60
74
  );
61
75
  }
@@ -0,0 +1,81 @@
1
+ import { BrainerceClient } from 'brainerce';
2
+ import { cache } from 'react';
3
+ import { SALES_CHANNEL_ID } from '@/core/lib/brainerce';
4
+ import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info';
5
+ import { getRequestOrigin } from '@/core/lib/site-url';
6
+
7
+ /**
8
+ * SERVER-ONLY half of the SDK wiring. Kept apart from `brainerce.ts` on
9
+ * purpose: everything here reaches `next/headers` (to resolve the request's
10
+ * origin), and ~30 client components import `getClient` from `brainerce.ts`.
11
+ * A single shared module would drag `next/headers` into the client bundle and
12
+ * fail the build outright — which is exactly what happened when these two
13
+ * functions still lived there.
14
+ *
15
+ * Import from here in Server Components, `generateMetadata`, route handlers,
16
+ * `sitemap.ts` / `robots.ts`. Never from a `'use client'` file.
17
+ */
18
+
19
+ /**
20
+ * Server-side client — calls the backend directly (no proxy needed for public
21
+ * data). Used by Server Components for SSR data fetching.
22
+ *
23
+ * ⛔ ASYNC on purpose. The backend compares the `Origin` we send against the
24
+ * domain configured on the sales channel, and a LIVE channel rejects both a
25
+ * mismatched Origin AND a missing one with 403. The correct value is only
26
+ * knowable from the request when no `SITE_URL` is configured — the norm for
27
+ * AI-builder deploys — so resolving it needs an await. This previously read
28
+ * `process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'`, which sent
29
+ * localhost from every such deploy, so every SSR call failed the moment the
30
+ * channel went Live.
31
+ */
32
+ export async function getServerClient(locale?: string): Promise<BrainerceClient> {
33
+ const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com';
34
+ const client = new BrainerceClient({
35
+ salesChannelId: SALES_CHANNEL_ID,
36
+ baseUrl: apiUrl,
37
+ origin: await getRequestOrigin(),
38
+ });
39
+ if (locale) {
40
+ client.setLocale(locale);
41
+ }
42
+ return client;
43
+ }
44
+
45
+ /**
46
+ * Server-side cached fetch of store info, used to seed `<StoreProvider>` at
47
+ * SSR so client components don't render with `null` storeInfo and bake the
48
+ * wrong currency symbol into the HTML Googlebot indexes.
49
+ *
50
+ * Two cache layers:
51
+ * - React `cache()` dedupes calls within a single request render so the
52
+ * layout + `generateMetadata` share one fetch.
53
+ * - Next.js `fetch({ next: { revalidate, tags } })` caches the response
54
+ * across requests for ~60s and lets the dashboard bust the cache via
55
+ * `revalidateTag('store-info')` on store-config save.
56
+ *
57
+ * Returns `null` on hard failure so the provider's client-side `useEffect`
58
+ * fallback can take over — behaviour identical to pre-SSR-hydration.
59
+ *
60
+ * Bypasses {@link getServerClient} on purpose: the SDK's internal `fetch`
61
+ * does not forward `next:` options, so it cannot participate in the Next.js
62
+ * Data Cache today. Switch back to the SDK once it accepts `fetchOptions`.
63
+ */
64
+ export const fetchStoreInfo = cache(async (locale?: string): Promise<PublicStoreInfo | null> => {
65
+ const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com';
66
+ const siteOrigin = await getRequestOrigin();
67
+ const url = `${apiUrl}/api/vc/${encodeURIComponent(SALES_CHANNEL_ID)}/info`;
68
+ try {
69
+ const res = await fetch(url, {
70
+ headers: {
71
+ Origin: siteOrigin,
72
+ ...(locale ? { 'Accept-Language': locale } : {}),
73
+ },
74
+ next: { revalidate: 60, tags: ['store-info'] },
75
+ });
76
+ if (!res.ok) return null;
77
+ return pickPublicStoreInfo(await res.json());
78
+ } catch {
79
+ return null;
80
+ }
81
+ });
@@ -1,110 +1,60 @@
1
- import { BrainerceClient } from 'brainerce';
2
- import { cache } from 'react';
3
- import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info';
4
-
5
- // Read either env var name. The new one is preferred; the old one is a soft
6
- // alias kept for backwards compatibility both are accepted by the SDK.
7
- const SALES_CHANNEL_ID =
8
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
9
- process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
10
- '<%= connectionId %>';
11
-
12
- // Singleton SDK client — routes through same-origin BFF proxy for httpOnly cookie auth
13
- let clientInstance: BrainerceClient | null = null;
14
-
15
- export function getClient(): BrainerceClient {
16
- if (!clientInstance) {
17
- clientInstance = new BrainerceClient({
18
- salesChannelId: SALES_CHANNEL_ID,
19
- baseUrl: '/api/store', // same-origin proxy handles auth via httpOnly cookie
20
- proxyMode: true, // skip client-side token checks; proxy adds Authorization header
21
- });
22
- }
23
- return clientInstance;
24
- }
25
- <% if (i18nEnabled) { %>
26
-
27
- /** Initialize client with a specific locale for translated content */
28
- export function initClientWithLocale(locale: string): BrainerceClient {
29
- const client = getClient();
30
- client.setLocale(locale);
31
- return client;
32
- }
33
- <% } %>
34
-
35
- // Cart ID helpers (not a security token — safe in localStorage)
36
- const CART_ID_KEY = 'brainerce_cart_id';
37
-
38
- export function getStoredCartId(): string | null {
39
- if (typeof window === 'undefined') return null;
40
- return localStorage.getItem(CART_ID_KEY);
41
- }
42
-
43
- export function setStoredCartId(cartId: string | null): void {
44
- if (typeof window === 'undefined') return;
45
- if (cartId) {
46
- localStorage.setItem(CART_ID_KEY, cartId);
47
- } else {
48
- localStorage.removeItem(CART_ID_KEY);
49
- }
50
- }
51
-
52
- // Initialize client (no token hydration — auth handled by httpOnly cookie)
53
- export function initClient(): BrainerceClient {
54
- return getClient();
55
- }
56
-
57
- // Server-side client calls backend directly (no proxy needed for public data)
58
- // Used by Server Components for SSR data fetching (generateMetadata, page rendering)
59
- export function getServerClient(locale?: string): BrainerceClient {
60
- const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com';
61
- const client = new BrainerceClient({
62
- salesChannelId: SALES_CHANNEL_ID,
63
- baseUrl: apiUrl,
64
- origin: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
65
- });
66
- if (locale) {
67
- client.setLocale(locale);
68
- }
69
- return client;
70
- }
71
-
72
- /**
73
- * Server-side cached fetch of store info, used to seed `<StoreProvider>` at
74
- * SSR so client components don't render with `null` storeInfo and bake the
75
- * wrong currency symbol into the HTML Googlebot indexes.
76
- *
77
- * Two cache layers:
78
- * - React `cache()` dedupes calls within a single request render so the
79
- * layout + `generateMetadata` share one fetch.
80
- * - Next.js `fetch({ next: { revalidate, tags } })` caches the response
81
- * across requests for ~60s and lets the dashboard bust the cache via
82
- * `revalidateTag('store-info')` on store-config save.
83
- *
84
- * Returns `null` on hard failure so the provider's client-side `useEffect`
85
- * fallback can take over — behaviour identical to pre-SSR-hydration.
86
- *
87
- * Bypasses {@link getServerClient} on purpose: the SDK's internal `fetch`
88
- * does not forward `next:` options, so it cannot participate in the Next.js
89
- * Data Cache today. Switch back to the SDK once it accepts `fetchOptions`.
90
- */
91
- export const fetchStoreInfo = cache(
92
- async (locale?: string): Promise<PublicStoreInfo | null> => {
93
- const apiUrl = process.env.BRAINERCE_API_URL || 'https://api.brainerce.com';
94
- const siteOrigin = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
95
- const url = `${apiUrl}/api/vc/${encodeURIComponent(SALES_CHANNEL_ID)}/info`;
96
- try {
97
- const res = await fetch(url, {
98
- headers: {
99
- Origin: siteOrigin,
100
- ...(locale ? { 'Accept-Language': locale } : {}),
101
- },
102
- next: { revalidate: 60, tags: ['store-info'] },
103
- });
104
- if (!res.ok) return null;
105
- return pickPublicStoreInfo(await res.json());
106
- } catch {
107
- return null;
108
- }
109
- },
110
- );
1
+ import { BrainerceClient } from 'brainerce';
2
+
3
+ /**
4
+ * CLIENT-SAFE half of the SDK wiring — imported by ~30 `'use client'`
5
+ * components. Nothing here may reach `next/headers` or any server-only API,
6
+ * or the client bundle fails to build. The server half lives in
7
+ * `brainerce.server.ts`.
8
+ */
9
+
10
+ // Read either env var name. The new one is preferred; the old one is a soft
11
+ // alias kept for backwards compatibility — both are accepted by the SDK.
12
+ export const SALES_CHANNEL_ID =
13
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
14
+ process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
15
+ '<%= connectionId %>';
16
+
17
+ // Singleton SDK client — routes through same-origin BFF proxy for httpOnly cookie auth
18
+ let clientInstance: BrainerceClient | null = null;
19
+
20
+ export function getClient(): BrainerceClient {
21
+ if (!clientInstance) {
22
+ clientInstance = new BrainerceClient({
23
+ salesChannelId: SALES_CHANNEL_ID,
24
+ baseUrl: '/api/store', // same-origin proxy handles auth via httpOnly cookie
25
+ proxyMode: true, // skip client-side token checks; proxy adds Authorization header
26
+ });
27
+ }
28
+ return clientInstance;
29
+ }
30
+ <% if (i18nEnabled) { %>
31
+
32
+ /** Initialize client with a specific locale for translated content */
33
+ export function initClientWithLocale(locale: string): BrainerceClient {
34
+ const client = getClient();
35
+ client.setLocale(locale);
36
+ return client;
37
+ }
38
+ <% } %>
39
+
40
+ // Cart ID helpers (not a security token — safe in localStorage)
41
+ const CART_ID_KEY = 'brainerce_cart_id';
42
+
43
+ export function getStoredCartId(): string | null {
44
+ if (typeof window === 'undefined') return null;
45
+ return localStorage.getItem(CART_ID_KEY);
46
+ }
47
+
48
+ export function setStoredCartId(cartId: string | null): void {
49
+ if (typeof window === 'undefined') return;
50
+ if (cartId) {
51
+ localStorage.setItem(CART_ID_KEY, cartId);
52
+ } else {
53
+ localStorage.removeItem(CART_ID_KEY);
54
+ }
55
+ }
56
+
57
+ // Initialize client (no token hydration auth handled by httpOnly cookie)
58
+ export function initClient(): BrainerceClient {
59
+ return getClient();
60
+ }