create-brainerce-store 1.62.0 → 1.64.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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,7 +1,7 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { notFound } from 'next/navigation';
3
3
  import type { Product } from 'brainerce';
4
- import { getServerClient } from '@/core/lib/brainerce';
4
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
5
5
  import { buildMetaDescription } from '@/core/lib/seo';
6
6
  import { sanitizeHtml } from '@/core/lib/sanitize';
7
7
  import { decodeSlug } from '@/core/lib/utils';
@@ -26,17 +26,43 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
26
26
  const { slug: rawSlug, locale } = await params;
27
27
  const slug = decodeSlug(rawSlug);
28
28
  try {
29
- const category = await getServerClient(locale).getCategoryBySlug(slug);
29
+ const [category, storeInfo] = await Promise.all([
30
+ getServerClient(locale).getCategoryBySlug(slug),
31
+ fetchStoreInfo(locale),
32
+ ]);
30
33
  const description =
31
34
  category.metaDescription || buildMetaDescription(category.description) || category.name;
32
35
  const canonicalSlug = category.slug || slug;
33
36
  const canonicalPath = locale
34
37
  ? `/${locale}/category/${canonicalSlug}`
35
38
  : `/category/${canonicalSlug}`;
39
+
40
+ // hreflang: category URLs use the same slug across locales (only the
41
+ // locale prefix changes) — the same convention getCategorySitemapEntries
42
+ // emits, so the alternates always match real, crawlable URLs.
43
+ const supportedLocales = storeInfo?.i18n?.supportedLocales ?? [];
44
+ const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? '';
45
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
46
+ const hreflangLanguages =
47
+ supportedLocales.length > 1
48
+ ? Object.fromEntries([
49
+ ...supportedLocales.map((loc) => [
50
+ loc,
51
+ loc === defaultLoc
52
+ ? `${baseUrl}/category/${canonicalSlug}`
53
+ : `${baseUrl}/${loc}/category/${canonicalSlug}`,
54
+ ]),
55
+ ['x-default', `${baseUrl}/category/${canonicalSlug}`],
56
+ ])
57
+ : undefined;
58
+
36
59
  return {
37
60
  title: category.name,
38
61
  description,
39
- alternates: { canonical: canonicalPath },
62
+ alternates: {
63
+ canonical: canonicalPath,
64
+ ...(hreflangLanguages ? { languages: hreflangLanguages } : {}),
65
+ },
40
66
  openGraph: {
41
67
  title: category.name,
42
68
  description,
@@ -31,6 +31,7 @@ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
31
31
  import { useTranslations } from '@/core/lib/translations';
32
32
  import { cn } from '@/core/lib/utils';
33
33
  import { isValidCheckoutId } from '@/core/lib/safe-redirect';
34
+ import { trackBeginCheckout } from '@/core/lib/tracking';
34
35
 
35
36
  type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
36
37
 
@@ -69,6 +70,17 @@ function CheckoutContent() {
69
70
  const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
70
71
  const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
71
72
 
73
+ // `begin_checkout` — fired once the cart is loaded, guarded by a ref so a
74
+ // re-render (or the shopper returning from a canceled payment) doesn't
75
+ // report a second checkout start for the same cart.
76
+ const beganCheckoutForCartRef = useRef<string | null>(null);
77
+ useEffect(() => {
78
+ if (!cart?.id || !cart.items?.length) return;
79
+ if (beganCheckoutForCartRef.current === cart.id) return;
80
+ beganCheckoutForCartRef.current = cart.id;
81
+ trackBeginCheckout(cart, currency);
82
+ }, [cart, currency]);
83
+
72
84
  // Check for returning from canceled payment
73
85
  const canceled = searchParams.get('canceled') === 'true';
74
86
  const checkoutIdParam = searchParams.get('checkout_id');
@@ -0,0 +1,14 @@
1
+ import type { Metadata } from 'next';
2
+
3
+ // The contact page itself is a client component (the form is dashboard-
4
+ // driven), so its metadata lives here. Indexable on purpose — a crawlable
5
+ // contact page is a store-trust signal for both Google and AI assistants.
6
+ export const metadata: Metadata = {
7
+ title: <%- language === 'he' ? "'צור קשר'" : "'Contact'" %>,
8
+ description: <%- language === 'he' ? "'יצירת קשר עם החנות — שאלות, הזמנות ושירות לקוחות.'" : "'Get in touch with the store — questions, orders, and support.'" %>,
9
+ alternates: { canonical: '/contact' },
10
+ };
11
+
12
+ export default function ContactLayout({ children }: { children: React.ReactNode }) {
13
+ return children;
14
+ }
@@ -1,36 +1,36 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- @layer base {
6
- :root {
7
- --background: 0 0% 100%;
8
- --foreground: 0 0% 9%;
9
- --primary: 0 0% 9%;
10
- --primary-foreground: 0 0% 98%;
11
- --secondary: 0 0% 96%;
12
- --secondary-foreground: 0 0% 9%;
13
- --muted: 0 0% 96%;
14
- --muted-foreground: 0 0% 45%;
15
- --accent: 0 0% 96%;
16
- --accent-foreground: 0 0% 9%;
17
- --destructive: 0 84% 60%;
18
- --destructive-foreground: 0 0% 98%;
19
- --border: 0 0% 90%;
20
- --input: 0 0% 90%;
21
- --ring: 0 0% 9%;
22
- --card: 0 0% 100%;
23
- --card-foreground: 0 0% 9%;
24
- --popover: 0 0% 100%;
25
- --popover-foreground: 0 0% 9%;
26
- --radius: 0.5rem;
27
- }
28
-
29
- * {
30
- @apply border-border;
31
- }
32
-
33
- body {
34
- @apply bg-background text-foreground antialiased;
35
- }
36
- }
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 0 0% 9%;
9
+ --primary: 0 0% 9%;
10
+ --primary-foreground: 0 0% 98%;
11
+ --secondary: 0 0% 96%;
12
+ --secondary-foreground: 0 0% 9%;
13
+ --muted: 0 0% 96%;
14
+ --muted-foreground: 0 0% 45%;
15
+ --accent: 0 0% 96%;
16
+ --accent-foreground: 0 0% 9%;
17
+ --destructive: 0 84% 60%;
18
+ --destructive-foreground: 0 0% 98%;
19
+ --border: 0 0% 90%;
20
+ --input: 0 0% 90%;
21
+ --ring: 0 0% 9%;
22
+ --card: 0 0% 100%;
23
+ --card-foreground: 0 0% 9%;
24
+ --popover: 0 0% 100%;
25
+ --popover-foreground: 0 0% 9%;
26
+ --radius: 0.5rem;
27
+ }
28
+
29
+ * {
30
+ @apply border-border;
31
+ }
32
+
33
+ body {
34
+ @apply bg-background text-foreground antialiased;
35
+ }
36
+ }
@@ -1,26 +1,26 @@
1
- /**
2
- * IndexNow key file — GET /indexnow-key.txt
3
- *
4
- * Brainerce's SEO Autopilot pings IndexNow (instant search-engine indexing)
5
- * whenever a blog post publishes. The ping includes
6
- * `keyLocation: https://<your-domain>/indexnow-key.txt`, and search engines
7
- * verify ownership by fetching this file and matching the key.
8
- *
9
- * The key comes from `getStoreInfo().seo.indexNowKey` — it is NOT a secret
10
- * (the file is public by protocol design). Returns 404 until the platform
11
- * generates a key for this sales channel, which is harmless.
12
- */
13
- import { getServerClient } from '@/core/lib/brainerce';
14
-
15
- export const revalidate = 3600;
16
-
17
- export async function GET() {
18
- const info = await getServerClient()
19
- .getStoreInfo()
20
- .catch(() => null);
21
- const key = info?.seo?.indexNowKey;
22
- if (!key) return new Response(null, { status: 404 });
23
- return new Response(key, {
24
- headers: { 'Content-Type': 'text/plain; charset=utf-8' },
25
- });
26
- }
1
+ /**
2
+ * IndexNow key file — GET /indexnow-key.txt
3
+ *
4
+ * Brainerce's SEO Autopilot pings IndexNow (instant search-engine indexing)
5
+ * whenever a blog post publishes. The ping includes
6
+ * `keyLocation: https://<your-domain>/indexnow-key.txt`, and search engines
7
+ * verify ownership by fetching this file and matching the key.
8
+ *
9
+ * The key comes from `getStoreInfo().seo.indexNowKey` — it is NOT a secret
10
+ * (the file is public by protocol design). Returns 404 until the platform
11
+ * generates a key for this sales channel, which is harmless.
12
+ */
13
+ import { getServerClient } from '@/core/lib/brainerce';
14
+
15
+ export const revalidate = 3600;
16
+
17
+ export async function GET() {
18
+ const info = await getServerClient()
19
+ .getStoreInfo()
20
+ .catch(() => null);
21
+ const key = info?.seo?.indexNowKey;
22
+ if (!key) return new Response(null, { status: 404 });
23
+ return new Response(key, {
24
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
25
+ });
26
+ }
@@ -6,6 +6,7 @@ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
6
6
  import { SiteHeader } from '@/ui/layout/site-header';
7
7
  import { SiteFooter } from '@/ui/layout/site-footer';
8
8
  import { BrainerceBotWidget } from '@/components/brainerce-bot';
9
+ import { TrackingBootstrap } from '@/components/tracking-bootstrap';
9
10
  import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
11
  import { getDirection, getMessages, supportedLocales } from '@/i18n';
11
12
  import { getNonce } from '@/core/lib/nonce';
@@ -90,6 +91,16 @@ export default async function RootLayout({
90
91
  .replace(/&/g, '\\u0026'),
91
92
  }}
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" />
93
104
  {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
94
105
  SPA route changes. Cookieless (no consent banner needed); the
95
106
  merchant can toggle it per sales channel in the dashboard. */}
@@ -105,6 +116,10 @@ export default async function RootLayout({
105
116
  </head>
106
117
  <body className={font.className}>
107
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 />
108
123
  <div className="min-h-screen flex flex-col">
109
124
  <AnnouncementBar announcements={announcements} />
110
125
  <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
@@ -125,6 +140,7 @@ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
125
140
  import { SiteHeader } from '@/ui/layout/site-header';
126
141
  import { SiteFooter } from '@/ui/layout/site-footer';
127
142
  import { BrainerceBotWidget } from '@/components/brainerce-bot';
143
+ import { TrackingBootstrap } from '@/components/tracking-bootstrap';
128
144
  import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
129
145
  import { getNonce } from '@/core/lib/nonce';
130
146
  import './globals.css';
@@ -197,6 +213,16 @@ export default async function RootLayout({
197
213
  .replace(/&/g, '\\u0026'),
198
214
  }}
199
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" />
200
226
  {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
201
227
  SPA route changes. Cookieless (no consent banner needed); the
202
228
  merchant can toggle it per sales channel in the dashboard. */}
@@ -212,6 +238,10 @@ export default async function RootLayout({
212
238
  </head>
213
239
  <body className={font.className}>
214
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 />
215
245
  <div className="min-h-screen flex flex-col">
216
246
  <AnnouncementBar announcements={announcements} />
217
247
  <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
@@ -2,23 +2,35 @@
2
2
  * llms.txt — GET /llms.txt
3
3
  *
4
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.
5
+ * Claude, AI Overviews): a concise markdown map of what the site is and where
6
+ * its key content lives, so AI crawlers ground answers in (and cite) the
7
+ * right pages. /agents.md is the richer agent-facing companion (the newer
8
+ * convention) — keep the two consistent.
8
9
  */
9
10
  import { getServerClient } from '@/core/lib/brainerce';
10
11
 
11
12
  export const revalidate = 3600;
12
13
 
14
+ interface CategoryNodeLike {
15
+ name: string;
16
+ slug?: string | null;
17
+ children?: CategoryNodeLike[];
18
+ }
19
+
13
20
  export async function GET() {
14
21
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
15
22
  const client = getServerClient();
16
23
 
17
- const [info, posts] = await Promise.all([
24
+ const [info, posts, categoriesRes] = await Promise.all([
18
25
  client.getStoreInfo().catch(() => null),
19
26
  client.blog.getPosts({ limit: 20 }).catch(() => null),
27
+ client.getCategories().catch(() => null),
20
28
  ]);
21
29
 
30
+ const topCategories: CategoryNodeLike[] = (
31
+ (categoriesRes?.categories as CategoryNodeLike[] | undefined) ?? []
32
+ ).filter((c) => c.slug);
33
+
22
34
  const lines: string[] = [
23
35
  `# ${info?.name ?? 'Store'}`,
24
36
  '',
@@ -26,8 +38,19 @@ export async function GET() {
26
38
  '## Key pages',
27
39
  '',
28
40
  `- [Products](${baseUrl}/products): full catalog`,
41
+ ...topCategories
42
+ .slice(0, 20)
43
+ .map((c) => `- [${c.name}](${baseUrl}/category/${c.slug}): category`),
29
44
  `- [Blog](${baseUrl}/blog): guides and articles`,
30
- ...(info?.contactEmail ? [`- Contact: ${info.contactEmail}`] : []),
45
+ `- [FAQ](${baseUrl}/faq)`,
46
+ `- [Contact](${baseUrl}/contact)`,
47
+ ...(info?.contactEmail ? [`- Contact email: ${info.contactEmail}`] : []),
48
+ '',
49
+ '## Machine-readable surfaces',
50
+ '',
51
+ `- [Agent guide](${baseUrl}/agents.md)`,
52
+ `- [Sitemap](${baseUrl}/sitemap.xml)`,
53
+ `- [Blog RSS](${baseUrl}/blog/rss.xml)`,
31
54
  ];
32
55
 
33
56
  if (posts && posts.data.length > 0) {
@@ -0,0 +1,35 @@
1
+ import Link from 'next/link';
2
+
3
+ /**
4
+ * 404 page — rendered for unknown URLs and for `notFound()` calls from
5
+ * product/category/blog pages (deleted products, renamed slugs). A branded
6
+ * 404 with a path back to the catalog keeps the visitor (and their SEO
7
+ * equity) on the site instead of bouncing off the framework default.
8
+ */
9
+ export default function NotFound() {
10
+ return (
11
+ <div className="mx-auto flex max-w-2xl flex-col items-center justify-center px-4 py-24 text-center">
12
+ <p className="text-sm font-medium text-muted-foreground">404</p>
13
+ <h1 className="mt-2 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
14
+ <%- language === 'he' ? 'העמוד לא נמצא' : 'Page not found' %>
15
+ </h1>
16
+ <p className="mt-4 text-muted-foreground">
17
+ <%- language === 'he' ? 'יכול להיות שהמוצר הוסר או שהכתובת השתנתה.' : 'The product may have been removed, or the address may have changed.' %>
18
+ </p>
19
+ <div className="mt-8 flex gap-4">
20
+ <Link
21
+ href="/"
22
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
23
+ >
24
+ <%- language === 'he' ? 'לדף הבית' : 'Go home' %>
25
+ </Link>
26
+ <Link
27
+ href="/products"
28
+ className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
29
+ >
30
+ <%- language === 'he' ? 'לכל המוצרים' : 'Browse products' %>
31
+ </Link>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,109 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import { fetchStoreInfo } from '@/core/lib/brainerce';
3
+
4
+ /**
5
+ * Default Open Graph card for every page that doesn't set its own og:image
6
+ * (home, /products, /faq, /contact, content pages without an OG image).
7
+ * Product / category / blog pages override this with their real images.
8
+ *
9
+ * Without this file, any page lacking an image gets NO social card at all —
10
+ * a bare-domain link in WhatsApp/Facebook/X renders as plain text.
11
+ */
12
+ export const size = { width: 1200, height: 630 };
13
+ export const contentType = 'image/png';
14
+ export const alt = 'Store';
15
+
16
+ /**
17
+ * Satori (the OG renderer) ships a Latin-only default font — a Hebrew or
18
+ * Arabic store name would render as tofu (□□□). Fetch a Noto Sans variant
19
+ * with the needed coverage at request time; fetching the CSS without a
20
+ * browser UA makes Google Fonts return TTF (which Satori accepts — it can't
21
+ * parse woff2). Cached by the framework; on any failure we render without
22
+ * text rather than shipping tofu.
23
+ */
24
+ async function loadFont(text: string): Promise<ArrayBuffer | null> {
25
+ try {
26
+ const css = await fetch(
27
+ `https://fonts.googleapis.com/css2?family=Noto+Sans+Hebrew:wght@700&family=Noto+Sans:wght@700&display=swap&text=${encodeURIComponent(text)}`,
28
+ { cache: 'force-cache' }
29
+ ).then((r) => r.text());
30
+ const url = css.match(/src:\s*url\((.+?)\)\s*format\(['"](?:truetype|opentype)['"]\)/)?.[1];
31
+ if (!url) return null;
32
+ return await fetch(url, { cache: 'force-cache' }).then((r) => r.arrayBuffer());
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ // Basic Latin through Latin Extended-B (U+0020–U+024F) plus whitespace,
39
+ // punctuation, and digits — the coverage of Satori's bundled default font.
40
+ const LATIN_ONLY = /^[ -ɏ\s\p{P}\p{N}]*$/u;
41
+
42
+ export default async function OpenGraphImage() {
43
+ const info = await fetchStoreInfo();
44
+ const name = info?.name?.trim() || '';
45
+ const description = info?.metaDescription?.trim() || '';
46
+ const logo = info?.logo || null;
47
+
48
+ const text = `${name} ${description}`.trim();
49
+ const font = text ? await loadFont(text) : null;
50
+ // Default font covers Latin only — if the font fetch failed and the text
51
+ // needs more coverage, drop the text instead of rendering tofu.
52
+ const canRenderText = Boolean(text) && (font !== null || LATIN_ONLY.test(text));
53
+
54
+ return new ImageResponse(
55
+ <div
56
+ style={{
57
+ width: '100%',
58
+ height: '100%',
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ gap: 32,
64
+ background: 'linear-gradient(135deg, #111113 0%, #1c1c22 55%, #26262e 100%)',
65
+ color: '#fafafa',
66
+ }}
67
+ >
68
+ {logo ? (
69
+ <img src={logo} alt="" style={{ maxHeight: 220, maxWidth: 520, objectFit: 'contain' }} />
70
+ ) : null}
71
+ {canRenderText && name ? (
72
+ <div
73
+ style={{
74
+ display: 'flex',
75
+ fontSize: logo ? 56 : 84,
76
+ fontWeight: 700,
77
+ textAlign: 'center',
78
+ maxWidth: 1000,
79
+ }}
80
+ >
81
+ {name}
82
+ </div>
83
+ ) : null}
84
+ {canRenderText && description ? (
85
+ <div
86
+ style={{
87
+ display: 'flex',
88
+ fontSize: 32,
89
+ color: '#a1a1aa',
90
+ textAlign: 'center',
91
+ maxWidth: 960,
92
+ }}
93
+ >
94
+ {description.length > 120 ? `${description.slice(0, 119)}…` : description}
95
+ </div>
96
+ ) : null}
97
+ </div>,
98
+ {
99
+ ...size,
100
+ ...(font
101
+ ? {
102
+ fonts: [
103
+ { name: 'Noto Sans', data: font, weight: 700 as const, style: 'normal' as const },
104
+ ],
105
+ }
106
+ : {}),
107
+ }
108
+ );
109
+ }