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,74 +1,74 @@
1
- 'use client';
2
-
3
- import type { TaxBreakdown } from 'brainerce';
4
- import { formatPrice } from 'brainerce';
5
- import { useTranslations } from '@/core/lib/translations';
6
- import { useCurrency } from '@/core/lib/use-currency';
7
- import { cn } from '@/core/lib/utils';
8
-
9
- interface TaxDisplayProps {
10
- /** Whether shipping address has been set */
11
- addressSet: boolean;
12
- /** Tax amount string from checkout (only available after address is set) */
13
- taxAmount?: string;
14
- /** Detailed tax breakdown (optional) */
15
- taxBreakdown?: TaxBreakdown | null;
16
- className?: string;
17
- }
18
-
19
- export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: TaxDisplayProps) {
20
- const t = useTranslations('checkout');
21
- const tc = useTranslations('common');
22
- const currency = useCurrency();
23
-
24
- // Before address is set
25
- if (!addressSet) {
26
- return (
27
- <div className={cn('flex items-center justify-between text-sm', className)}>
28
- <span className="text-muted-foreground">{tc('tax')}</span>
29
- <span className="text-muted-foreground text-xs">{t('calculatedAfterAddress')}</span>
30
- </div>
31
- );
32
- }
33
-
34
- // Inclusive-pricing stores back the tax out of the displayed price, so
35
- // `checkout.taxAmount` is stored as 0 and the real VAT lives on the
36
- // aggregated breakdown. Surface whichever is present.
37
- const explicitTax = taxAmount ? parseFloat(taxAmount) : 0;
38
- const tax =
39
- explicitTax > 0
40
- ? explicitTax
41
- : typeof taxBreakdown?.totalTax === 'number'
42
- ? taxBreakdown.totalTax
43
- : 0;
44
-
45
- // When there's a per-rate breakdown, prefer per-row display ("VAT 18% ₪3.05").
46
- // Otherwise show a single aggregated tax line.
47
- const hasBreakdown = !!taxBreakdown?.breakdown && taxBreakdown.breakdown.length > 0;
48
-
49
- if (hasBreakdown) {
50
- return (
51
- <div className={cn('space-y-1', className)}>
52
- {taxBreakdown!.breakdown.map((item, index) => (
53
- <div key={index} className="flex items-center justify-between text-sm">
54
- <span className="text-muted-foreground">
55
- {item.name} ({(item.rate * 100).toFixed(1)}%)
56
- </span>
57
- <span className="text-foreground font-medium">
58
- {formatPrice(item.amount, { currency }) as string}
59
- </span>
60
- </div>
61
- ))}
62
- </div>
63
- );
64
- }
65
-
66
- return (
67
- <div className={cn('flex items-center justify-between text-sm', className)}>
68
- <span className="text-muted-foreground">{tc('tax')}</span>
69
- <span className="text-foreground font-medium">
70
- {tax > 0 ? (formatPrice(tax, { currency }) as string) : t('noTax')}
71
- </span>
72
- </div>
73
- );
74
- }
1
+ 'use client';
2
+
3
+ import type { TaxBreakdown } from 'brainerce';
4
+ import { formatPrice } from 'brainerce';
5
+ import { useTranslations } from '@/core/lib/translations';
6
+ import { useCurrency } from '@/core/lib/use-currency';
7
+ import { cn } from '@/core/lib/utils';
8
+
9
+ interface TaxDisplayProps {
10
+ /** Whether shipping address has been set */
11
+ addressSet: boolean;
12
+ /** Tax amount string from checkout (only available after address is set) */
13
+ taxAmount?: string;
14
+ /** Detailed tax breakdown (optional) */
15
+ taxBreakdown?: TaxBreakdown | null;
16
+ className?: string;
17
+ }
18
+
19
+ export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: TaxDisplayProps) {
20
+ const t = useTranslations('checkout');
21
+ const tc = useTranslations('common');
22
+ const currency = useCurrency();
23
+
24
+ // Before address is set
25
+ if (!addressSet) {
26
+ return (
27
+ <div className={cn('flex items-center justify-between text-sm', className)}>
28
+ <span className="text-muted-foreground">{tc('tax')}</span>
29
+ <span className="text-muted-foreground text-xs">{t('calculatedAfterAddress')}</span>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ // Inclusive-pricing stores back the tax out of the displayed price, so
35
+ // `checkout.taxAmount` is stored as 0 and the real VAT lives on the
36
+ // aggregated breakdown. Surface whichever is present.
37
+ const explicitTax = taxAmount ? parseFloat(taxAmount) : 0;
38
+ const tax =
39
+ explicitTax > 0
40
+ ? explicitTax
41
+ : typeof taxBreakdown?.totalTax === 'number'
42
+ ? taxBreakdown.totalTax
43
+ : 0;
44
+
45
+ // When there's a per-rate breakdown, prefer per-row display ("VAT 18% ₪3.05").
46
+ // Otherwise show a single aggregated tax line.
47
+ const hasBreakdown = !!taxBreakdown?.breakdown && taxBreakdown.breakdown.length > 0;
48
+
49
+ if (hasBreakdown) {
50
+ return (
51
+ <div className={cn('space-y-1', className)}>
52
+ {taxBreakdown!.breakdown.map((item, index) => (
53
+ <div key={index} className="flex items-center justify-between text-sm">
54
+ <span className="text-muted-foreground">
55
+ {item.name} ({(item.rate * 100).toFixed(1)}%)
56
+ </span>
57
+ <span className="text-foreground font-medium">
58
+ {formatPrice(item.amount, { currency }) as string}
59
+ </span>
60
+ </div>
61
+ ))}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div className={cn('flex items-center justify-between text-sm', className)}>
68
+ <span className="text-muted-foreground">{tc('tax')}</span>
69
+ <span className="text-foreground font-medium">
70
+ {tax > 0 ? (formatPrice(tax, { currency }) as string) : t('noTax')}
71
+ </span>
72
+ </div>
73
+ );
74
+ }
@@ -1,59 +1,59 @@
1
- import type { BlogPost } from 'brainerce';
2
- import { buildArticleJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
- import { getNonce } from '@/core/lib/nonce';
4
-
5
- interface ArticleJsonLdProps {
6
- post: BlogPost;
7
- url: string;
8
- /** Store display name — feeds Article.publisher (and the author fallback). */
9
- organizationName?: string;
10
- }
11
-
12
- /**
13
- * Article + BreadcrumbList structured data for a blog post page.
14
- *
15
- * Uses the SDK builders for the schema shape, but renders the <script> here so
16
- * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
- */
18
- export async function ArticleJsonLd({ post, url, organizationName }: ArticleJsonLdProps) {
19
- const nonce = await getNonce();
20
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
-
22
- const articleJsonLd = buildArticleJsonLd(post, {
23
- siteUrl: baseUrl,
24
- path: url,
25
- organizationName,
26
- });
27
-
28
- const breadcrumbJsonLd = buildBreadcrumbJsonLd([
29
- { name: 'Home', url: baseUrl || '/' },
30
- { name: 'Blog', url: `${baseUrl}/blog` },
31
- { name: post.title, url },
32
- ]);
33
-
34
- return (
35
- <>
36
- <script
37
- type="application/ld+json"
38
- nonce={nonce}
39
- suppressHydrationWarning
40
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(articleJsonLd) }}
41
- />
42
- <script
43
- type="application/ld+json"
44
- nonce={nonce}
45
- suppressHydrationWarning
46
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
47
- />
48
- </>
49
- );
50
- }
51
-
52
- // Escape `<`, `>`, `&` so AI-generated article copy can't break out of the
53
- // <script> element or inject HTML.
54
- function serializeJsonLd(value: unknown): string {
55
- return JSON.stringify(value)
56
- .replace(/</g, '\\u003c')
57
- .replace(/>/g, '\\u003e')
58
- .replace(/&/g, '\\u0026');
59
- }
1
+ import type { BlogPost } from 'brainerce';
2
+ import { buildArticleJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
+ import { getNonce } from '@/core/lib/nonce';
4
+
5
+ interface ArticleJsonLdProps {
6
+ post: BlogPost;
7
+ url: string;
8
+ /** Store display name — feeds Article.publisher (and the author fallback). */
9
+ organizationName?: string;
10
+ }
11
+
12
+ /**
13
+ * Article + BreadcrumbList structured data for a blog post page.
14
+ *
15
+ * Uses the SDK builders for the schema shape, but renders the <script> here so
16
+ * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
+ */
18
+ export async function ArticleJsonLd({ post, url, organizationName }: ArticleJsonLdProps) {
19
+ const nonce = await getNonce();
20
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
+
22
+ const articleJsonLd = buildArticleJsonLd(post, {
23
+ siteUrl: baseUrl,
24
+ path: url,
25
+ organizationName,
26
+ });
27
+
28
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd([
29
+ { name: 'Home', url: baseUrl || '/' },
30
+ { name: 'Blog', url: `${baseUrl}/blog` },
31
+ { name: post.title, url },
32
+ ]);
33
+
34
+ return (
35
+ <>
36
+ <script
37
+ type="application/ld+json"
38
+ nonce={nonce}
39
+ suppressHydrationWarning
40
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(articleJsonLd) }}
41
+ />
42
+ <script
43
+ type="application/ld+json"
44
+ nonce={nonce}
45
+ suppressHydrationWarning
46
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
47
+ />
48
+ </>
49
+ );
50
+ }
51
+
52
+ // Escape `<`, `>`, `&` so AI-generated article copy can't break out of the
53
+ // <script> element or inject HTML.
54
+ function serializeJsonLd(value: unknown): string {
55
+ return JSON.stringify(value)
56
+ .replace(/</g, '\\u003c')
57
+ .replace(/>/g, '\\u003e')
58
+ .replace(/&/g, '\\u0026');
59
+ }
@@ -1,37 +1,37 @@
1
- import Link from 'next/link';
2
-
3
- interface Crumb {
4
- name: string;
5
- /** Site-relative path, e.g. "/category/shoes". Omit on the current (last) crumb. */
6
- href?: string;
7
- }
8
-
9
- /**
10
- * Visible breadcrumb trail. Renders the exact same path structured data
11
- * already declares (see category-json-ld.tsx / product-json-ld.tsx) as real
12
- * `<Link>`s — search engines weight on-page navigation links much more than
13
- * sitemap-only or script-only (JSON-LD) mentions of a URL.
14
- */
15
- export function Breadcrumbs({ items }: { items: Crumb[] }) {
16
- if (items.length < 2) return null;
17
- return (
18
- <nav aria-label="Breadcrumb" className="text-muted-foreground mb-4 text-sm">
19
- <ol className="flex flex-wrap items-center gap-1.5">
20
- {items.map((item, idx) => (
21
- <li key={`${item.name}-${idx}`} className="flex items-center gap-1.5">
22
- {idx > 0 && <span aria-hidden="true">/</span>}
23
- {item.href ? (
24
- <Link href={item.href} className="hover:text-foreground transition-colors">
25
- {item.name}
26
- </Link>
27
- ) : (
28
- <span aria-current="page" className="text-foreground">
29
- {item.name}
30
- </span>
31
- )}
32
- </li>
33
- ))}
34
- </ol>
35
- </nav>
36
- );
37
- }
1
+ import Link from 'next/link';
2
+
3
+ interface Crumb {
4
+ name: string;
5
+ /** Site-relative path, e.g. "/category/shoes". Omit on the current (last) crumb. */
6
+ href?: string;
7
+ }
8
+
9
+ /**
10
+ * Visible breadcrumb trail. Renders the exact same path structured data
11
+ * already declares (see category-json-ld.tsx / product-json-ld.tsx) as real
12
+ * `<Link>`s — search engines weight on-page navigation links much more than
13
+ * sitemap-only or script-only (JSON-LD) mentions of a URL.
14
+ */
15
+ export function Breadcrumbs({ items }: { items: Crumb[] }) {
16
+ if (items.length < 2) return null;
17
+ return (
18
+ <nav aria-label="Breadcrumb" className="text-muted-foreground mb-4 text-sm">
19
+ <ol className="flex flex-wrap items-center gap-1.5">
20
+ {items.map((item, idx) => (
21
+ <li key={`${item.name}-${idx}`} className="flex items-center gap-1.5">
22
+ {idx > 0 && <span aria-hidden="true">/</span>}
23
+ {item.href ? (
24
+ <Link href={item.href} className="hover:text-foreground transition-colors">
25
+ {item.name}
26
+ </Link>
27
+ ) : (
28
+ <span aria-current="page" className="text-foreground">
29
+ {item.name}
30
+ </span>
31
+ )}
32
+ </li>
33
+ ))}
34
+ </ol>
35
+ </nav>
36
+ );
37
+ }
@@ -1,61 +1,61 @@
1
- import type { CategoryDetail } from 'brainerce';
2
- import { buildCollectionPageJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
- import { getNonce } from '@/core/lib/nonce';
4
-
5
- interface CategoryJsonLdProps {
6
- category: CategoryDetail;
7
- url: string;
8
- }
9
-
10
- /**
11
- * CollectionPage + BreadcrumbList structured data for a category (collection)
12
- * landing page. NEVER emit Product markup on a listing page — Google's Product
13
- * rich results are single-product only (see ProductJsonLd for PDPs).
14
- *
15
- * Uses the SDK builders for the schema shape, but renders the <script> here so
16
- * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
- */
18
- export async function CategoryJsonLd({ category, url }: CategoryJsonLdProps) {
19
- const nonce = await getNonce();
20
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
-
22
- const collectionJsonLd = buildCollectionPageJsonLd(category, { siteUrl: baseUrl, path: url });
23
-
24
- // Breadcrumb: Home → ancestor categories → this category.
25
- const crumbs: Array<{ name: string; url: string }> = [
26
- { name: 'Home', url: baseUrl || '/' },
27
- ];
28
- for (const ancestor of category.breadcrumb) {
29
- if (ancestor.slug) {
30
- crumbs.push({ name: ancestor.name, url: `${baseUrl}/category/${ancestor.slug}` });
31
- }
32
- }
33
- crumbs.push({ name: category.name, url });
34
- const breadcrumbJsonLd = buildBreadcrumbJsonLd(crumbs);
35
-
36
- return (
37
- <>
38
- <script
39
- type="application/ld+json"
40
- nonce={nonce}
41
- suppressHydrationWarning
42
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(collectionJsonLd) }}
43
- />
44
- <script
45
- type="application/ld+json"
46
- nonce={nonce}
47
- suppressHydrationWarning
48
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
49
- />
50
- </>
51
- );
52
- }
53
-
54
- // Escape `<`, `>`, `&` so merchant-authored category copy can't break out of
55
- // the <script> element or inject HTML.
56
- function serializeJsonLd(value: unknown): string {
57
- return JSON.stringify(value)
58
- .replace(/</g, '\\u003c')
59
- .replace(/>/g, '\\u003e')
60
- .replace(/&/g, '\\u0026');
61
- }
1
+ import type { CategoryDetail } from 'brainerce';
2
+ import { buildCollectionPageJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
+ import { getNonce } from '@/core/lib/nonce';
4
+
5
+ interface CategoryJsonLdProps {
6
+ category: CategoryDetail;
7
+ url: string;
8
+ }
9
+
10
+ /**
11
+ * CollectionPage + BreadcrumbList structured data for a category (collection)
12
+ * landing page. NEVER emit Product markup on a listing page — Google's Product
13
+ * rich results are single-product only (see ProductJsonLd for PDPs).
14
+ *
15
+ * Uses the SDK builders for the schema shape, but renders the <script> here so
16
+ * we can attach the CSP nonce (the SDK's jsonLdScriptProps can't).
17
+ */
18
+ export async function CategoryJsonLd({ category, url }: CategoryJsonLdProps) {
19
+ const nonce = await getNonce();
20
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
+
22
+ const collectionJsonLd = buildCollectionPageJsonLd(category, { siteUrl: baseUrl, path: url });
23
+
24
+ // Breadcrumb: Home → ancestor categories → this category.
25
+ const crumbs: Array<{ name: string; url: string }> = [
26
+ { name: 'Home', url: baseUrl || '/' },
27
+ ];
28
+ for (const ancestor of category.breadcrumb) {
29
+ if (ancestor.slug) {
30
+ crumbs.push({ name: ancestor.name, url: `${baseUrl}/category/${ancestor.slug}` });
31
+ }
32
+ }
33
+ crumbs.push({ name: category.name, url });
34
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd(crumbs);
35
+
36
+ return (
37
+ <>
38
+ <script
39
+ type="application/ld+json"
40
+ nonce={nonce}
41
+ suppressHydrationWarning
42
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(collectionJsonLd) }}
43
+ />
44
+ <script
45
+ type="application/ld+json"
46
+ nonce={nonce}
47
+ suppressHydrationWarning
48
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
49
+ />
50
+ </>
51
+ );
52
+ }
53
+
54
+ // Escape `<`, `>`, `&` so merchant-authored category copy can't break out of
55
+ // the <script> element or inject HTML.
56
+ function serializeJsonLd(value: unknown): string {
57
+ return JSON.stringify(value)
58
+ .replace(/</g, '\\u003c')
59
+ .replace(/>/g, '\\u003e')
60
+ .replace(/&/g, '\\u0026');
61
+ }
@@ -1,97 +1,97 @@
1
- import type { StoreInfo } from 'brainerce';
2
- import { getNonce } from '@/core/lib/nonce';
3
- import { stripHtmlForSeo } from '@/core/lib/seo';
4
-
5
- interface OrganizationJsonLdProps {
6
- storeInfo: StoreInfo;
7
- baseUrl: string;
8
- }
9
-
10
- // Emits Organization + WebSite JSON-LD on the storefront homepage.
11
- //
12
- // Why these two specifically:
13
- // - Organization → feeds the Google Knowledge Panel (store name, logo,
14
- // description) and is the primary signal AI Overviews / ChatGPT /
15
- // Perplexity ingest when citing the store.
16
- // - WebSite → enables Sitelinks Searchbox (Google) and tells crawlers
17
- // the canonical site URL. The `potentialAction` block is what makes
18
- // the search box appear in branded SERP results.
19
- export async function OrganizationJsonLd({ storeInfo, baseUrl }: OrganizationJsonLdProps) {
20
- const nonce = await getNonce();
21
-
22
- const cleanDescription = stripHtmlForSeo(storeInfo.metaDescription ?? undefined);
23
- const logo =
24
- typeof storeInfo.logo === 'string' && storeInfo.logo.trim() ? storeInfo.logo : undefined;
25
- const contactEmail =
26
- typeof storeInfo.contactEmail === 'string' && storeInfo.contactEmail.trim()
27
- ? storeInfo.contactEmail.trim()
28
- : undefined;
29
- const contactPhone =
30
- typeof storeInfo.contactPhone === 'string' && storeInfo.contactPhone.trim()
31
- ? storeInfo.contactPhone.trim()
32
- : undefined;
33
- // sameAs is the JSON-LD property Google uses to associate a brand with its
34
- // social profiles. Order doesn't matter; we just filter out empties.
35
- const sameAs = storeInfo.socialLinks
36
- ? Object.values(storeInfo.socialLinks).filter(
37
- (url): url is string => typeof url === 'string' && url.trim().length > 0
38
- )
39
- : [];
40
-
41
- const organizationJsonLd: Record<string, unknown> = {
42
- '@context': 'https://schema.org',
43
- '@type': 'Organization',
44
- name: storeInfo.name,
45
- url: baseUrl,
46
- ...(logo ? { logo } : {}),
47
- ...(cleanDescription ? { description: cleanDescription } : {}),
48
- ...(contactEmail ? { email: contactEmail } : {}),
49
- ...(contactPhone ? { telephone: contactPhone } : {}),
50
- ...(sameAs.length > 0 ? { sameAs } : {}),
51
- };
52
-
53
- const websiteJsonLd: Record<string, unknown> = {
54
- '@context': 'https://schema.org',
55
- '@type': 'WebSite',
56
- name: storeInfo.name,
57
- url: baseUrl,
58
- potentialAction: {
59
- '@type': 'SearchAction',
60
- target: {
61
- // Must match the storefront's real search param — the product listing
62
- // reads `?search=` (see use-product-listing.ts), not `?q=`. A wrong
63
- // param makes the sitelinks search box land on an empty listing.
64
- '@type': 'EntryPoint',
65
- urlTemplate: `${baseUrl}/products?search={search_term_string}`,
66
- },
67
- 'query-input': 'required name=search_term_string',
68
- },
69
- };
70
-
71
- return (
72
- <>
73
- <script
74
- type="application/ld+json"
75
- nonce={nonce}
76
- suppressHydrationWarning
77
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(organizationJsonLd) }}
78
- />
79
- <script
80
- type="application/ld+json"
81
- nonce={nonce}
82
- suppressHydrationWarning
83
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(websiteJsonLd) }}
84
- />
85
- </>
86
- );
87
- }
88
-
89
- // Escape `<`, `>`, `&` to \uXXXX so seller-controlled fields can't break out
90
- // of the <script> with `</script>` or inject HTML. Same approach as
91
- // product-json-ld.tsx — keep the two in sync.
92
- function serializeJsonLd(value: unknown): string {
93
- return JSON.stringify(value)
94
- .replace(/</g, '\\u003c')
95
- .replace(/>/g, '\\u003e')
96
- .replace(/&/g, '\\u0026');
97
- }
1
+ import type { StoreInfo } from 'brainerce';
2
+ import { getNonce } from '@/core/lib/nonce';
3
+ import { stripHtmlForSeo } from '@/core/lib/seo';
4
+
5
+ interface OrganizationJsonLdProps {
6
+ storeInfo: StoreInfo;
7
+ baseUrl: string;
8
+ }
9
+
10
+ // Emits Organization + WebSite JSON-LD on the storefront homepage.
11
+ //
12
+ // Why these two specifically:
13
+ // - Organization → feeds the Google Knowledge Panel (store name, logo,
14
+ // description) and is the primary signal AI Overviews / ChatGPT /
15
+ // Perplexity ingest when citing the store.
16
+ // - WebSite → enables Sitelinks Searchbox (Google) and tells crawlers
17
+ // the canonical site URL. The `potentialAction` block is what makes
18
+ // the search box appear in branded SERP results.
19
+ export async function OrganizationJsonLd({ storeInfo, baseUrl }: OrganizationJsonLdProps) {
20
+ const nonce = await getNonce();
21
+
22
+ const cleanDescription = stripHtmlForSeo(storeInfo.metaDescription ?? undefined);
23
+ const logo =
24
+ typeof storeInfo.logo === 'string' && storeInfo.logo.trim() ? storeInfo.logo : undefined;
25
+ const contactEmail =
26
+ typeof storeInfo.contactEmail === 'string' && storeInfo.contactEmail.trim()
27
+ ? storeInfo.contactEmail.trim()
28
+ : undefined;
29
+ const contactPhone =
30
+ typeof storeInfo.contactPhone === 'string' && storeInfo.contactPhone.trim()
31
+ ? storeInfo.contactPhone.trim()
32
+ : undefined;
33
+ // sameAs is the JSON-LD property Google uses to associate a brand with its
34
+ // social profiles. Order doesn't matter; we just filter out empties.
35
+ const sameAs = storeInfo.socialLinks
36
+ ? Object.values(storeInfo.socialLinks).filter(
37
+ (url): url is string => typeof url === 'string' && url.trim().length > 0
38
+ )
39
+ : [];
40
+
41
+ const organizationJsonLd: Record<string, unknown> = {
42
+ '@context': 'https://schema.org',
43
+ '@type': 'Organization',
44
+ name: storeInfo.name,
45
+ url: baseUrl,
46
+ ...(logo ? { logo } : {}),
47
+ ...(cleanDescription ? { description: cleanDescription } : {}),
48
+ ...(contactEmail ? { email: contactEmail } : {}),
49
+ ...(contactPhone ? { telephone: contactPhone } : {}),
50
+ ...(sameAs.length > 0 ? { sameAs } : {}),
51
+ };
52
+
53
+ const websiteJsonLd: Record<string, unknown> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'WebSite',
56
+ name: storeInfo.name,
57
+ url: baseUrl,
58
+ potentialAction: {
59
+ '@type': 'SearchAction',
60
+ target: {
61
+ // Must match the storefront's real search param — the product listing
62
+ // reads `?search=` (see use-product-listing.ts), not `?q=`. A wrong
63
+ // param makes the sitelinks search box land on an empty listing.
64
+ '@type': 'EntryPoint',
65
+ urlTemplate: `${baseUrl}/products?search={search_term_string}`,
66
+ },
67
+ 'query-input': 'required name=search_term_string',
68
+ },
69
+ };
70
+
71
+ return (
72
+ <>
73
+ <script
74
+ type="application/ld+json"
75
+ nonce={nonce}
76
+ suppressHydrationWarning
77
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(organizationJsonLd) }}
78
+ />
79
+ <script
80
+ type="application/ld+json"
81
+ nonce={nonce}
82
+ suppressHydrationWarning
83
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(websiteJsonLd) }}
84
+ />
85
+ </>
86
+ );
87
+ }
88
+
89
+ // Escape `<`, `>`, `&` to \uXXXX so seller-controlled fields can't break out
90
+ // of the <script> with `</script>` or inject HTML. Same approach as
91
+ // product-json-ld.tsx — keep the two in sync.
92
+ function serializeJsonLd(value: unknown): string {
93
+ return JSON.stringify(value)
94
+ .replace(/</g, '\\u003c')
95
+ .replace(/>/g, '\\u003e')
96
+ .replace(/&/g, '\\u0026');
97
+ }