create-brainerce-store 1.52.5 → 1.54.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.
@@ -1,227 +1,227 @@
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 { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
- import { getDirection, getMessages, supportedLocales } from '@/i18n';
11
- import { getNonce } from '@/core/lib/nonce';
12
- import '../globals.css';
13
-
14
- <%- fontVariable %>
15
-
16
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
17
-
18
- export const metadata: Metadata = {
19
- metadataBase: new URL(baseUrl),
20
- title: {
21
- default: <%- storeNameJs %>,
22
- template: <%- titleTemplateJs %>,
23
- },
24
- description: <%- storeNameJs %>,
25
- alternates: {
26
- canonical: '/',
27
- },
28
- openGraph: {
29
- siteName: <%- storeNameJs %>,
30
- type: 'website',
31
- },
32
- robots: {
33
- index: true,
34
- follow: true,
35
- },
36
- };
37
-
38
- const organizationJsonLd = {
39
- '@context': 'https://schema.org',
40
- '@type': 'Organization',
41
- name: <%- storeNameJs %>,
42
- url: baseUrl,
43
- };
44
-
45
- export function generateStaticParams() {
46
- return supportedLocales.map((locale) => ({ locale }));
47
- }
48
-
49
- export default async function RootLayout({
50
- children,
51
- params,
52
- }: {
53
- children: React.ReactNode;
54
- params: Promise<{ locale: string }>;
55
- }) {
56
- const { locale } = await params;
57
- const dir = getDirection(locale);
58
- const nonce = await getNonce();
59
-
60
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
61
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
62
- // seeded a particular content type yet. New stores ship with default rows
63
- // seeded by the backend (StoresService.seedDefaultContent).
64
- const client = getServerClient(locale);
65
- const [announcements, siteHeader, siteFooter, storeInfo, messages] = await Promise.all([
66
- client.content.announcement.list(locale).catch(() => []),
67
- client.content.header.get('main', locale).catch(() => null),
68
- client.content.footer.get('main', locale).catch(() => null),
69
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
70
- // render with the real currency, feature flags, and i18n config at frame 0
71
- // — without this, Googlebot sees USD/defaults baked into the HTML.
72
- fetchStoreInfo(locale),
73
- // SSR-fetch UI message strings so translated text renders at frame 0 —
74
- // without this, every page (and every locale switch) shows raw
75
- // "namespace.key" strings until the client-only fetch resolves.
76
- getMessages(locale),
77
- ]);
78
-
79
- return (
80
- <html lang={locale} dir={dir}>
81
- <head>
82
- <script
83
- type="application/ld+json"
84
- nonce={nonce}
85
- suppressHydrationWarning
86
- dangerouslySetInnerHTML={{
87
- __html: JSON.stringify(organizationJsonLd)
88
- .replace(/</g, '\\u003c')
89
- .replace(/>/g, '\\u003e')
90
- .replace(/&/g, '\\u0026'),
91
- }}
92
- />
93
- {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
94
- SPA route changes. Cookieless (no consent banner needed); the
95
- merchant can toggle it per sales channel in the dashboard. */}
96
- <script
97
- defer
98
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
99
- data-channel={
100
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
101
- }
102
- nonce={nonce}
103
- suppressHydrationWarning
104
- />
105
- </head>
106
- <body className={font.className}>
107
- <StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
108
- <div className="min-h-screen flex flex-col">
109
- <AnnouncementBar announcements={announcements} />
110
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
111
- <main className="flex-1">{children}</main>
112
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
113
- </div>
114
- <BrainerceBotWidget />
115
- </StoreProvider>
116
- </body>
117
- </html>
118
- );
119
- }
120
- <% } else { %>
121
- import type { Metadata } from 'next';
122
- <%- fontImport %>
123
- import { StoreProvider } from '@/core/providers/store-provider';
124
- import { AnnouncementBar } from '@/ui/layout/announcement-bar';
125
- import { SiteHeader } from '@/ui/layout/site-header';
126
- import { SiteFooter } from '@/ui/layout/site-footer';
127
- import { BrainerceBotWidget } from '@/components/brainerce-bot';
128
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
129
- import { getNonce } from '@/core/lib/nonce';
130
- import './globals.css';
131
-
132
- <%- fontVariable %>
133
-
134
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
135
-
136
- export const metadata: Metadata = {
137
- metadataBase: new URL(baseUrl),
138
- title: {
139
- default: <%- storeNameJs %>,
140
- template: <%- titleTemplateJs %>,
141
- },
142
- description: <%- storeNameJs %>,
143
- alternates: {
144
- canonical: '/',
145
- },
146
- openGraph: {
147
- siteName: <%- storeNameJs %>,
148
- locale: '<%= ogLocale %>',
149
- type: 'website',
150
- },
151
- robots: {
152
- index: true,
153
- follow: true,
154
- },
155
- };
156
-
157
- const organizationJsonLd = {
158
- '@context': 'https://schema.org',
159
- '@type': 'Organization',
160
- name: <%- storeNameJs %>,
161
- url: baseUrl,
162
- };
163
-
164
- export default async function RootLayout({
165
- children,
166
- }: {
167
- children: React.ReactNode;
168
- }) {
169
- const nonce = await getNonce();
170
-
171
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
172
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
173
- // seeded a particular content type yet. New stores ship with default rows
174
- // seeded by the backend (StoresService.seedDefaultContent).
175
- const client = getServerClient();
176
- const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
177
- client.content.announcement.list().catch(() => []),
178
- client.content.header.get('main').catch(() => null),
179
- client.content.footer.get('main').catch(() => null),
180
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
181
- // render with the real currency, feature flags, and i18n config at frame 0
182
- // — without this, Googlebot sees USD/defaults baked into the HTML.
183
- fetchStoreInfo(),
184
- ]);
185
-
186
- return (
187
- <html lang="<%= language %>" dir="<%= direction %>">
188
- <head>
189
- <script
190
- type="application/ld+json"
191
- nonce={nonce}
192
- suppressHydrationWarning
193
- dangerouslySetInnerHTML={{
194
- __html: JSON.stringify(organizationJsonLd)
195
- .replace(/</g, '\\u003c')
196
- .replace(/>/g, '\\u003e')
197
- .replace(/&/g, '\\u0026'),
198
- }}
199
- />
200
- {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
201
- SPA route changes. Cookieless (no consent banner needed); the
202
- merchant can toggle it per sales channel in the dashboard. */}
203
- <script
204
- defer
205
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
206
- data-channel={
207
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
208
- }
209
- nonce={nonce}
210
- suppressHydrationWarning
211
- />
212
- </head>
213
- <body className={font.className}>
214
- <StoreProvider initialStoreInfo={storeInfo}>
215
- <div className="min-h-screen flex flex-col">
216
- <AnnouncementBar announcements={announcements} />
217
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
218
- <main className="flex-1">{children}</main>
219
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
220
- </div>
221
- <BrainerceBotWidget />
222
- </StoreProvider>
223
- </body>
224
- </html>
225
- );
226
- }
227
- <% } %>
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 { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
+ import { getDirection, getMessages, supportedLocales } from '@/i18n';
11
+ import { getNonce } from '@/core/lib/nonce';
12
+ import '../globals.css';
13
+
14
+ <%- fontVariable %>
15
+
16
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
17
+
18
+ export const metadata: Metadata = {
19
+ metadataBase: new URL(baseUrl),
20
+ title: {
21
+ default: <%- storeNameJs %>,
22
+ template: <%- titleTemplateJs %>,
23
+ },
24
+ description: <%- storeNameJs %>,
25
+ alternates: {
26
+ canonical: '/',
27
+ },
28
+ openGraph: {
29
+ siteName: <%- storeNameJs %>,
30
+ type: 'website',
31
+ },
32
+ robots: {
33
+ index: true,
34
+ follow: true,
35
+ },
36
+ };
37
+
38
+ const organizationJsonLd = {
39
+ '@context': 'https://schema.org',
40
+ '@type': 'Organization',
41
+ name: <%- storeNameJs %>,
42
+ url: baseUrl,
43
+ };
44
+
45
+ export function generateStaticParams() {
46
+ return supportedLocales.map((locale) => ({ locale }));
47
+ }
48
+
49
+ export default async function RootLayout({
50
+ children,
51
+ params,
52
+ }: {
53
+ children: React.ReactNode;
54
+ params: Promise<{ locale: string }>;
55
+ }) {
56
+ const { locale } = await params;
57
+ const dir = getDirection(locale);
58
+ const nonce = await getNonce();
59
+
60
+ // Merchant-driven layout chrome — fetched server-side. Each call falls back
61
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
62
+ // seeded a particular content type yet. New stores ship with default rows
63
+ // seeded by the backend (StoresService.seedDefaultContent).
64
+ const client = getServerClient(locale);
65
+ const [announcements, siteHeader, siteFooter, storeInfo, messages] = await Promise.all([
66
+ client.content.announcement.list(locale).catch(() => []),
67
+ client.content.header.get('main', locale).catch(() => null),
68
+ client.content.footer.get('main', locale).catch(() => null),
69
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
70
+ // render with the real currency, feature flags, and i18n config at frame 0
71
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
72
+ fetchStoreInfo(locale),
73
+ // SSR-fetch UI message strings so translated text renders at frame 0 —
74
+ // without this, every page (and every locale switch) shows raw
75
+ // "namespace.key" strings until the client-only fetch resolves.
76
+ getMessages(locale),
77
+ ]);
78
+
79
+ return (
80
+ <html lang={locale} dir={dir}>
81
+ <head>
82
+ <script
83
+ type="application/ld+json"
84
+ nonce={nonce}
85
+ suppressHydrationWarning
86
+ dangerouslySetInnerHTML={{
87
+ __html: JSON.stringify(organizationJsonLd)
88
+ .replace(/</g, '\\u003c')
89
+ .replace(/>/g, '\\u003e')
90
+ .replace(/&/g, '\\u0026'),
91
+ }}
92
+ />
93
+ {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
94
+ SPA route changes. Cookieless (no consent banner needed); the
95
+ merchant can toggle it per sales channel in the dashboard. */}
96
+ <script
97
+ defer
98
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
99
+ data-channel={
100
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
101
+ }
102
+ nonce={nonce}
103
+ suppressHydrationWarning
104
+ />
105
+ </head>
106
+ <body className={font.className}>
107
+ <StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
108
+ <div className="min-h-screen flex flex-col">
109
+ <AnnouncementBar announcements={announcements} />
110
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
111
+ <main className="flex-1">{children}</main>
112
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
113
+ </div>
114
+ <BrainerceBotWidget />
115
+ </StoreProvider>
116
+ </body>
117
+ </html>
118
+ );
119
+ }
120
+ <% } else { %>
121
+ import type { Metadata } from 'next';
122
+ <%- fontImport %>
123
+ import { StoreProvider } from '@/core/providers/store-provider';
124
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
125
+ import { SiteHeader } from '@/ui/layout/site-header';
126
+ import { SiteFooter } from '@/ui/layout/site-footer';
127
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
128
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
129
+ import { getNonce } from '@/core/lib/nonce';
130
+ import './globals.css';
131
+
132
+ <%- fontVariable %>
133
+
134
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
135
+
136
+ export const metadata: Metadata = {
137
+ metadataBase: new URL(baseUrl),
138
+ title: {
139
+ default: <%- storeNameJs %>,
140
+ template: <%- titleTemplateJs %>,
141
+ },
142
+ description: <%- storeNameJs %>,
143
+ alternates: {
144
+ canonical: '/',
145
+ },
146
+ openGraph: {
147
+ siteName: <%- storeNameJs %>,
148
+ locale: '<%= ogLocale %>',
149
+ type: 'website',
150
+ },
151
+ robots: {
152
+ index: true,
153
+ follow: true,
154
+ },
155
+ };
156
+
157
+ const organizationJsonLd = {
158
+ '@context': 'https://schema.org',
159
+ '@type': 'Organization',
160
+ name: <%- storeNameJs %>,
161
+ url: baseUrl,
162
+ };
163
+
164
+ export default async function RootLayout({
165
+ children,
166
+ }: {
167
+ children: React.ReactNode;
168
+ }) {
169
+ const nonce = await getNonce();
170
+
171
+ // Merchant-driven layout chrome — fetched server-side. Each call falls back
172
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
173
+ // seeded a particular content type yet. New stores ship with default rows
174
+ // seeded by the backend (StoresService.seedDefaultContent).
175
+ const client = getServerClient();
176
+ const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
177
+ client.content.announcement.list().catch(() => []),
178
+ client.content.header.get('main').catch(() => null),
179
+ client.content.footer.get('main').catch(() => null),
180
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
181
+ // render with the real currency, feature flags, and i18n config at frame 0
182
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
183
+ fetchStoreInfo(),
184
+ ]);
185
+
186
+ return (
187
+ <html lang="<%= language %>" dir="<%= direction %>">
188
+ <head>
189
+ <script
190
+ type="application/ld+json"
191
+ nonce={nonce}
192
+ suppressHydrationWarning
193
+ dangerouslySetInnerHTML={{
194
+ __html: JSON.stringify(organizationJsonLd)
195
+ .replace(/</g, '\\u003c')
196
+ .replace(/>/g, '\\u003e')
197
+ .replace(/&/g, '\\u0026'),
198
+ }}
199
+ />
200
+ {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
201
+ SPA route changes. Cookieless (no consent banner needed); the
202
+ merchant can toggle it per sales channel in the dashboard. */}
203
+ <script
204
+ defer
205
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
206
+ data-channel={
207
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
208
+ }
209
+ nonce={nonce}
210
+ suppressHydrationWarning
211
+ />
212
+ </head>
213
+ <body className={font.className}>
214
+ <StoreProvider initialStoreInfo={storeInfo}>
215
+ <div className="min-h-screen flex flex-col">
216
+ <AnnouncementBar announcements={announcements} />
217
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
218
+ <main className="flex-1">{children}</main>
219
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
220
+ </div>
221
+ <BrainerceBotWidget />
222
+ </StoreProvider>
223
+ </body>
224
+ </html>
225
+ );
226
+ }
227
+ <% } %>
@@ -0,0 +1,44 @@
1
+ /**
2
+ * llms.txt — GET /llms.txt
3
+ *
4
+ * A machine-readable site summary for AI answer engines (ChatGPT, Perplexity,
5
+ * Claude, AI Overviews). Emerging convention: a concise markdown map of what
6
+ * the site is and where its key content lives, so AI crawlers ground answers
7
+ * in (and cite) the right pages.
8
+ */
9
+ import { getServerClient } from '@/core/lib/brainerce';
10
+
11
+ export const revalidate = 3600;
12
+
13
+ export async function GET() {
14
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
15
+ const client = getServerClient();
16
+
17
+ const [info, posts] = await Promise.all([
18
+ client.getStoreInfo().catch(() => null),
19
+ client.blog.getPosts({ limit: 20 }).catch(() => null),
20
+ ]);
21
+
22
+ const lines: string[] = [
23
+ `# ${info?.name ?? 'Store'}`,
24
+ '',
25
+ ...(info?.metaDescription ? [`> ${info.metaDescription}`, ''] : []),
26
+ '## Key pages',
27
+ '',
28
+ `- [Products](${baseUrl}/products): full catalog`,
29
+ `- [Blog](${baseUrl}/blog): guides and articles`,
30
+ ...(info?.contactEmail ? [`- Contact: ${info.contactEmail}`] : []),
31
+ ];
32
+
33
+ if (posts && posts.data.length > 0) {
34
+ lines.push('', '## Recent articles', '');
35
+ for (const post of posts.data) {
36
+ const summary = post.seoDescription ?? post.excerpt ?? '';
37
+ lines.push(`- [${post.title}](${baseUrl}/blog/${post.slug})${summary ? `: ${summary}` : ''}`);
38
+ }
39
+ }
40
+
41
+ return new Response(lines.join('\n') + '\n', {
42
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
43
+ });
44
+ }
@@ -1,4 +1,5 @@
1
1
  import type { MetadataRoute } from 'next';
