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,285 +1,301 @@
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 { SiteFooter } from '@/ui/layout/site-footer';
8
- import { CartDrawer } from '@/ui/cart/cart-drawer';
9
- import { BrainerceBotWidget } from '@/components/brainerce-bot';
10
- import { TrackingBootstrap } from '@/components/tracking-bootstrap';
11
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
12
- import { getDirection, getMessages, supportedLocales } from '@/i18n';
13
- import { getNonce } from '@/core/lib/nonce';
14
- import '../globals.css';
15
-
16
- // Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
17
- // (Hebrew serif the gallery voice) for display. Exposed as CSS variables
18
- // consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
19
- const sans = Assistant({
20
- subsets: ['hebrew', 'latin'],
21
- weight: ['400', '500', '600', '700'],
22
- variable: '--font-sans',
23
- });
24
- const serif = Frank_Ruhl_Libre({
25
- subsets: ['hebrew', 'latin'],
26
- weight: ['400', '500', '700'],
27
- variable: '--font-serif',
28
- });
29
-
30
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
31
-
32
- export const metadata: Metadata = {
33
- metadataBase: new URL(baseUrl),
34
- title: {
35
- default: <%- storeNameJs %>,
36
- template: <%- titleTemplateJs %>,
37
- },
38
- description: <%- storeNameJs %>,
39
- alternates: {
40
- canonical: '/',
41
- },
42
- openGraph: {
43
- siteName: <%- storeNameJs %>,
44
- type: 'website',
45
- },
46
- robots: {
47
- index: true,
48
- follow: true,
49
- },
50
- };
51
-
52
- const organizationJsonLd = {
53
- '@context': 'https://schema.org',
54
- '@type': 'Organization',
55
- name: <%- storeNameJs %>,
56
- url: baseUrl,
57
- };
58
-
59
- export function generateStaticParams() {
60
- return supportedLocales.map((locale) => ({ locale }));
61
- }
62
-
63
- export default async function RootLayout({
64
- children,
65
- params,
66
- }: {
67
- children: React.ReactNode;
68
- params: Promise<{ locale: string }>;
69
- }) {
70
- const { locale } = await params;
71
- const dir = getDirection(locale);
72
- const nonce = await getNonce();
73
-
74
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
75
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
76
- // seeded a particular content type yet. New stores ship with default rows
77
- // seeded by the backend (StoresService.seedDefaultContent).
78
- const client = getServerClient(locale);
79
- const [announcements, siteHeader, siteFooter, storeInfo, messages] = await Promise.all([
80
- client.content.announcement.list(locale).catch(() => []),
81
- client.content.header.get('main', locale).catch(() => null),
82
- client.content.footer.get('main', locale).catch(() => null),
83
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
84
- // render with the real currency, feature flags, and i18n config at frame 0
85
- // without this, Googlebot sees USD/defaults baked into the HTML.
86
- fetchStoreInfo(locale),
87
- // SSR-fetch UI message strings so translated text renders at frame 0 —
88
- // without this, every page (and every locale switch) shows raw
89
- // "namespace.key" strings until the client-only fetch resolves.
90
- getMessages(locale),
91
- ]);
92
-
93
- return (
94
- <html lang={locale} dir={dir}>
95
- <head>
96
- <script
97
- type="application/ld+json"
98
- nonce={nonce}
99
- suppressHydrationWarning
100
- dangerouslySetInnerHTML={{
101
- __html: JSON.stringify(organizationJsonLd)
102
- .replace(/</g, '\\u003c')
103
- .replace(/>/g, '\\u003e')
104
- .replace(/&/g, '\\u0026'),
105
- }}
106
- />
107
- {/* Google Search Console verification — token set by the merchant in
108
- the dashboard (channel settings → Google site verification). Also
109
- what the Merchant Center website claim checks for. Renders nothing
110
- until a token is configured. */}
111
- {storeInfo?.seo?.googleSiteVerification ? (
112
- <meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
113
- ) : null}
114
- {/* RSS autodiscovery — feed readers and aggregators find the blog feed
115
- from any page. The route itself lives at /blog/rss.xml. */}
116
- <link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
117
- {/* Brainerce cookieless traffic analytics auto-tracks pageviews +
118
- SPA route changes. Cookieless (no consent banner needed); the
119
- merchant can toggle it per sales channel in the dashboard. */}
120
- <script
121
- defer
122
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
123
- data-channel={
124
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
125
- }
126
- nonce={nonce}
127
- suppressHydrationWarning
128
- />
129
- </head>
130
- <body className={`${sans.variable} ${serif.variable} font-sans`}>
131
- <StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
132
- {/* Mounted ahead of the page tree on purpose: React runs effects in
133
- tree order, so booting the tags here guarantees fbq/ttq exist by
134
- the time a page component fires its first e-commerce event. */}
135
- <TrackingBootstrap />
136
- <div className="min-h-screen flex flex-col">
137
- <AnnouncementBar announcements={announcements} />
138
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
139
- <main className="flex-1">{children}</main>
140
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} locale={locale} />
141
- </div>
142
- <CartDrawer />
143
- <BrainerceBotWidget />
144
- </StoreProvider>
145
- </body>
146
- </html>
147
- );
148
- }
149
- <% } else { %>
150
- import type { Metadata } from 'next';
151
- import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
152
- import { StoreProvider } from '@/core/providers/store-provider';
153
- import { AnnouncementBar } from '@/ui/layout/announcement-bar';
154
- import { SiteHeader } from '@/ui/layout/site-header';
155
- import { SiteFooter } from '@/ui/layout/site-footer';
156
- import { CartDrawer } from '@/ui/cart/cart-drawer';
157
- import { BrainerceBotWidget } from '@/components/brainerce-bot';
158
- import { TrackingBootstrap } from '@/components/tracking-bootstrap';
159
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
160
- import { getNonce } from '@/core/lib/nonce';
161
- import './globals.css';
162
-
163
- // Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
164
- // (Hebrew serif the gallery voice) for display. Exposed as CSS variables
165
- // consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
166
- const sans = Assistant({
167
- subsets: ['hebrew', 'latin'],
168
- weight: ['400', '500', '600', '700'],
169
- variable: '--font-sans',
170
- });
171
- const serif = Frank_Ruhl_Libre({
172
- subsets: ['hebrew', 'latin'],
173
- weight: ['400', '500', '700'],
174
- variable: '--font-serif',
175
- });
176
-
177
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
178
-
179
- export const metadata: Metadata = {
180
- metadataBase: new URL(baseUrl),
181
- title: {
182
- default: <%- storeNameJs %>,
183
- template: <%- titleTemplateJs %>,
184
- },
185
- description: <%- storeNameJs %>,
186
- alternates: {
187
- canonical: '/',
188
- },
189
- openGraph: {
190
- siteName: <%- storeNameJs %>,
191
- locale: '<%= ogLocale %>',
192
- type: 'website',
193
- },
194
- robots: {
195
- index: true,
196
- follow: true,
197
- },
198
- };
199
-
200
- const organizationJsonLd = {
201
- '@context': 'https://schema.org',
202
- '@type': 'Organization',
203
- name: <%- storeNameJs %>,
204
- url: baseUrl,
205
- };
206
-
207
- export default async function RootLayout({
208
- children,
209
- }: {
210
- children: React.ReactNode;
211
- }) {
212
- const nonce = await getNonce();
213
-
214
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
215
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
216
- // seeded a particular content type yet. New stores ship with default rows
217
- // seeded by the backend (StoresService.seedDefaultContent).
218
- const client = getServerClient();
219
- const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
220
- client.content.announcement.list().catch(() => []),
221
- client.content.header.get('main').catch(() => null),
222
- client.content.footer.get('main').catch(() => null),
223
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
224
- // render with the real currency, feature flags, and i18n config at frame 0
225
- // — without this, Googlebot sees USD/defaults baked into the HTML.
226
- fetchStoreInfo(),
227
- ]);
228
-
229
- return (
230
- <html lang="<%= language %>" dir="<%= direction %>">
231
- <head>
232
- <script
233
- type="application/ld+json"
234
- nonce={nonce}
235
- suppressHydrationWarning
236
- dangerouslySetInnerHTML={{
237
- __html: JSON.stringify(organizationJsonLd)
238
- .replace(/</g, '\\u003c')
239
- .replace(/>/g, '\\u003e')
240
- .replace(/&/g, '\\u0026'),
241
- }}
242
- />
243
- {/* Google Search Console verification — token set by the merchant in
244
- the dashboard (channel settings → Google site verification). Also
245
- what the Merchant Center website claim checks for. Renders nothing
246
- until a token is configured. */}
247
- {storeInfo?.seo?.googleSiteVerification ? (
248
- <meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
249
- ) : null}
250
- {/* RSS autodiscovery — feed readers and aggregators find the blog feed
251
- from any page. The route itself lives at /blog/rss.xml. */}
252
- <link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
253
- {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
254
- SPA route changes. Cookieless (no consent banner needed); the
255
- merchant can toggle it per sales channel in the dashboard. */}
256
- <script
257
- defer
258
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
259
- data-channel={
260
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
261
- }
262
- nonce={nonce}
263
- suppressHydrationWarning
264
- />
265
- </head>
266
- <body className={`${sans.variable} ${serif.variable} font-sans`}>
267
- <StoreProvider initialStoreInfo={storeInfo}>
268
- {/* Mounted ahead of the page tree on purpose: React runs effects in
269
- tree order, so booting the tags here guarantees fbq/ttq exist by
270
- the time a page component fires its first e-commerce event. */}
271
- <TrackingBootstrap />
272
- <div className="min-h-screen flex flex-col">
273
- <AnnouncementBar announcements={announcements} />
274
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
275
- <main className="flex-1">{children}</main>
276
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
277
- </div>
278
- <CartDrawer />
279
- <BrainerceBotWidget />
280
- </StoreProvider>
281
- </body>
282
- </html>
283
- );
284
- }
285
- <% } %>
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 { SiteFooter } from '@/ui/layout/site-footer';
8
+ import { CartDrawer } from '@/ui/cart/cart-drawer';
9
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
10
+ import { TrackingBootstrap } from '@/components/tracking-bootstrap';
11
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
12
+ import { getDirection, getMessages, supportedLocales } from '@/i18n';
13
+ import { getNonce } from '@/core/lib/nonce';
14
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
15
+ import '../globals.css';
16
+
17
+ // Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
18
+ // (Hebrew serif the gallery voice) for display. Exposed as CSS variables
19
+ // consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
20
+ const sans = Assistant({
21
+ subsets: ['hebrew', 'latin'],
22
+ weight: ['400', '500', '600', '700'],
23
+ variable: '--font-sans',
24
+ });
25
+ const serif = Frank_Ruhl_Libre({
26
+ subsets: ['hebrew', 'latin'],
27
+ weight: ['400', '500', '700'],
28
+ variable: '--font-serif',
29
+ });
30
+
31
+ // Resolved per request rather than read from a hardcoded env default: the
32
+ // scaffolder cannot know the deploy domain, so `metadataBase` is the one
33
+ // value that must never be guessed. See core/lib/site-url.ts.
34
+ export async function generateMetadata(): Promise<Metadata> {
35
+ const baseUrl = await getCanonicalSiteUrl();
36
+ return {
37
+ metadataBase: new URL(baseUrl),
38
+ title: {
39
+ default: <%- storeNameJs %>,
40
+ template: <%- titleTemplateJs %>,
41
+ },
42
+ description: <%- storeNameJs %>,
43
+ alternates: {
44
+ canonical: '/',
45
+ },
46
+ openGraph: {
47
+ siteName: <%- storeNameJs %>,
48
+ type: 'website',
49
+ },
50
+ robots: {
51
+ index: true,
52
+ follow: true,
53
+ },
54
+ };
55
+ }
56
+
57
+ function buildOrganizationJsonLd(baseUrl: string) {
58
+ return {
59
+ '@context': 'https://schema.org',
60
+ '@type': 'Organization',
61
+ name: <%- storeNameJs %>,
62
+ url: baseUrl,
63
+ };
64
+ }
65
+
66
+ export function generateStaticParams() {
67
+ return supportedLocales.map((locale) => ({ locale }));
68
+ }
69
+
70
+ export default async function RootLayout({
71
+ children,
72
+ params,
73
+ }: {
74
+ children: React.ReactNode;
75
+ params: Promise<{ locale: string }>;
76
+ }) {
77
+ const { locale } = await params;
78
+ const dir = getDirection(locale);
79
+ const nonce = await getNonce();
80
+ const baseUrl = await getCanonicalSiteUrl();
81
+
82
+ // Merchant-driven layout chrome — fetched server-side. Each call falls back
83
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
84
+ // seeded a particular content type yet. New stores ship with default rows
85
+ // seeded by the backend (StoresService.seedDefaultContent).
86
+ const client = await getServerClient(locale);
87
+ const [announcements, siteHeader, siteFooter, storeInfo, messages] = await Promise.all([
88
+ client.content.announcement.list(locale).catch(() => []),
89
+ client.content.header.get('main', locale).catch(() => null),
90
+ client.content.footer.get('main', locale).catch(() => null),
91
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
92
+ // render with the real currency, feature flags, and i18n config at frame 0
93
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
94
+ fetchStoreInfo(locale),
95
+ // SSR-fetch UI message strings so translated text renders at frame 0 —
96
+ // without this, every page (and every locale switch) shows raw
97
+ // "namespace.key" strings until the client-only fetch resolves.
98
+ getMessages(locale),
99
+ ]);
100
+
101
+ return (
102
+ <html lang={locale} dir={dir}>
103
+ <head>
104
+ <script
105
+ type="application/ld+json"
106
+ nonce={nonce}
107
+ suppressHydrationWarning
108
+ dangerouslySetInnerHTML={{
109
+ __html: JSON.stringify(buildOrganizationJsonLd(baseUrl))
110
+ .replace(/</g, '\\u003c')
111
+ .replace(/>/g, '\\u003e')
112
+ .replace(/&/g, '\\u0026'),
113
+ }}
114
+ />
115
+ {/* Google Search Console verification token set by the merchant in
116
+ the dashboard (channel settings → Google site verification). Also
117
+ what the Merchant Center website claim checks for. Renders nothing
118
+ until a token is configured. */}
119
+ {storeInfo?.seo?.googleSiteVerification ? (
120
+ <meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
121
+ ) : null}
122
+ {/* RSS autodiscovery — feed readers and aggregators find the blog feed
123
+ from any page. The route itself lives at /blog/rss.xml. */}
124
+ <link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
125
+ {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
126
+ SPA route changes. Cookieless (no consent banner needed); the
127
+ merchant can toggle it per sales channel in the dashboard. */}
128
+ <script
129
+ defer
130
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
131
+ data-channel={
132
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
133
+ }
134
+ nonce={nonce}
135
+ suppressHydrationWarning
136
+ />
137
+ </head>
138
+ <body className={`${sans.variable} ${serif.variable} font-sans`}>
139
+ <StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
140
+ {/* Mounted ahead of the page tree on purpose: React runs effects in
141
+ tree order, so booting the tags here guarantees fbq/ttq exist by
142
+ the time a page component fires its first e-commerce event. */}
143
+ <TrackingBootstrap />
144
+ <div className="min-h-screen flex flex-col">
145
+ <AnnouncementBar announcements={announcements} />
146
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
147
+ <main className="flex-1">{children}</main>
148
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} locale={locale} />
149
+ </div>
150
+ <CartDrawer />
151
+ <BrainerceBotWidget />
152
+ </StoreProvider>
153
+ </body>
154
+ </html>
155
+ );
156
+ }
157
+ <% } else { %>
158
+ import type { Metadata } from 'next';
159
+ import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
160
+ import { StoreProvider } from '@/core/providers/store-provider';
161
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
162
+ import { SiteHeader } from '@/ui/layout/site-header';
163
+ import { SiteFooter } from '@/ui/layout/site-footer';
164
+ import { CartDrawer } from '@/ui/cart/cart-drawer';
165
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
166
+ import { TrackingBootstrap } from '@/components/tracking-bootstrap';
167
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
168
+ import { getNonce } from '@/core/lib/nonce';
169
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
170
+ import './globals.css';
171
+
172
+ // Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
173
+ // (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
174
+ // consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
175
+ const sans = Assistant({
176
+ subsets: ['hebrew', 'latin'],
177
+ weight: ['400', '500', '600', '700'],
178
+ variable: '--font-sans',
179
+ });
180
+ const serif = Frank_Ruhl_Libre({
181
+ subsets: ['hebrew', 'latin'],
182
+ weight: ['400', '500', '700'],
183
+ variable: '--font-serif',
184
+ });
185
+
186
+ // Resolved per request rather than read from a hardcoded env default: the
187
+ // scaffolder cannot know the deploy domain, so `metadataBase` is the one
188
+ // value that must never be guessed. See core/lib/site-url.ts.
189
+ export async function generateMetadata(): Promise<Metadata> {
190
+ const baseUrl = await getCanonicalSiteUrl();
191
+ return {
192
+ metadataBase: new URL(baseUrl),
193
+ title: {
194
+ default: <%- storeNameJs %>,
195
+ template: <%- titleTemplateJs %>,
196
+ },
197
+ description: <%- storeNameJs %>,
198
+ alternates: {
199
+ canonical: '/',
200
+ },
201
+ openGraph: {
202
+ siteName: <%- storeNameJs %>,
203
+ locale: '<%= ogLocale %>',
204
+ type: 'website',
205
+ },
206
+ robots: {
207
+ index: true,
208
+ follow: true,
209
+ },
210
+ };
211
+ }
212
+
213
+ function buildOrganizationJsonLd(baseUrl: string) {
214
+ return {
215
+ '@context': 'https://schema.org',
216
+ '@type': 'Organization',
217
+ name: <%- storeNameJs %>,
218
+ url: baseUrl,
219
+ };
220
+ }
221
+
222
+ export default async function RootLayout({
223
+ children,
224
+ }: {
225
+ children: React.ReactNode;
226
+ }) {
227
+ const nonce = await getNonce();
228
+ const baseUrl = await getCanonicalSiteUrl();
229
+
230
+ // Merchant-driven layout chrome fetched server-side. Each call falls back
231
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
232
+ // seeded a particular content type yet. New stores ship with default rows
233
+ // seeded by the backend (StoresService.seedDefaultContent).
234
+ const client = await getServerClient();
235
+ const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
236
+ client.content.announcement.list().catch(() => []),
237
+ client.content.header.get('main').catch(() => null),
238
+ client.content.footer.get('main').catch(() => null),
239
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
240
+ // render with the real currency, feature flags, and i18n config at frame 0
241
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
242
+ fetchStoreInfo(),
243
+ ]);
244
+
245
+ return (
246
+ <html lang="<%= language %>" dir="<%= direction %>">
247
+ <head>
248
+ <script
249
+ type="application/ld+json"
250
+ nonce={nonce}
251
+ suppressHydrationWarning
252
+ dangerouslySetInnerHTML={{
253
+ __html: JSON.stringify(buildOrganizationJsonLd(baseUrl))
254
+ .replace(/</g, '\\u003c')
255
+ .replace(/>/g, '\\u003e')
256
+ .replace(/&/g, '\\u0026'),
257
+ }}
258
+ />
259
+ {/* Google Search Console verification — token set by the merchant in
260
+ the dashboard (channel settings → Google site verification). Also
261
+ what the Merchant Center website claim checks for. Renders nothing
262
+ until a token is configured. */}
263
+ {storeInfo?.seo?.googleSiteVerification ? (
264
+ <meta name="google-site-verification" content={storeInfo.seo.googleSiteVerification} />
265
+ ) : null}
266
+ {/* RSS autodiscovery — feed readers and aggregators find the blog feed
267
+ from any page. The route itself lives at /blog/rss.xml. */}
268
+ <link rel="alternate" type="application/rss+xml" title="Blog" href="/blog/rss.xml" />
269
+ {/* Brainerce cookieless traffic analytics auto-tracks pageviews +
270
+ SPA route changes. Cookieless (no consent banner needed); the
271
+ merchant can toggle it per sales channel in the dashboard. */}
272
+ <script
273
+ defer
274
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
275
+ data-channel={
276
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
277
+ }
278
+ nonce={nonce}
279
+ suppressHydrationWarning
280
+ />
281
+ </head>
282
+ <body className={`${sans.variable} ${serif.variable} font-sans`}>
283
+ <StoreProvider initialStoreInfo={storeInfo}>
284
+ {/* Mounted ahead of the page tree on purpose: React runs effects in
285
+ tree order, so booting the tags here guarantees fbq/ttq exist by
286
+ the time a page component fires its first e-commerce event. */}
287
+ <TrackingBootstrap />
288
+ <div className="min-h-screen flex flex-col">
289
+ <AnnouncementBar announcements={announcements} />
290
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
291
+ <main className="flex-1">{children}</main>
292
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
293
+ </div>
294
+ <CartDrawer />
295
+ <BrainerceBotWidget />
296
+ </StoreProvider>
297
+ </body>
298
+ </html>
299
+ );
300
+ }
301
+ <% } %>