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.
- package/dist/index.js +2 -2
- package/messages/en.json +505 -502
- package/messages/he.json +505 -502
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +45 -24
- package/templates/nextjs/base/.eslintrc.json +51 -57
- package/templates/nextjs/base/AGENTS.md.ejs +107 -81
- package/templates/nextjs/base/CLAUDE.md.ejs +118 -92
- package/templates/nextjs/base/next.config.ts +103 -86
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
- package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +311 -308
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
- package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +6 -5
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
- package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
- package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
- package/templates/nextjs/base/src/app/page.tsx +65 -64
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +92 -92
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +10 -5
- package/templates/nextjs/base/src/app/robots.ts +3 -2
- package/templates/nextjs/base/src/app/sitemap.ts +4 -3
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +16 -2
- package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
- package/templates/nextjs/base/src/core/lib/site-url.ts +197 -0
- package/templates/nextjs/base/src/ui/product/faq-section.tsx.ejs +46 -0
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
- package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ name: '
|
|
31
|
-
{ name:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.replace(
|
|
58
|
-
.replace(
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.replace(
|
|
59
|
-
.replace(
|
|
60
|
-
|
|
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 =
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export function
|
|
44
|
-
if (typeof window === 'undefined') return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|