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