create-brainerce-store 1.84.0 → 1.86.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/README.md +130 -92
- package/dist/index.js +39 -17
- package/messages/en.json +9 -2
- package/messages/he.json +9 -2
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +10 -1
- package/templates/nextjs/base/AGENTS.md.ejs +229 -207
- package/templates/nextjs/base/AI-GUIDE.md +4 -0
- package/templates/nextjs/base/CLAUDE.md.ejs +240 -218
- package/templates/nextjs/base/scripts/connect.mjs +115 -18
- package/templates/nextjs/base/scripts/env-file.mjs +93 -0
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +109 -83
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +2 -1
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +2 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +1325 -1302
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +3 -2
- package/templates/nextjs/base/src/app/icon.svg +11 -0
- package/templates/nextjs/base/src/app/layout.tsx.ejs +50 -21
- package/templates/nextjs/base/src/app/opengraph-image.tsx +5 -1
- package/templates/nextjs/base/src/app/reset-password/page.tsx +25 -138
- package/templates/nextjs/base/src/app/reset-password/reset-password-form.tsx +138 -0
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +17 -2
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +174 -137
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +130 -12
- package/templates/nextjs/base/src/core/lib/store-name.ts.ejs +56 -0
- package/templates/nextjs/base/src/core/lib/tracking.ts +25 -14
- package/templates/nextjs/base/src/core/lib/use-store-name.ts +18 -0
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +160 -160
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +2 -2
- package/templates/nextjs/base/src/ui/product/price-display.tsx +85 -62
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +539 -537
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +374 -345
- package/templates/nextjs/designs/atelier/globals.css +408 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +91 -96
- package/templates/nextjs/designs/atelier/messages-patch/he.json +91 -96
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +131 -128
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +2 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +222 -219
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +178 -151
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +61 -31
- package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +161 -155
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +232 -227
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +90 -69
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +271 -268
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +560 -553
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +113 -110
- package/templates/nextjs/designs/atelier/ui/shared/no-photo-tile.tsx +73 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +73 -52
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +428 -426
|
@@ -1,345 +1,374 @@
|
|
|
1
|
-
<% if (i18nEnabled) { %>
|
|
2
|
-
import type { Metadata } from 'next';
|
|
3
|
-
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
4
|
-
import { StoreProvider } from '@/core/providers/store-provider';
|
|
5
|
-
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
6
|
-
import { SiteHeader } from '@/ui/layout/site-header';
|
|
7
|
-
import { DiscountBannerStrip } from '@/ui/home/discount-banner-strip';
|
|
8
|
-
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
9
|
-
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
10
|
-
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
11
|
-
import { TrackingBootstrap } from '@/components/tracking-bootstrap';
|
|
12
|
-
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
13
|
-
import { resolveRegion } from '@/core/lib/region.server';
|
|
14
|
-
import { getDirection, getMessages, supportedLocales } from '@/i18n';
|
|
15
|
-
import { getNonce } from '@/core/lib/nonce';
|
|
16
|
-
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
17
|
-
import '
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
client.
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
4
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
5
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
6
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
7
|
+
import { DiscountBannerStrip } from '@/ui/home/discount-banner-strip';
|
|
8
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
9
|
+
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
10
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
11
|
+
import { TrackingBootstrap } from '@/components/tracking-bootstrap';
|
|
12
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
13
|
+
import { resolveRegion } from '@/core/lib/region.server';
|
|
14
|
+
import { getDirection, getMessages, supportedLocales } from '@/i18n';
|
|
15
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
16
|
+
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
17
|
+
import { resolveStoreName, buildTitleTemplate } from '@/core/lib/store-name';
|
|
18
|
+
import { buildMetaDescription } from '@/core/lib/seo';
|
|
19
|
+
import '../globals.css';
|
|
20
|
+
|
|
21
|
+
// Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
|
|
22
|
+
// (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
|
|
23
|
+
// consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
|
|
24
|
+
const sans = Assistant({
|
|
25
|
+
subsets: ['hebrew', 'latin'],
|
|
26
|
+
weight: ['400', '500', '600', '700'],
|
|
27
|
+
variable: '--font-sans',
|
|
28
|
+
});
|
|
29
|
+
const serif = Frank_Ruhl_Libre({
|
|
30
|
+
subsets: ['hebrew', 'latin'],
|
|
31
|
+
weight: ['400', '500', '700'],
|
|
32
|
+
variable: '--font-serif',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Resolved per request rather than read from a hardcoded env default: the
|
|
36
|
+
// scaffolder cannot know the deploy domain, so `metadataBase` is the one
|
|
37
|
+
// value that must never be guessed. See core/lib/site-url.ts.
|
|
38
|
+
//
|
|
39
|
+
// The store NAME is resolved the same way, and for the same reason: under
|
|
40
|
+
// `--defer-connection` the scaffolder only knew the directory name. Live
|
|
41
|
+
// `storeInfo.name` wins (`fetchStoreInfo` is `cache()`-wrapped, so this shares
|
|
42
|
+
// the layout's fetch), then the build-time NEXT_PUBLIC_STORE_NAME, then the
|
|
43
|
+
// scaffold literal — see core/lib/store-name.ts.
|
|
44
|
+
export async function generateMetadata({
|
|
45
|
+
params,
|
|
46
|
+
}: {
|
|
47
|
+
params: Promise<{ locale: string }>;
|
|
48
|
+
}): Promise<Metadata> {
|
|
49
|
+
const { locale } = await params;
|
|
50
|
+
const [baseUrl, storeInfo] = await Promise.all([getCanonicalSiteUrl(), fetchStoreInfo(locale)]);
|
|
51
|
+
const storeName = resolveStoreName(storeInfo);
|
|
52
|
+
return {
|
|
53
|
+
metadataBase: new URL(baseUrl),
|
|
54
|
+
title: {
|
|
55
|
+
default: storeName,
|
|
56
|
+
template: buildTitleTemplate(storeName),
|
|
57
|
+
},
|
|
58
|
+
description: buildMetaDescription(storeInfo?.metaDescription) || storeName,
|
|
59
|
+
alternates: {
|
|
60
|
+
canonical: '/',
|
|
61
|
+
},
|
|
62
|
+
openGraph: {
|
|
63
|
+
siteName: storeName,
|
|
64
|
+
type: 'website',
|
|
65
|
+
},
|
|
66
|
+
robots: {
|
|
67
|
+
index: true,
|
|
68
|
+
follow: true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildOrganizationJsonLd(baseUrl: string, storeName: string) {
|
|
74
|
+
return {
|
|
75
|
+
'@context': 'https://schema.org',
|
|
76
|
+
'@type': 'Organization',
|
|
77
|
+
name: storeName,
|
|
78
|
+
url: baseUrl,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function generateStaticParams() {
|
|
83
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default async function RootLayout({
|
|
87
|
+
children,
|
|
88
|
+
params,
|
|
89
|
+
}: {
|
|
90
|
+
children: React.ReactNode;
|
|
91
|
+
params: Promise<{ locale: string }>;
|
|
92
|
+
}) {
|
|
93
|
+
const { locale } = await params;
|
|
94
|
+
const dir = getDirection(locale);
|
|
95
|
+
const nonce = await getNonce();
|
|
96
|
+
const baseUrl = await getCanonicalSiteUrl();
|
|
97
|
+
|
|
98
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
99
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
100
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
101
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
102
|
+
const client = await getServerClient(locale);
|
|
103
|
+
const [announcements, siteHeader, siteFooter, storeInfo, messages, discountBanners, regionInfo] =
|
|
104
|
+
await Promise.all([
|
|
105
|
+
client.content.announcement.list(locale).catch(() => []),
|
|
106
|
+
client.content.header.get('main', locale).catch(() => null),
|
|
107
|
+
client.content.footer.get('main', locale).catch(() => null),
|
|
108
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
109
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
110
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
111
|
+
fetchStoreInfo(locale),
|
|
112
|
+
// SSR-fetch UI message strings so translated text renders at frame 0 —
|
|
113
|
+
// without this, every page (and every locale switch) shows raw
|
|
114
|
+
// "namespace.key" strings until the client-only fetch resolves.
|
|
115
|
+
getMessages(locale),
|
|
116
|
+
// Store-wide discount banners. Fetched here rather than on the home
|
|
117
|
+
// page so they render below the header on EVERY page. `.catch(() => [])`
|
|
118
|
+
// keeps a store with no active discounts (and an API hiccup) silent:
|
|
119
|
+
// the strip itself returns null on an empty array.
|
|
120
|
+
client.getDiscountBanners().catch(() => []),
|
|
121
|
+
// Which region prices this request: the shopper's cookie, else the edge
|
|
122
|
+
// geo header, else the store default. Resolved here so SSR and the client
|
|
123
|
+
// agree from frame 0 and every page shares one round trip (it is
|
|
124
|
+
// `cache()`-wrapped). Never throws: a store with no regions answers
|
|
125
|
+
// `{ regions: [], region: null }` and every consumer then sends no
|
|
126
|
+
// `regionId`, which is what this storefront did before regions existed.
|
|
127
|
+
resolveRegion(),
|
|
128
|
+
]);
|
|
129
|
+
// One resolved name for the JSON-LD, the header and the footer. Live data
|
|
130
|
+
// first; the scaffold-time literal is only the last fallback.
|
|
131
|
+
const storeName = resolveStoreName(storeInfo);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<html lang={locale} dir={dir} data-scroll-behavior="smooth">
|
|
135
|
+
<head>
|
|
136
|
+
<script
|
|
137
|
+
type="application/ld+json"
|
|
138
|
+
nonce={nonce}
|
|
139
|
+
suppressHydrationWarning
|
|
140
|
+
dangerouslySetInnerHTML={{
|
|
141
|
+
__html: JSON.stringify(buildOrganizationJsonLd(baseUrl, storeName))
|
|
142
|
+
.replace(/</g, '\\u003c')
|
|
143
|
+
.replace(/>/g, '\\u003e')
|
|
144
|
+
.replace(/&/g, '\\u0026'),
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
{/* Google Search Console verification — token set by the merchant in
|
|
148
|
+
the dashboard (channel settings → Google site verification). Also
|
|
149
|
+
what the Merchant Center website claim checks for. Renders nothing
|
|
150
|
+
until a token is configured. */}
|
|
151
|
+
{storeInfo?.seo?.googleSiteVerification ? (
|
|
152
|
+
<meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
|
|
153
|
+
) : null}
|
|
154
|
+
{/* RSS autodiscovery — feed readers and aggregators find the blog feed
|
|
155
|
+
from any page. The route itself lives at /blog/rss.xml. */}
|
|
156
|
+
<link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
|
|
157
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
158
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
159
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
160
|
+
<script
|
|
161
|
+
defer
|
|
162
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
163
|
+
data-channel={
|
|
164
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
165
|
+
}
|
|
166
|
+
nonce={nonce}
|
|
167
|
+
suppressHydrationWarning
|
|
168
|
+
/>
|
|
169
|
+
</head>
|
|
170
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
171
|
+
<StoreProvider
|
|
172
|
+
locale={locale}
|
|
173
|
+
initialStoreInfo={storeInfo}
|
|
174
|
+
initialMessages={messages}
|
|
175
|
+
initialRegions={regionInfo.regions}
|
|
176
|
+
initialRegion={regionInfo.region}
|
|
177
|
+
initialCountry={regionInfo.country}
|
|
178
|
+
>
|
|
179
|
+
{/* Mounted ahead of the page tree on purpose: React runs effects in
|
|
180
|
+
tree order, so booting the tags here guarantees fbq/ttq exist by
|
|
181
|
+
the time a page component fires its first e-commerce event. */}
|
|
182
|
+
<TrackingBootstrap />
|
|
183
|
+
<div className="min-h-screen flex flex-col">
|
|
184
|
+
<AnnouncementBar announcements={announcements} />
|
|
185
|
+
<SiteHeader header={siteHeader} storeName={storeName} />
|
|
186
|
+
<DiscountBannerStrip banners={discountBanners} />
|
|
187
|
+
<main className="flex-1">{children}</main>
|
|
188
|
+
<SiteFooter footer={siteFooter} storeName={storeName} locale={locale} />
|
|
189
|
+
</div>
|
|
190
|
+
<CartDrawer />
|
|
191
|
+
<BrainerceBotWidget />
|
|
192
|
+
</StoreProvider>
|
|
193
|
+
</body>
|
|
194
|
+
</html>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
<% } else { %>
|
|
198
|
+
import type { Metadata } from 'next';
|
|
199
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
200
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
201
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
202
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
203
|
+
import { DiscountBannerStrip } from '@/ui/home/discount-banner-strip';
|
|
204
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
205
|
+
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
206
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
207
|
+
import { TrackingBootstrap } from '@/components/tracking-bootstrap';
|
|
208
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
209
|
+
import { resolveRegion } from '@/core/lib/region.server';
|
|
210
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
211
|
+
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
212
|
+
import { resolveStoreName, buildTitleTemplate } from '@/core/lib/store-name';
|
|
213
|
+
import { buildMetaDescription } from '@/core/lib/seo';
|
|
214
|
+
import './globals.css';
|
|
215
|
+
|
|
216
|
+
// Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
|
|
217
|
+
// (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
|
|
218
|
+
// consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
|
|
219
|
+
const sans = Assistant({
|
|
220
|
+
subsets: ['hebrew', 'latin'],
|
|
221
|
+
weight: ['400', '500', '600', '700'],
|
|
222
|
+
variable: '--font-sans',
|
|
223
|
+
});
|
|
224
|
+
const serif = Frank_Ruhl_Libre({
|
|
225
|
+
subsets: ['hebrew', 'latin'],
|
|
226
|
+
weight: ['400', '500', '700'],
|
|
227
|
+
variable: '--font-serif',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Resolved per request rather than read from a hardcoded env default: the
|
|
231
|
+
// scaffolder cannot know the deploy domain, so `metadataBase` is the one
|
|
232
|
+
// value that must never be guessed. See core/lib/site-url.ts.
|
|
233
|
+
//
|
|
234
|
+
// The store NAME is resolved the same way, and for the same reason: under
|
|
235
|
+
// `--defer-connection` the scaffolder only knew the directory name. Live
|
|
236
|
+
// `storeInfo.name` wins (`fetchStoreInfo` is `cache()`-wrapped, so this shares
|
|
237
|
+
// the layout's fetch), then the build-time NEXT_PUBLIC_STORE_NAME, then the
|
|
238
|
+
// scaffold literal — see core/lib/store-name.ts.
|
|
239
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
240
|
+
const [baseUrl, storeInfo] = await Promise.all([getCanonicalSiteUrl(), fetchStoreInfo()]);
|
|
241
|
+
const storeName = resolveStoreName(storeInfo);
|
|
242
|
+
return {
|
|
243
|
+
metadataBase: new URL(baseUrl),
|
|
244
|
+
title: {
|
|
245
|
+
default: storeName,
|
|
246
|
+
template: buildTitleTemplate(storeName),
|
|
247
|
+
},
|
|
248
|
+
description: buildMetaDescription(storeInfo?.metaDescription) || storeName,
|
|
249
|
+
alternates: {
|
|
250
|
+
canonical: '/',
|
|
251
|
+
},
|
|
252
|
+
openGraph: {
|
|
253
|
+
siteName: storeName,
|
|
254
|
+
locale: '<%= ogLocale %>',
|
|
255
|
+
type: 'website',
|
|
256
|
+
},
|
|
257
|
+
robots: {
|
|
258
|
+
index: true,
|
|
259
|
+
follow: true,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildOrganizationJsonLd(baseUrl: string, storeName: string) {
|
|
265
|
+
return {
|
|
266
|
+
'@context': 'https://schema.org',
|
|
267
|
+
'@type': 'Organization',
|
|
268
|
+
name: storeName,
|
|
269
|
+
url: baseUrl,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default async function RootLayout({
|
|
274
|
+
children,
|
|
275
|
+
}: {
|
|
276
|
+
children: React.ReactNode;
|
|
277
|
+
}) {
|
|
278
|
+
const nonce = await getNonce();
|
|
279
|
+
const baseUrl = await getCanonicalSiteUrl();
|
|
280
|
+
|
|
281
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
282
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
283
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
284
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
285
|
+
const client = await getServerClient();
|
|
286
|
+
const [announcements, siteHeader, siteFooter, storeInfo, discountBanners, regionInfo] =
|
|
287
|
+
await Promise.all([
|
|
288
|
+
client.content.announcement.list().catch(() => []),
|
|
289
|
+
client.content.header.get('main').catch(() => null),
|
|
290
|
+
client.content.footer.get('main').catch(() => null),
|
|
291
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
292
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
293
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
294
|
+
fetchStoreInfo(),
|
|
295
|
+
// Store-wide discount banners. Fetched here rather than on the home
|
|
296
|
+
// page so they render below the header on EVERY page. `.catch(() => [])`
|
|
297
|
+
// keeps a store with no active discounts (and an API hiccup) silent:
|
|
298
|
+
// the strip itself returns null on an empty array.
|
|
299
|
+
client.getDiscountBanners().catch(() => []),
|
|
300
|
+
// Which region prices this request: the shopper's cookie, else the edge
|
|
301
|
+
// geo header, else the store default. Resolved here so SSR and the client
|
|
302
|
+
// agree from frame 0 and every page shares one round trip (it is
|
|
303
|
+
// `cache()`-wrapped). Never throws: a store with no regions answers
|
|
304
|
+
// `{ regions: [], region: null }` and every consumer then sends no
|
|
305
|
+
// `regionId`, which is what this storefront did before regions existed.
|
|
306
|
+
resolveRegion(),
|
|
307
|
+
]);
|
|
308
|
+
// One resolved name for the JSON-LD, the header and the footer. Live data
|
|
309
|
+
// first; the scaffold-time literal is only the last fallback.
|
|
310
|
+
const storeName = resolveStoreName(storeInfo);
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<html lang="<%= language %>" dir="<%= direction %>" data-scroll-behavior="smooth">
|
|
314
|
+
<head>
|
|
315
|
+
<script
|
|
316
|
+
type="application/ld+json"
|
|
317
|
+
nonce={nonce}
|
|
318
|
+
suppressHydrationWarning
|
|
319
|
+
dangerouslySetInnerHTML={{
|
|
320
|
+
__html: JSON.stringify(buildOrganizationJsonLd(baseUrl, storeName))
|
|
321
|
+
.replace(/</g, '\\u003c')
|
|
322
|
+
.replace(/>/g, '\\u003e')
|
|
323
|
+
.replace(/&/g, '\\u0026'),
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
{/* Google Search Console verification — token set by the merchant in
|
|
327
|
+
the dashboard (channel settings → Google site verification). Also
|
|
328
|
+
what the Merchant Center website claim checks for. Renders nothing
|
|
329
|
+
until a token is configured. */}
|
|
330
|
+
{storeInfo?.seo?.googleSiteVerification ? (
|
|
331
|
+
<meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
|
|
332
|
+
) : null}
|
|
333
|
+
{/* RSS autodiscovery — feed readers and aggregators find the blog feed
|
|
334
|
+
from any page. The route itself lives at /blog/rss.xml. */}
|
|
335
|
+
<link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
|
|
336
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
337
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
338
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
339
|
+
<script
|
|
340
|
+
defer
|
|
341
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
342
|
+
data-channel={
|
|
343
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
344
|
+
}
|
|
345
|
+
nonce={nonce}
|
|
346
|
+
suppressHydrationWarning
|
|
347
|
+
/>
|
|
348
|
+
</head>
|
|
349
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
350
|
+
<StoreProvider
|
|
351
|
+
initialStoreInfo={storeInfo}
|
|
352
|
+
initialRegions={regionInfo.regions}
|
|
353
|
+
initialRegion={regionInfo.region}
|
|
354
|
+
initialCountry={regionInfo.country}
|
|
355
|
+
>
|
|
356
|
+
{/* Mounted ahead of the page tree on purpose: React runs effects in
|
|
357
|
+
tree order, so booting the tags here guarantees fbq/ttq exist by
|
|
358
|
+
the time a page component fires its first e-commerce event. */}
|
|
359
|
+
<TrackingBootstrap />
|
|
360
|
+
<div className="min-h-screen flex flex-col">
|
|
361
|
+
<AnnouncementBar announcements={announcements} />
|
|
362
|
+
<SiteHeader header={siteHeader} storeName={storeName} />
|
|
363
|
+
<DiscountBannerStrip banners={discountBanners} />
|
|
364
|
+
<main className="flex-1">{children}</main>
|
|
365
|
+
<SiteFooter footer={siteFooter} storeName={storeName} />
|
|
366
|
+
</div>
|
|
367
|
+
<CartDrawer />
|
|
368
|
+
<BrainerceBotWidget />
|
|
369
|
+
</StoreProvider>
|
|
370
|
+
</body>
|
|
371
|
+
</html>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
<% } %>
|