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,64 +1,65 @@
1
- import type { Metadata } from 'next';
2
- import { getServerClient } from '@/core/lib/brainerce';
3
- import { buildMetaDescription } from '@/core/lib/seo';
4
- import { OrganizationJsonLd } from '@/components/seo/organization-json-ld';
5
- import { RichTextBlock } from '@/ui/layout/rich-text-block';
6
- import { HomeClient } from '@/ui/home/home-client';
7
-
8
- // Build homepage metadata server-side so Google, Facebook, WhatsApp etc.
9
- // see a real <meta name="description"> on first request. Falls back to the
10
- // store name when the merchant hasn't authored a meta description yet.
11
- export async function generateMetadata(): Promise<Metadata> {
12
- try {
13
- const storeInfo = await getServerClient().getStoreInfo();
14
- const description = buildMetaDescription(storeInfo.metaDescription) || undefined;
15
- return {
16
- title: storeInfo.name,
17
- description,
18
- openGraph: {
19
- title: storeInfo.name,
20
- description,
21
- type: 'website',
22
- },
23
- twitter: {
24
- card: 'summary_large_image',
25
- title: storeInfo.name,
26
- description,
27
- },
28
- };
29
- } catch {
30
- return {};
31
- }
32
- }
33
-
34
- export default async function HomePage() {
35
- // Fetch store info server-side so the JSON-LD is in the initial HTML —
36
- // crawlers and AI Overviews see it without needing to execute JavaScript.
37
- // Failure here is non-fatal: the client component re-fetches via the
38
- // StoreProvider so the page still renders.
39
- //
40
- // Also fetch the merchant's homepage intro (RICH_TEXT key='main'). When
41
- // present, it renders as a sanitized HTML block above the product grid —
42
- // typical use is brand introduction, value-prop copy, or a seasonal
43
- // announcement that needs more formatting than the AnnouncementBar.
44
- // When absent, RichTextBlock returns null cleanly so the layout collapses
45
- // to just the product grid.
46
- const client = getServerClient();
47
- const [storeInfo, intro] = await Promise.all([
48
- client.getStoreInfo().catch(() => null),
49
- client.content.richText.get('main').catch(() => null),
50
- ]);
51
-
52
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
53
-
54
- return (
55
- <>
56
- {storeInfo && baseUrl ? <OrganizationJsonLd storeInfo={storeInfo} baseUrl={baseUrl} /> : null}
57
- <RichTextBlock
58
- block={intro}
59
- className="prose prose-sm dark:prose-invert mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8"
60
- />
61
- <HomeClient />
62
- </>
63
- );
64
- }
1
+ import type { Metadata } from 'next';
2
+ import { getServerClient } from '@/core/lib/brainerce.server';
3
+ import { buildMetaDescription } from '@/core/lib/seo';
4
+ import { OrganizationJsonLd } from '@/components/seo/organization-json-ld';
5
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
6
+ import { RichTextBlock } from '@/ui/layout/rich-text-block';
7
+ import { HomeClient } from '@/ui/home/home-client';
8
+
9
+ // Build homepage metadata server-side so Google, Facebook, WhatsApp etc.
10
+ // see a real <meta name="description"> on first request. Falls back to the
11
+ // store name when the merchant hasn't authored a meta description yet.
12
+ export async function generateMetadata(): Promise<Metadata> {
13
+ try {
14
+ const storeInfo = await (await getServerClient()).getStoreInfo();
15
+ const description = buildMetaDescription(storeInfo.metaDescription) || undefined;
16
+ return {
17
+ title: storeInfo.name,
18
+ description,
19
+ openGraph: {
20
+ title: storeInfo.name,
21
+ description,
22
+ type: 'website',
23
+ },
24
+ twitter: {
25
+ card: 'summary_large_image',
26
+ title: storeInfo.name,
27
+ description,
28
+ },
29
+ };
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+
35
+ export default async function HomePage() {
36
+ // Fetch store info server-side so the JSON-LD is in the initial HTML —
37
+ // crawlers and AI Overviews see it without needing to execute JavaScript.
38
+ // Failure here is non-fatal: the client component re-fetches via the
39
+ // StoreProvider so the page still renders.
40
+ //
41
+ // Also fetch the merchant's homepage intro (RICH_TEXT key='main'). When
42
+ // present, it renders as a sanitized HTML block above the product grid —
43
+ // typical use is brand introduction, value-prop copy, or a seasonal
44
+ // announcement that needs more formatting than the AnnouncementBar.
45
+ // When absent, RichTextBlock returns null cleanly so the layout collapses
46
+ // to just the product grid.
47
+ const client = await getServerClient();
48
+ const [storeInfo, intro] = await Promise.all([
49
+ client.getStoreInfo().catch(() => null),
50
+ client.content.richText.get('main').catch(() => null),
51
+ ]);
52
+
53
+ const baseUrl = await getCanonicalSiteUrl();
54
+
55
+ return (
56
+ <>
57
+ {storeInfo && baseUrl ? <OrganizationJsonLd storeInfo={storeInfo} baseUrl={baseUrl} /> : null}
58
+ <RichTextBlock
59
+ block={intro}
60
+ className="prose prose-sm dark:prose-invert mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8"
61
+ />
62
+ <HomeClient />
63
+ </>
64
+ );
65
+ }
@@ -1,92 +1,92 @@
1
- /**
2
- * Static page route — fully driven by the merchant's dashboard.
3
- *
4
- * The merchant creates pages in the Brainerce dashboard under
5
- * Sell → Content → Page (one row per page, each with its own `slug`)
6
- *
7
- * This catch-all renders any PAGE by its `data.slug`. `/pages/about` →
8
- * fetches PAGE with slug='about'. Mounting under `/pages/` (rather than
9
- * the root) keeps the route from clashing with existing static routes
10
- * like /cart, /login, /products.
11
- *
12
- * Security: page HTML is sanitized via `sanitizeHtml` before injecting.
13
- */
14
- import * as React from 'react';
15
- import type { Metadata } from 'next';
16
- import { notFound } from 'next/navigation';
17
-
18
- import { getServerClient } from '@/core/lib/brainerce';
19
- import { sanitizeHtml } from '@/core/lib/sanitize';
20
- import { decodeSlug } from '@/core/lib/utils';
21
-
22
- <% if (i18nEnabled) { %>
23
- type PageProps = {
24
- params: Promise<{ locale: string; slug: string }>;
25
- };
26
-
27
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
28
- const { locale, slug: rawSlug } = await params;
29
- const slug = decodeSlug(rawSlug);
30
- const page = await getServerClient(locale).content.page.getBySlug(slug, locale).catch(() => null);
31
- if (!page) return { title: slug };
32
- const seo = page.data.seo ?? {};
33
- return {
34
- title: seo.title ?? page.data.title,
35
- description: seo.description,
36
- openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
37
- };
38
- }
39
-
40
- export default async function StaticPage({ params }: PageProps) {
41
- const { locale, slug: rawSlug } = await params;
42
- const slug = decodeSlug(rawSlug);
43
- const page = await getServerClient(locale).content.page.getBySlug(slug, locale).catch(() => null);
44
- if (!page) notFound();
45
- return (
46
- <article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
47
- <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">
48
- {page.data.title}
49
- </h1>
50
- <div
51
- className="prose prose-sm dark:prose-invert max-w-none"
52
- dangerouslySetInnerHTML={{ __html: sanitizeHtml(page.data.html) }}
53
- />
54
- </article>
55
- );
56
- }
57
- <% } else { %>
58
- type PageProps = {
59
- params: Promise<{ slug: string }>;
60
- };
61
-
62
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
63
- const { slug: rawSlug } = await params;
64
- const slug = decodeSlug(rawSlug);
65
- const page = await getServerClient().content.page.getBySlug(slug).catch(() => null);
66
- if (!page) return { title: slug };
67
- const seo = page.data.seo ?? {};
68
- return {
69
- title: seo.title ?? page.data.title,
70
- description: seo.description,
71
- openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
72
- };
73
- }
74
-
75
- export default async function StaticPage({ params }: PageProps) {
76
- const { slug: rawSlug } = await params;
77
- const slug = decodeSlug(rawSlug);
78
- const page = await getServerClient().content.page.getBySlug(slug).catch(() => null);
79
- if (!page) notFound();
80
- return (
81
- <article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
82
- <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">
83
- {page.data.title}
84
- </h1>
85
- <div
86
- className="prose prose-sm dark:prose-invert max-w-none"
87
- dangerouslySetInnerHTML={{ __html: sanitizeHtml(page.data.html) }}
88
- />
89
- </article>
90
- );
91
- }
92
- <% } %>
1
+ /**
2
+ * Static page route — fully driven by the merchant's dashboard.
3
+ *
4
+ * The merchant creates pages in the Brainerce dashboard under
5
+ * Sell → Content → Page (one row per page, each with its own `slug`)
6
+ *
7
+ * This catch-all renders any PAGE by its `data.slug`. `/pages/about` →
8
+ * fetches PAGE with slug='about'. Mounting under `/pages/` (rather than
9
+ * the root) keeps the route from clashing with existing static routes
10
+ * like /cart, /login, /products.
11
+ *
12
+ * Security: page HTML is sanitized via `sanitizeHtml` before injecting.
13
+ */
14
+ import * as React from 'react';
15
+ import type { Metadata } from 'next';
16
+ import { notFound } from 'next/navigation';
17
+
18
+ import { getServerClient } from '@/core/lib/brainerce.server';
19
+ import { sanitizeHtml } from '@/core/lib/sanitize';
20
+ import { decodeSlug } from '@/core/lib/utils';
21
+
22
+ <% if (i18nEnabled) { %>
23
+ type PageProps = {
24
+ params: Promise<{ locale: string; slug: string }>;
25
+ };
26
+
27
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
28
+ const { locale, slug: rawSlug } = await params;
29
+ const slug = decodeSlug(rawSlug);
30
+ const page = await (await getServerClient(locale)).content.page.getBySlug(slug, locale).catch(() => null);
31
+ if (!page) return { title: slug };
32
+ const seo = page.data.seo ?? {};
33
+ return {
34
+ title: seo.title ?? page.data.title,
35
+ description: seo.description,
36
+ openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
37
+ };
38
+ }
39
+
40
+ export default async function StaticPage({ params }: PageProps) {
41
+ const { locale, slug: rawSlug } = await params;
42
+ const slug = decodeSlug(rawSlug);
43
+ const page = await (await getServerClient(locale)).content.page.getBySlug(slug, locale).catch(() => null);
44
+ if (!page) notFound();
45
+ return (
46
+ <article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
47
+ <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">
48
+ {page.data.title}
49
+ </h1>
50
+ <div
51
+ className="prose prose-sm dark:prose-invert max-w-none"
52
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(page.data.html) }}
53
+ />
54
+ </article>
55
+ );
56
+ }
57
+ <% } else { %>
58
+ type PageProps = {
59
+ params: Promise<{ slug: string }>;
60
+ };
61
+
62
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
63
+ const { slug: rawSlug } = await params;
64
+ const slug = decodeSlug(rawSlug);
65
+ const page = await (await getServerClient()).content.page.getBySlug(slug).catch(() => null);
66
+ if (!page) return { title: slug };
67
+ const seo = page.data.seo ?? {};
68
+ return {
69
+ title: seo.title ?? page.data.title,
70
+ description: seo.description,
71
+ openGraph: seo.ogImage ? { images: [{ url: seo.ogImage }] } : undefined,
72
+ };
73
+ }
74
+
75
+ export default async function StaticPage({ params }: PageProps) {
76
+ const { slug: rawSlug } = await params;
77
+ const slug = decodeSlug(rawSlug);
78
+ const page = await (await getServerClient()).content.page.getBySlug(slug).catch(() => null);
79
+ if (!page) notFound();
80
+ return (
81
+ <article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
82
+ <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">
83
+ {page.data.title}
84
+ </h1>
85
+ <div
86
+ className="prose prose-sm dark:prose-invert max-w-none"
87
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(page.data.html) }}
88
+ />
89
+ </article>
90
+ );
91
+ }
92
+ <% } %>
@@ -1,13 +1,15 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { notFound, permanentRedirect } from 'next/navigation';
3
3
  import { getProductPriceInfo } from 'brainerce';