2
+ import { getBlogSitemapEntries, getCategorySitemapEntries } from 'brainerce';
2
3
  import { getServerClient } from '@/core/lib/brainerce';
3
4
 
4
5
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
@@ -42,7 +43,23 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
42
43
  });
43
44
  });
44
45
 
45
- return [...staticPages, ...productPages];
46
+ // Category (collection) pages — the highest-value organic-SEO surface;
47
+ // must be crawlable so they can rank for broad research-intent queries.
48
+ const categoryPages: MetadataRoute.Sitemap = await getCategorySitemapEntries(client, {
49
+ siteUrl: baseUrl,
50
+ locales: isMultiLocale ? supportedLocales : undefined,
51
+ defaultLocale: defaultLoc,
52
+ }).catch(() => []);
53
+
54
+ // Blog posts — the SEO Autopilot (and manual publishes) must be
55
+ // discoverable; missing blog entries = articles never get crawled.
56
+ const blogPages: MetadataRoute.Sitemap = await getBlogSitemapEntries(client, {
57
+ siteUrl: baseUrl,
58
+ locales: isMultiLocale ? supportedLocales : undefined,
59
+ defaultLocale: defaultLoc,
60
+ }).catch(() => []);
61
+
62
+ return [...staticPages, ...productPages, ...categoryPages, ...blogPages];
46
63
  } catch {
47
64
  return staticPages;
48
65
  }
@@ -34,6 +34,13 @@ export function BrainerceBotWidget() {
34
34
  BrainerceBot.mount({
35
35
  connectionId,
36
36
  baseUrl: process.env.NEXT_PUBLIC_BRAINERCE_API_URL || undefined,
37
+ // Order-lookup login gating: this template's own same-origin BFF proxy
38
+ // (see src/app/api/store/[...path]/route.ts) already forwards the
39
+ // shopper's httpOnly session cookie as a Bearer header — the same
40
+ // mechanism @/core/lib/brainerce's own client uses. Opting the bot in
41
+ // here lets it detect login state without the widget ever touching the
42
+ // cookie itself.
43
+ customerSessionProxyPath: '/api/store',
37
44
  onAddToCart: async ({ productId, variantId, quantity }) => {
38
45
  try {
39
46
  const { getClient } = await import('@/core/lib/brainerce');