create-brainerce-store 1.88.1 → 1.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -2
- package/messages/en.json +21 -0
- package/messages/he.json +21 -0
- package/package.json +3 -2
- package/templates/nextjs/base/.eslintrc.json +1 -0
- package/templates/nextjs/base/AI-GUIDE.md +35 -0
- package/templates/nextjs/base/TRANSLATIONS.md +6 -0
- package/templates/nextjs/base/scripts/agent-docs.mjs +76 -0
- package/templates/nextjs/base/scripts/connect.mjs +6 -0
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +102 -96
- package/templates/nextjs/base/src/app/blog/[slug]/{page.tsx.ejs → page.tsx} +41 -144
- package/templates/nextjs/base/src/app/blog/page.tsx +187 -0
- package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +7 -3
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +26 -11
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +20 -4
- package/templates/nextjs/base/src/app/layout.tsx.ejs +18 -50
- package/templates/nextjs/base/src/app/page.tsx +109 -75
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +7 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +267 -220
- package/templates/nextjs/base/src/app/products/page.tsx +184 -19
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +9 -1
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +30 -1
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +12 -6
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +9 -10
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +15 -6
- package/templates/nextjs/base/src/components/seo/product-og-meta.tsx +46 -0
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +24 -4
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +99 -116
- package/templates/nextjs/base/src/core/hooks/use-reviews-enabled.ts +17 -0
- package/templates/nextjs/base/src/core/lib/listing-query.ts +199 -0
- package/templates/nextjs/base/src/core/lib/locale-path.ts +29 -0
- package/templates/nextjs/base/src/core/lib/reviews-disabled.ts +19 -0
- package/templates/nextjs/base/src/core/lib/server-messages.ts.ejs +34 -0
- package/templates/nextjs/base/src/core/lib/store-info.ts +20 -0
- package/templates/nextjs/base/src/core/providers/listing-seed.tsx +66 -0
- package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +6 -2
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +18 -50
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +8 -2
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +7 -2
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +6 -2
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +6 -2
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +0 -277
- package/templates/nextjs/base/src/app/products/layout.tsx.ejs +0 -18
|
@@ -13,6 +13,7 @@ import { resolveRegion } from '@/core/lib/region.server';
|
|
|
13
13
|
import { getDirection, getMessages, supportedLocales } from '@/i18n';
|
|
14
14
|
import { getNonce } from '@/core/lib/nonce';
|
|
15
15
|
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
16
|
+
import { getServerNamespace } from '@/core/lib/server-messages';
|
|
16
17
|
import { resolveStoreName, buildTitleTemplate } from '@/core/lib/store-name';
|
|
17
18
|
import { buildMetaDescription } from '@/core/lib/seo';
|
|
18
19
|
import '../globals.css';
|
|
@@ -43,9 +44,6 @@ export async function generateMetadata({
|
|
|
43
44
|
template: buildTitleTemplate(storeName),
|
|
44
45
|
},
|
|
45
46
|
description: buildMetaDescription(storeInfo?.metaDescription) || storeName,
|
|
46
|
-
alternates: {
|
|
47
|
-
canonical: '/',
|
|
48
|
-
},
|
|
49
47
|
openGraph: {
|
|
50
48
|
siteName: storeName,
|
|
51
49
|
type: 'website',
|
|
@@ -57,15 +55,6 @@ export async function generateMetadata({
|
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
function buildOrganizationJsonLd(baseUrl: string, storeName: string) {
|
|
61
|
-
return {
|
|
62
|
-
'@context': 'https://schema.org',
|
|
63
|
-
'@type': 'Organization',
|
|
64
|
-
name: storeName,
|
|
65
|
-
url: baseUrl,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
58
|
export function generateStaticParams() {
|
|
70
59
|
return supportedLocales.map((locale) => ({ locale }));
|
|
71
60
|
}
|
|
@@ -80,7 +69,6 @@ export default async function RootLayout({
|
|
|
80
69
|
const { locale } = await params;
|
|
81
70
|
const dir = getDirection(locale);
|
|
82
71
|
const nonce = await getNonce();
|
|
83
|
-
const baseUrl = await getCanonicalSiteUrl();
|
|
84
72
|
|
|
85
73
|
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
86
74
|
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
@@ -116,21 +104,17 @@ export default async function RootLayout({
|
|
|
116
104
|
// One resolved name for the JSON-LD, the header and the footer. Live data
|
|
117
105
|
// first; the scaffold-time literal is only the last fallback.
|
|
118
106
|
const storeName = resolveStoreName(storeInfo);
|
|
107
|
+
// Feed title in the request locale (a literal "Blog" was every language's).
|
|
108
|
+
const seoStrings = await getServerNamespace('seo', locale);
|
|
119
109
|
|
|
120
110
|
return (
|
|
121
111
|
<html lang={locale} dir={dir}>
|
|
122
112
|
<head>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
__html: JSON.stringify(buildOrganizationJsonLd(baseUrl, storeName))
|
|
129
|
-
.replace(/</g, '\\u003c')
|
|
130
|
-
.replace(/>/g, '\\u003e')
|
|
131
|
-
.replace(/&/g, '\\u0026'),
|
|
132
|
-
}}
|
|
133
|
-
/>
|
|
113
|
+
{/* No Organization JSON-LD here. This layout wraps EVERY page, and the
|
|
114
|
+
homepage already emits the full Organization + WebSite block
|
|
115
|
+
(components/seo/organization-json-ld.tsx); a second, thinner
|
|
116
|
+
Organization on the same page is a duplicate entity, not a reinforcement.
|
|
117
|
+
Organization data belongs on the homepage only. */}
|
|
134
118
|
{/* Google Search Console verification — token set by the merchant in
|
|
135
119
|
the dashboard (channel settings → Google site verification). Also
|
|
136
120
|
what the Merchant Center website claim checks for. Renders nothing
|
|
@@ -140,7 +124,7 @@ export default async function RootLayout({
|
|
|
140
124
|
) : null}
|
|
141
125
|
{/* RSS autodiscovery — feed readers and aggregators find the blog feed
|
|
142
126
|
from any page. The route itself lives at /blog/rss.xml. */}
|
|
143
|
-
<link rel="alternate" type="application/rss+xml" title=
|
|
127
|
+
<link rel="alternate" type="application/rss+xml" title={seoStrings.blog ?? ''} href="/blog/rss.xml" />
|
|
144
128
|
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
145
129
|
SPA route changes. Cookieless (no consent banner needed); the
|
|
146
130
|
merchant can toggle it per sales channel in the dashboard. */}
|
|
@@ -194,6 +178,7 @@ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
|
194
178
|
import { resolveRegion } from '@/core/lib/region.server';
|
|
195
179
|
import { getNonce } from '@/core/lib/nonce';
|
|
196
180
|
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
181
|
+
import { getServerNamespace } from '@/core/lib/server-messages';
|
|
197
182
|
import { resolveStoreName, buildTitleTemplate } from '@/core/lib/store-name';
|
|
198
183
|
import { buildMetaDescription } from '@/core/lib/seo';
|
|
199
184
|
import './globals.css';
|
|
@@ -219,9 +204,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
219
204
|
template: buildTitleTemplate(storeName),
|
|
220
205
|
},
|
|
221
206
|
description: buildMetaDescription(storeInfo?.metaDescription) || storeName,
|
|
222
|
-
alternates: {
|
|
223
|
-
canonical: '/',
|
|
224
|
-
},
|
|
225
207
|
openGraph: {
|
|
226
208
|
siteName: storeName,
|
|
227
209
|
locale: '<%= ogLocale %>',
|
|
@@ -234,22 +216,12 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
234
216
|
};
|
|
235
217
|
}
|
|
236
218
|
|
|
237
|
-
function buildOrganizationJsonLd(baseUrl: string, storeName: string) {
|
|
238
|
-
return {
|
|
239
|
-
'@context': 'https://schema.org',
|
|
240
|
-
'@type': 'Organization',
|
|
241
|
-
name: storeName,
|
|
242
|
-
url: baseUrl,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
219
|
export default async function RootLayout({
|
|
247
220
|
children,
|
|
248
221
|
}: {
|
|
249
222
|
children: React.ReactNode;
|
|
250
223
|
}) {
|
|
251
224
|
const nonce = await getNonce();
|
|
252
|
-
const baseUrl = await getCanonicalSiteUrl();
|
|
253
225
|
|
|
254
226
|
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
255
227
|
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
@@ -281,21 +253,17 @@ export default async function RootLayout({
|
|
|
281
253
|
// One resolved name for the JSON-LD, the header and the footer. Live data
|
|
282
254
|
// first; the scaffold-time literal is only the last fallback.
|
|
283
255
|
const storeName = resolveStoreName(storeInfo);
|
|
256
|
+
// Feed title in the request locale (a literal "Blog" was every language's).
|
|
257
|
+
const seoStrings = await getServerNamespace('seo');
|
|
284
258
|
|
|
285
259
|
return (
|
|
286
260
|
<html lang="<%= language %>" dir="<%= direction %>">
|
|
287
261
|
<head>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
__html: JSON.stringify(buildOrganizationJsonLd(baseUrl, storeName))
|
|
294
|
-
.replace(/</g, '\\u003c')
|
|
295
|
-
.replace(/>/g, '\\u003e')
|
|
296
|
-
.replace(/&/g, '\\u0026'),
|
|
297
|
-
}}
|
|
298
|
-
/>
|
|
262
|
+
{/* No Organization JSON-LD here. This layout wraps EVERY page, and the
|
|
263
|
+
homepage already emits the full Organization + WebSite block
|
|
264
|
+
(components/seo/organization-json-ld.tsx); a second, thinner
|
|
265
|
+
Organization on the same page is a duplicate entity, not a reinforcement.
|
|
266
|
+
Organization data belongs on the homepage only. */}
|
|
299
267
|
{/* Google Search Console verification — token set by the merchant in
|
|
300
268
|
the dashboard (channel settings → Google site verification). Also
|
|
301
269
|
what the Merchant Center website claim checks for. Renders nothing
|
|
@@ -305,7 +273,7 @@ export default async function RootLayout({
|
|
|
305
273
|
) : null}
|
|
306
274
|
{/* RSS autodiscovery — feed readers and aggregators find the blog feed
|
|
307
275
|
from any page. The route itself lives at /blog/rss.xml. */}
|
|
308
|
-
<link rel="alternate" type="application/rss+xml" title=
|
|
276
|
+
<link rel="alternate" type="application/rss+xml" title={seoStrings.blog ?? ''} href="/blog/rss.xml" />
|
|
309
277
|
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
310
278
|
SPA route changes. Cookieless (no consent banner needed); the
|
|
311
279
|
merchant can toggle it per sales channel in the dashboard. */}
|
|
@@ -1,75 +1,109 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
import { Suspense } from 'react';
|
|
3
|
-
import { getServerClient } from '@/core/lib/brainerce.server';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { getServerClient } from '@/core/lib/brainerce.server';
|
|
4
|
+
import { getRegionId } from '@/core/lib/region.server';
|
|
5
|
+
import { HOME_FEATURED_LIMIT } from '@/core/lib/listing-query';
|
|
6
|
+
import { localePath } from '@/core/lib/locale-path';
|
|
7
|
+
import { buildMetaDescription } from '@/core/lib/seo';
|
|
8
|
+
import { OrganizationJsonLd } from '@/components/seo/organization-json-ld';
|
|
9
|
+
import { getCanonicalSiteUrl } from '@/core/lib/site-url';
|
|
10
|
+
import { ListingSeedProvider, type HomeSeed } from '@/core/providers/listing-seed';
|
|
11
|
+
import { RichTextBlock } from '@/ui/layout/rich-text-block';
|
|
12
|
+
import { HomeClient } from '@/ui/home/home-client';
|
|
13
|
+
import { ReferralGreeting } from '@/components/referral/referral-greeting';
|
|
14
|
+
|
|
15
|
+
// `locale` exists only on a multi-language storefront (the `[locale]` segment);
|
|
16
|
+
// a single-language one has no params at all.
|
|
17
|
+
type Props = { params: Promise<{ locale?: string }> };
|
|
18
|
+
|
|
19
|
+
// Build homepage metadata server-side so Google, Facebook, WhatsApp etc.
|
|
20
|
+
// see a real <meta name="description"> on first request. Falls back to the
|
|
21
|
+
// store name when the merchant hasn't authored a meta description yet.
|
|
22
|
+
//
|
|
23
|
+
// The canonical lives HERE, not in the root layout: a layout-level canonical is
|
|
24
|
+
// inherited by every page that does not set its own, which told Google that the
|
|
25
|
+
// FAQ, the blog and every other page were duplicates of the homepage.
|
|
26
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
27
|
+
try {
|
|
28
|
+
const { locale } = await params;
|
|
29
|
+
const storeInfo = await (await getServerClient(locale)).getStoreInfo();
|
|
30
|
+
const description = buildMetaDescription(storeInfo.metaDescription) || undefined;
|
|
31
|
+
return {
|
|
32
|
+
title: storeInfo.name,
|
|
33
|
+
description,
|
|
34
|
+
alternates: { canonical: localePath(locale, '/') },
|
|
35
|
+
// `openGraph` replaces the layout's object wholesale rather than merging
|
|
36
|
+
// into it, so `siteName` has to be repeated or the tag disappears.
|
|
37
|
+
openGraph: {
|
|
38
|
+
title: storeInfo.name,
|
|
39
|
+
description,
|
|
40
|
+
siteName: storeInfo.name,
|
|
41
|
+
type: 'website',
|
|
42
|
+
},
|
|
43
|
+
twitter: {
|
|
44
|
+
card: 'summary_large_image',
|
|
45
|
+
title: storeInfo.name,
|
|
46
|
+
description,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function HomePage({ params }: Props) {
|
|
55
|
+
const { locale } = await params;
|
|
56
|
+
// Fetch store info server-side so the JSON-LD is in the initial HTML —
|
|
57
|
+
// crawlers and AI Overviews see it without needing to execute JavaScript.
|
|
58
|
+
// Failure here is non-fatal: the client component re-fetches via the
|
|
59
|
+
// StoreProvider so the page still renders.
|
|
60
|
+
//
|
|
61
|
+
// Also fetch the merchant's homepage intro (RICH_TEXT key='main'). When
|
|
62
|
+
// present, it renders as a sanitized HTML block above the product grid —
|
|
63
|
+
// typical use is brand introduction, value-prop copy, or a seasonal
|
|
64
|
+
// announcement that needs more formatting than the AnnouncementBar.
|
|
65
|
+
// When absent, RichTextBlock returns null cleanly so the layout collapses
|
|
66
|
+
// to just the product grid.
|
|
67
|
+
const client = await getServerClient(locale);
|
|
68
|
+
const regionId = await getRegionId();
|
|
69
|
+
const [storeInfo, intro, featured] = await Promise.all([
|
|
70
|
+
client.getStoreInfo().catch(() => null),
|
|
71
|
+
client.content.richText.get('main').catch(() => null),
|
|
72
|
+
// The featured grid, fetched here so the initial HTML carries real
|
|
73
|
+
// products. Without it the page was a spinner until the browser ran
|
|
74
|
+
// JavaScript, which is all a non-rendering crawler ever saw of the
|
|
75
|
+
// homepage. A failure is non-fatal: the client hook fetches instead.
|
|
76
|
+
client
|
|
77
|
+
.getProducts({
|
|
78
|
+
limit: HOME_FEATURED_LIMIT,
|
|
79
|
+
sortBy: 'createdAt',
|
|
80
|
+
sortOrder: 'desc',
|
|
81
|
+
...(regionId ? { regionId } : {}),
|
|
82
|
+
})
|
|
83
|
+
.catch(() => null),
|
|
84
|
+
]);
|
|
85
|
+
const homeSeed: HomeSeed | null = featured ? { products: featured.data, regionId } : null;
|
|
86
|
+
|
|
87
|
+
const baseUrl = await getCanonicalSiteUrl();
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
{storeInfo && baseUrl ? <OrganizationJsonLd storeInfo={storeInfo} baseUrl={baseUrl} /> : null}
|
|
92
|
+
{/* Referral landing. A referral link is `/?ref=<code>`, so it lands here.
|
|
93
|
+
Renders nothing without a valid `?ref=`, and nothing at all on a store
|
|
94
|
+
with referrals switched off. The Suspense boundary is required: the
|
|
95
|
+
component reads `useSearchParams()`, which would otherwise opt this
|
|
96
|
+
whole page out of static rendering. */}
|
|
97
|
+
<Suspense fallback={null}>
|
|
98
|
+
<ReferralGreeting className="mt-6" />
|
|
99
|
+
</Suspense>
|
|
100
|
+
<RichTextBlock
|
|
101
|
+
block={intro}
|
|
102
|
+
className="prose prose-sm dark:prose-invert mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8"
|
|
103
|
+
/>
|
|
104
|
+
<ListingSeedProvider home={homeSeed}>
|
|
105
|
+
<HomeClient />
|
|
106
|
+
</ListingSeedProvider>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -16,6 +16,7 @@ import type { Metadata } from 'next';
|
|
|
16
16
|
import { notFound } from 'next/navigation';
|
|
17
17
|
|
|
18
18
|
import { getServerClient } from '@/core/lib/brainerce.server';
|
|
19
|
+
import { localePath } from '@/core/lib/locale-path';
|
|
19
20
|
import { sanitizeHtml } from '@/core/lib/sanitize';
|
|
20
21
|
import { decodeSlug } from '@/core/lib/utils';
|
|
21
22
|
|
|
@@ -33,6 +34,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
33
34
|
return {
|
|
34
35
|
title: seo.title ?? page.data.title,
|
|
35
36
|
description: seo.description,
|
|
37
|
+
// Canonical per page: the root layout no longer sets one, and the one it
|
|
38
|
+
// used to set named the homepage as the canonical of every page without its own.
|
|
39
|
+
alternates: { canonical: localePath(locale, `/pages/${slug}`) },
|
|
36
40
|
openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
@@ -71,6 +75,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
71
75
|
return {
|
|
72
76
|
title: seo.title ?? page.data.title,
|
|
73
77
|
description: seo.description,
|
|
78
|
+
// Canonical per page: the root layout no longer sets one, and the one it
|
|
79
|
+
// used to set named the homepage as the canonical of every page without its own.
|
|
80
|
+
alternates: { canonical: localePath(undefined, `/pages/${slug}`) },
|
|
74
81
|
openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
|
|
75
82
|
};
|
|
76
83
|
}
|