4
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
4
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
5
5
  import { resolveCurrency } from '@/core/lib/resolve-currency';
6
6
  import { buildMetaDescription } from '@/core/lib/seo';
7
7
  import { decodeSlug } from '@/core/lib/utils';
8
8
  import { ProductJsonLd } from '@/components/seo/product-json-ld';
9
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
9
10
  import { Breadcrumbs } from '@/components/seo/breadcrumbs';
10
11
  import { ReviewsSection } from '@/ui/product/reviews-section';
12
+ import { ProductFaqSection } from '@/ui/product/faq-section';
11
13
  import { ProductClientSection } from '@/ui/product/product-client-section';
12
14
 
13
15
  type Props = {
@@ -37,7 +39,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
37
39
  const slug = decodeSlug(rawSlug);
38
40
 
39
41
  try {
40
- const client = getServerClient(locale);
42
+ const client = await getServerClient(locale);
41
43
  // Fetch product + store info in parallel; storeInfo gives us the live
42
44
  // currency for OG price tags (with env-var + USD fallbacks so we never
43
45
  // silently emit a wrong currency when the API is briefly unreachable).
@@ -69,7 +71,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
69
71
  // Locales come from storeInfo.i18n — already fetched above, zero extra cost.
70
72
  const supportedLocales = storeInfo?.i18n?.supportedLocales ?? [];
71
73
  const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? '';
72
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
74
+ const baseUrl = await getCanonicalSiteUrl();
73
75
  const baseSlug = product.slug || slug;
74
76
  const localeSlugs = product.localeSlugs ?? {};
75
77
 
@@ -132,7 +134,7 @@ export default async function ProductDetailPage({ params }: Props) {
132
134
  const { slug: rawSlug, locale } = await params;
133
135
  const slug = decodeSlug(rawSlug);
134
136
 
135
- const client = getServerClient(locale);
137
+ const client = await getServerClient(locale);
136
138
  let product;
137
139
  try {
138
140
  product = await client.getProductBySlug(slug);
@@ -152,7 +154,7 @@ export default async function ProductDetailPage({ params }: Props) {
152
154
  notFound();
153
155
  }
154
156
 
155
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
157
+ const baseUrl = await getCanonicalSiteUrl();
156
158
  const productUrl = `${baseUrl}/products/${slug}`;
157
159
  // Reuse the cached storeInfo from generateMetadata's call — React cache()
158
160
  // collapses both into one backend request per render. resolveCurrency owns
@@ -181,6 +183,9 @@ export default async function ProductDetailPage({ params }: Props) {
181
183
  <Breadcrumbs items={breadcrumbItems} />
182
184
  </div>
183
185
  <ProductClientSection product={product} />
186
+ {/* Visible Q&A + the FAQPage JSON-LD above read the same product.faq —
187
+ the extractable format AI answer engines cite. */}
188
+ <ProductFaqSection product={product} locale={locale} />
184
189
  <ReviewsSection productId={product.id} />
185
190
  </>
186
191
  );
@@ -1,4 +1,5 @@
1
1
  import type { MetadataRoute } from 'next';
2
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
2
3
 
3
4
  /**
4
5
  * robots.txt with an explicit AI-crawler policy.
@@ -47,8 +48,8 @@ const AI_TRAINING_BOTS = [
47
48
  'Applebot-Extended',
48
49
  ];
49
50
 
50
- export default function robots(): MetadataRoute.Robots {
51
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
51
+ export default async function robots(): Promise<MetadataRoute.Robots> {
52
+ const baseUrl = await getCanonicalSiteUrl();
52
53
 
53
54
  return {
54
55
  rules: [
@@ -4,7 +4,8 @@ import {
4
4
  getCategorySitemapEntries,
5
5
  getProductSitemapEntries,
6
6
  } from 'brainerce';
7
- import { getServerClient } from '@/core/lib/brainerce';
7
+ import { getServerClient } from '@/core/lib/brainerce.server';
8
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
8
9
 
9
10
  /**
10
11
  * Storefront sitemap: static pages + every published product, category,
@@ -16,9 +17,9 @@ import { getServerClient } from '@/core/lib/brainerce';
16
17
  * The helper uses a dedicated lightweight endpoint with a 5000-product cap.
17
18
  */
18
19
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
19
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
20
+ const baseUrl = await getCanonicalSiteUrl();
20
21
 
21
- const client = getServerClient();
22
+ const client = await getServerClient();
22
23
  const storeInfo = await client.getStoreInfo().catch(() => null);
23
24
  const supportedLocales = storeInfo?.i18n?.supportedLocales ?? [];
24
25
  const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? '';