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,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,11 +1,12 @@
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';
11
12
  import { ProductFaqSection } from '@/ui/product/faq-section';
@@ -38,7 +39,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
38
39
  const slug = decodeSlug(rawSlug);
39
40
 
40
41
  try {
41
- const client = getServerClient(locale);
42
+ const client = await getServerClient(locale);
42
43
  // Fetch product + store info in parallel; storeInfo gives us the live
43
44
  // currency for OG price tags (with env-var + USD fallbacks so we never
44
45
  // silently emit a wrong currency when the API is briefly unreachable).
@@ -70,7 +71,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
70
71
  // Locales come from storeInfo.i18n — already fetched above, zero extra cost.
71
72
  const supportedLocales = storeInfo?.i18n?.supportedLocales ?? [];
72
73
  const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? '';
73
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
74
+ const baseUrl = await getCanonicalSiteUrl();
74
75
  const baseSlug = product.slug || slug;
75
76
  const localeSlugs = product.localeSlugs ?? {};
76
77
 
@@ -133,7 +134,7 @@ export default async function ProductDetailPage({ params }: Props) {
133
134
  const { slug: rawSlug, locale } = await params;
134
135
  const slug = decodeSlug(rawSlug);
135
136
 
136
- const client = getServerClient(locale);
137
+ const client = await getServerClient(locale);
137
138
  let product;
138
139
  try {
139
140
  product = await client.getProductBySlug(slug);
@@ -153,7 +154,7 @@ export default async function ProductDetailPage({ params }: Props) {
153
154
  notFound();
154
155
  }
155
156
 
156
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
157
+ const baseUrl = await getCanonicalSiteUrl();
157
158
  const productUrl = `${baseUrl}/products/${slug}`;
158
159
  // Reuse the cached storeInfo from generateMetadata's call — React cache()
159
160
  // collapses both into one backend request per render. resolveCurrency owns
@@ -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 ?? '';