create-brainerce-store 1.50.1 → 1.51.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 (189) hide show
  1. package/dist/index.js +171 -66
  2. package/messages/en.json +4 -1
  3. package/messages/he.json +4 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.claude/commands/design.md +51 -0
  6. package/templates/nextjs/base/.env.local.ejs +5 -0
  7. package/templates/nextjs/base/.eslintrc.json +57 -3
  8. package/templates/nextjs/base/AGENTS.md +41 -0
  9. package/templates/nextjs/base/AI-GUIDE.md +94 -0
  10. package/templates/nextjs/base/CLAUDE.md +42 -0
  11. package/templates/nextjs/base/TRANSLATIONS.md +1 -1
  12. package/templates/nextjs/base/next.config.ts +18 -10
  13. package/templates/nextjs/base/package.json.ejs +5 -1
  14. package/templates/nextjs/base/src/app/account/page.tsx +5 -5
  15. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +1 -1
  16. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +2 -2
  17. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +1 -1
  18. package/templates/nextjs/base/src/app/auth/callback/page.tsx +4 -4
  19. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +4 -4
  20. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +2 -2
  21. package/templates/nextjs/base/src/app/cart/page.tsx +2 -173
  22. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -11
  23. package/templates/nextjs/base/src/app/contact/page.tsx +3 -3
  24. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +2 -2
  25. package/templates/nextjs/base/src/app/forgot-password/page.tsx +4 -4
  26. package/templates/nextjs/base/src/app/layout.tsx.ejs +16 -14
  27. package/templates/nextjs/base/src/app/login/page.tsx +4 -4
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +19 -12
  29. package/templates/nextjs/base/src/app/page.tsx +4 -4
  30. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +3 -3
  31. package/templates/nextjs/base/src/app/payment-complete/page.tsx +1 -1
  32. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -6
  33. package/templates/nextjs/base/src/app/products/page.tsx +4 -623
  34. package/templates/nextjs/base/src/app/register/page.tsx +4 -4
  35. package/templates/nextjs/base/src/app/reset-password/page.tsx +5 -5
  36. package/templates/nextjs/base/src/app/sitemap.ts +1 -1
  37. package/templates/nextjs/base/src/app/verify-email/page.tsx +5 -5
  38. package/templates/nextjs/base/src/components/account/address-book.tsx +3 -3
  39. package/templates/nextjs/base/src/components/account/order-customizations.tsx +3 -3
  40. package/templates/nextjs/base/src/components/account/order-history.tsx +13 -5
  41. package/templates/nextjs/base/src/components/account/order-payment-block.tsx +2 -2
  42. package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +2 -2
  43. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +2 -2
  44. package/templates/nextjs/base/src/components/account/profile-section.tsx +3 -3
  45. package/templates/nextjs/base/src/components/auth/login-form.tsx +4 -4
  46. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +4 -4
  47. package/templates/nextjs/base/src/components/auth/register-form.tsx +4 -4
  48. package/templates/nextjs/base/src/components/brainerce-bot.tsx +2 -2
  49. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +24 -3
  50. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +2 -2
  51. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +2 -2
  52. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +4 -4
  53. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +6 -6
  54. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +3 -3
  55. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +3 -3
  56. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +3 -3
  57. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +2 -2
  58. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +3 -3
  59. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -0
  60. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -0
  61. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -0
  62. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +278 -0
  63. package/templates/nextjs/base/src/{hooks → core/hooks}/use-search.ts +1 -1
  64. package/templates/nextjs/base/src/{lib → core/lib}/brainerce.ts.ejs +1 -1
  65. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -0
  66. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -0
  67. package/templates/nextjs/base/src/{lib → core/lib}/resolve-currency.ts +1 -1
  68. package/templates/nextjs/base/src/{lib → core/lib}/safe-redirect.ts +1 -1
  69. package/templates/nextjs/base/src/{lib → core/lib}/sanitize-html.ts +12 -0
  70. package/templates/nextjs/base/src/{lib → core/lib}/sanitize.ts.ejs +1 -1
  71. package/templates/nextjs/base/src/{lib → core/lib}/use-currency.ts +1 -1
  72. package/templates/nextjs/base/src/{providers → core/providers}/store-provider.tsx.ejs +4 -4
  73. package/templates/nextjs/base/src/middleware.ts.ejs +2 -2
  74. package/templates/nextjs/base/src/{components → ui}/cart/cart-bundle-offer.tsx +5 -5
  75. package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +6 -6
  76. package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +1 -1
  77. package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +4 -4
  78. package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +5 -5
  79. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +149 -0
  80. package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +4 -4
  81. package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +4 -4
  82. package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +2 -2
  83. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -0
  84. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -0
  85. package/templates/nextjs/base/src/ui/home/hero-section.tsx +29 -0
  86. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
  87. package/templates/nextjs/base/src/{components/content → ui/layout}/faq-section.tsx.ejs +2 -2
  88. package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +3 -3
  89. package/templates/nextjs/base/src/{components/content → ui/layout}/rich-text-block.tsx.ejs +1 -1
  90. package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +1 -1
  91. package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +18 -51
  92. package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +1 -1
  93. package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +6 -6
  94. package/templates/nextjs/base/src/{components/products → ui/product}/modifier-group-selector.tsx +10 -77
  95. package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +2 -2
  96. package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +10 -10
  97. package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +45 -206
  98. package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
  99. package/templates/nextjs/base/src/ui/product/product-listing.tsx +429 -0
  100. package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +4 -4
  101. package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +2 -2
  102. package/templates/nextjs/base/src/{components/reviews → ui/product}/reviews-section.tsx +1 -1
  103. package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +2 -2
  104. package/templates/nextjs/base/src/{components/products → ui/product}/variant-selector.tsx +3 -3
  105. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -0
  106. package/templates/nextjs/base/src/{components → ui}/shared/loading-spinner.tsx +2 -2
  107. package/templates/nextjs/base/tailwind.config.ts +12 -0
  108. package/templates/nextjs/designs/passepartout/app-overlay/layout.tsx.ejs +253 -0
  109. package/templates/nextjs/designs/passepartout/globals.css +412 -0
  110. package/templates/nextjs/designs/passepartout/messages-patch/en.json +87 -0
  111. package/templates/nextjs/designs/passepartout/messages-patch/he.json +87 -0
  112. package/templates/nextjs/designs/passepartout/ui/cart/cart-bundle-offer.tsx +111 -0
  113. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +39 -0
  114. package/templates/nextjs/designs/passepartout/ui/cart/cart-upgrade-banner.tsx +141 -0
  115. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +149 -0
  116. package/templates/nextjs/designs/passepartout/ui/cart/free-shipping-bar.tsx +62 -0
  117. package/templates/nextjs/designs/passepartout/ui/cart/reservation-countdown.tsx +103 -0
  118. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +401 -0
  119. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +139 -0
  120. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +45 -0
  121. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +103 -0
  122. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +159 -0
  123. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +155 -0
  124. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +60 -0
  125. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +34 -0
  126. package/templates/nextjs/designs/passepartout/ui/product/discount-badge.tsx +22 -0
  127. package/templates/nextjs/designs/passepartout/ui/product/frequently-bought-together.tsx +205 -0
  128. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +111 -0
  129. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +265 -0
  130. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +667 -0
  131. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +35 -0
  132. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +427 -0
  133. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +113 -0
  134. package/templates/nextjs/designs/passepartout/ui/product/review-form.tsx +294 -0
  135. package/templates/nextjs/designs/passepartout/ui/product/reviews-section.tsx.ejs +96 -0
  136. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +52 -0
  137. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +307 -0
  138. package/templates/nextjs/designs/passepartout/ui/shared/fly-to-cart.ts +85 -0
  139. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +33 -0
  140. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +87 -0
  141. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +121 -0
  142. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +25 -0
  143. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +97 -0
  144. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +103 -0
  145. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
  146. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +117 -0
  147. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +52 -0
  148. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +79 -0
  149. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -0
  150. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -0
  151. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -0
  152. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -0
  153. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -0
  154. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -0
  155. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -0
  156. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +74 -0
  157. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +34 -0
  158. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -0
  159. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -0
  160. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +443 -0
  161. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +17 -0
  162. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +166 -0
  163. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +163 -0
  164. package/templates/nextjs/ui-canvas/product/price-display.tsx +50 -0
  165. package/templates/nextjs/ui-canvas/product/product-card.tsx +197 -0
  166. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -0
  167. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -0
  168. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -0
  169. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +95 -0
  170. package/templates/nextjs/ui-canvas/product/review-form.tsx +269 -0
  171. package/templates/nextjs/ui-canvas/product/reviews-section.tsx +78 -0
  172. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +43 -0
  173. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +210 -0
  174. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +29 -0
  175. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +25 -0
  176. package/templates/nextjs/base/src/app/home-client.tsx +0 -98
  177. package/templates/nextjs/base/src/components/products/allergen-chips.tsx +0 -78
  178. /package/templates/nextjs/base/src/{lib → core/lib}/auth.ts +0 -0
  179. /package/templates/nextjs/base/src/{lib → core/lib}/csrf.ts +0 -0
  180. /package/templates/nextjs/base/src/{lib → core/lib}/navigation.tsx.ejs +0 -0
  181. /package/templates/nextjs/base/src/{lib → core/lib}/nonce.ts +0 -0
  182. /package/templates/nextjs/base/src/{lib → core/lib}/seo.ts +0 -0
  183. /package/templates/nextjs/base/src/{lib → core/lib}/store-info.ts +0 -0
  184. /package/templates/nextjs/base/src/{lib → core/lib}/translations.ts.ejs +0 -0
  185. /package/templates/nextjs/base/src/{lib → core/lib}/utils.ts +0 -0
  186. /package/templates/nextjs/base/src/{lib → core/lib}/validation.ts +0 -0
  187. /package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +0 -0
  188. /package/templates/nextjs/base/src/{components → ui}/layout/language-switcher.tsx.ejs +0 -0
  189. /package/templates/nextjs/base/src/{components/content → ui/layout}/site-footer.tsx.ejs +0 -0
@@ -0,0 +1,94 @@
1
+ # AI-GUIDE — designing this store safely
2
+
3
+ You (an AI coding assistant, or a human designer) have been asked to redesign
4
+ this storefront. This guide is the contract that lets you go as bold as you
5
+ want without breaking commerce. It was distilled from real agency builds.
6
+
7
+ ## The one rule
8
+
9
+ **`src/core/` is the platform's. `src/ui/` is yours.**
10
+
11
+ Everything under `src/ui/` may be rewritten from scratch — layouts, markup,
12
+ classes, animations, entire visual languages. (Canvas-mode stores —
13
+ scaffolded with `--canvas` — ship `src/ui/` as bare unstyled skeletons with
14
+ `DESIGN ME` markers; same rules apply, you're just starting from zero.) Everything under `src/core/`
15
+ (and `src/app/api/`) keeps the store working: cart, checkout, variants,
16
+ inventory, auth, SEO, i18n. Never modify it. If a redesign seems to require a
17
+ core change, stop and say so instead.
18
+
19
+ ## File map
20
+
21
+ | Path | Owner | Notes |
22
+ |------|-------|-------|
23
+ | `src/ui/layout/` | you | header, footer, announcement bar |
24
+ | `src/ui/home/` | you | homepage sections |
25
+ | `src/ui/product/` | you | card, grid, listing, product page, price, badges, selectors |
26
+ | `src/ui/cart/` | you | cart page pieces |
27
+ | `src/ui/shared/` | you | loader, image wrapper |
28
+ | `src/app/globals.css` | you | tokens + global styles |
29
+ | `src/core/**` | platform | hooks, lib, providers — read, never edit |
30
+ | `src/components/{checkout,auth,account,seo}` | platform | core-styled in v1; they inherit your tokens |
31
+ | `src/app/**` | platform | thin routes; edit only to wire new ui components |
32
+
33
+ ## The hook contract (your data supply)
34
+
35
+ UI components get *everything* from `@/core/hooks/*` and
36
+ `@/core/providers/store-provider` — never from `@/core/lib` directly:
37
+
38
+ - `useHomeData()` → `{ products, banners, loading }`
39
+ - `useProductListing()` → products + filters/sort/facets/pagination + handlers
40
+ - `useProductPage(product)` → variant/image selection, `priceInfo`, `inventory`,
41
+ `quantity`, `handleAddToCart`, `addingToCart`, `addedMessage`, customization
42
+ + modifier state and errors
43
+ - `useCartPage()` / `useCart()` → `{ cart, itemCount, totals, refreshCart }`
44
+
45
+ Hooks return data and handlers, never JSX. All catalog content (names, prices,
46
+ images, stock) comes from them — **never hardcode products**.
47
+
48
+ ## Verification loop (before you declare anything done)
49
+
50
+ 1. `pnpm exec tsc --noEmit` → must be 0.
51
+ 2. `pnpm dev` → drive the real flow you changed: home → listing → product →
52
+ add to cart → cart → checkout page renders.
53
+ 3. Screenshot desktop (1440) AND mobile (390), in the store's language.
54
+ 4. If the store is RTL: check nothing is left-anchored that should be
55
+ right-anchored, and directional arrows point with the reading direction.
56
+
57
+ ## Hard-won gotchas (each one cost a real debugging session)
58
+
59
+ - **RTL + cloned images**: `next/image` renders its `<img>` with `inset: 0`.
60
+ If you clone one for an animation and set `left`, the leftover `right: 0`
61
+ wins in RTL documents (over-constrained boxes resolve to the RIGHT).
62
+ Set `inset: 'auto'` before positioning.
63
+ - **Never name a custom class `overline`** — it is a Tailwind utility
64
+ (`text-decoration-line: overline`) and will draw a line above your text.
65
+ - **`rounded-md` / `rounded-lg` are fixed radii** — they ignore your
66
+ `--radius` token. For token-driven corners use `rounded` (DEFAULT) or set
67
+ radii explicitly.
68
+ - **Hebrew has no uppercase** — remove `text-transform: uppercase` and wide
69
+ `letter-spacing` from heading styles; they are Latin idioms that distort
70
+ Hebrew (tracking is acceptable only at caption sizes).
71
+ - **Logical properties only**: `ms-/me-`, `ps-/pe-`, `start-/end-`,
72
+ `text-start/end`. Never `ml-/mr-`, `left-/right-`, `text-left/right`,
73
+ `flex-row-reverse`.
74
+ - **Motion**: wrap every animation in a `prefers-reduced-motion: reduce`
75
+ guard. One well-orchestrated reveal beats scattered effects.
76
+ - **Fonts**: any face used for body or headlines must include the store's
77
+ script (e.g. Google Fonts `hebrew` subset). Latin-only display faces are
78
+ accent-only.
79
+
80
+ ## Never do
81
+
82
+ - Modify `src/core/`, `src/app/api/`, or checkout/auth/account internals.
83
+ - Remove accessibility attributes (`aria-*`, `role`, `alt`, focus styles).
84
+ - Hardcode catalog content, prices, or currency symbols.
85
+ - Swallow the add-to-cart / checkout error states — restyle them, keep them.
86
+ - Introduce new runtime dependencies without flagging it.
87
+
88
+ ## Adding things
89
+
90
+ - New homepage section → new component in `src/ui/home/`, composed in
91
+ `home-client.tsx`, data via existing hooks only.
92
+ - New UI copy → add keys to **both** `messages/he.json` and
93
+ `messages/en.json` (or all supported locales), use `useTranslations()`.
94
+ - New microinteraction → CSS-first; JS only for things CSS cannot do.
@@ -0,0 +1,42 @@
1
+ # CLAUDE.md — this is a Brainerce storefront
2
+
3
+ This store's commerce engine (cart, checkout, variants, auth, i18n) is
4
+ already wired and working. Your job here is almost always **design**.
5
+
6
+ ## The one rule
7
+
8
+ **`src/core/` is the platform's. `src/ui/` is yours.**
9
+
10
+ The shipped `src/ui/` is a working reference, not a design to preserve —
11
+ full delete-and-rebuild of ui files is encouraged and expected. (Stores
12
+ scaffolded with `--canvas` ship `src/ui/` as bare unstyled skeletons —
13
+ there is no reference look at all; the design is entirely yours to create.)
14
+ Rewrite anything under `src/ui/` and `src/app/globals.css` as boldly as you
15
+ like — the store keeps working. Never modify `src/core/`, `src/app/api/`, or
16
+ the checkout/auth/account components. Data and behavior come exclusively from
17
+ `@/core/hooks/*` and `@/core/providers/store-provider` — hooks return state
18
+ and handlers, never JSX. Never hardcode catalog content.
19
+
20
+ **Read `AI-GUIDE.md` before any redesign** — it has the full file map, hook
21
+ contracts, and hard-won RTL/i18n gotchas that will save you real debugging
22
+ time.
23
+
24
+ ## Redesigning? Use the ready-made flow
25
+
26
+ Run `/design <your art-direction brief>` — it walks the whole process:
27
+ concept → implementation in `ui/` → verification.
28
+
29
+ ## Verify before declaring done
30
+
31
+ 1. `pnpm exec tsc --noEmit` → 0 errors
32
+ 2. `pnpm dev` → drive the changed flow in a real browser (home → product →
33
+ add to cart → cart)
34
+ 3. Screenshot desktop (1440px) and mobile (390px)
35
+ 4. RTL stores: check anchoring and arrow directions
36
+
37
+ ## i18n
38
+
39
+ Every user-facing string goes through `useTranslations()` with keys in **all**
40
+ files under `messages/`. Hebrew: no uppercase transforms, no wide
41
+ letter-spacing on headings, logical CSS properties only (`ms-/me-`, `ps-/pe-`,
42
+ `start-/end-`).
@@ -37,7 +37,7 @@ You write **one** `fetch` and it works for every language.
37
37
 
38
38
  ```tsx
39
39
  // src/app/[locale]/products/[slug]/page.tsx
40
- import { getServerClient } from '@/lib/server-client';
40
+ import { getServerClient } from '@/core/lib/server-client';
41
41
  import { headers } from 'next/headers';
42
42
 
43
43
  export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
@@ -1,4 +1,5 @@
1
1
  import type { NextConfig } from 'next';
2
+ import { OPTIMIZED_IMAGE_HOSTS } from './src/core/lib/image-hosts';
2
3
 
3
4
  // Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
4
5
  // for a production build. Next.js inlines NEXT_PUBLIC_* into the client
@@ -28,16 +29,23 @@ const nextConfig: NextConfig = {
28
29
  serverExternalPackages: ['isomorphic-dompurify'],
29
30
  images: {
30
31
  // The storefront is a consumer of the Brainerce API — it has to render
31
- // whatever image URLs the API returns. In practice those URLs can be on
32
- // cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
33
- // self-hosted) depending on whether the product's image-import job has
34
- // completed on the backend. Rather than hard-fail on unknown hosts, skip
35
- // the server-side optimizer entirely and let the browser fetch each image
36
- // directly from origin. No server-side fetching no SSRF or DoS surface
37
- // on this Next server. Trade-off: no webp/resize/lazy optimization, so
38
- // LCP is marginally worse. Acceptable; the storefront is not the right
39
- // layer to enforce a hostname policy.
40
- unoptimized: true,
32
+ // whatever image URLs the API returns. In practice those URLs are
33
+ // usually on cdn.brainerce.com, but a product/variant can still carry a
34
+ // raw upstream-merchant URL (WooCommerce, Shopify, self-hosted) while
35
+ // its image-import job is pending or failed. Rather than allowlist every
36
+ // possible merchant host (or hard-fail on them), only cdn.brainerce.com
37
+ // goes through the server-side optimizer; components render product/blog
38
+ // images via `CdnImage` (src/ui/shared/cdn-image.tsx), which
39
+ // detects any other host and renders it `unoptimized` an unresized
40
+ // direct-from-origin fetch, same as this template's prior behavior —
41
+ // instead of next/image throwing on an unconfigured hostname. No
42
+ // server-side fetching of unknown hosts → no SSRF/DoS surface on this
43
+ // Next server.
44
+ remotePatterns: OPTIMIZED_IMAGE_HOSTS.map((hostname) => ({
45
+ protocol: 'https' as const,
46
+ hostname,
47
+ pathname: '/**',
48
+ })),
41
49
  },
42
50
  async headers() {
43
51
  return [
@@ -16,7 +16,8 @@
16
16
  "react-dom": "^19.0.0",
17
17
  "clsx": "^2.1.0",
18
18
  "tailwind-merge": "^2.2.0",
19
- "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>"
19
+ "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
20
+ "sharp": "^0.35.3"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/node": "^20.0.0",
@@ -31,5 +32,8 @@
31
32
  },
32
33
  "engines": {
33
34
  "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
35
+ },
36
+ "pnpm": {
37
+ "onlyBuiltDependencies": ["sharp"]
34
38
  }
35
39
  }
@@ -1,15 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect, useState } from 'react';
4
- import { useRouter } from '@/lib/navigation';
4
+ import { useRouter } from '@/core/lib/navigation';
5
5
  import type { CustomerProfile, Order } from 'brainerce';
6
- import { getClient } from '@/lib/brainerce';
7
- import { useAuth } from '@/providers/store-provider';
8
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
6
+ import { getClient } from '@/core/lib/brainerce';
7
+ import { useAuth } from '@/core/providers/store-provider';
8
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
9
  import { ProfileSection } from '@/components/account/profile-section';
10
10
  import { AddressBook } from '@/components/account/address-book';
11
11
  import { OrderHistory } from '@/components/account/order-history';
12
- import { useTranslations } from '@/lib/translations';
12
+ import { useTranslations } from '@/core/lib/translations';
13
13
 
14
14
  export default function AccountPage() {
15
15
  const router = useRouter();
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { checkCsrf } from '@/lib/csrf';
2
+ import { checkCsrf } from '@/core/lib/csrf';
3
3
 
4
4
  const TOKEN_COOKIE = 'brainerce_customer_token';
5
5
  const LOGGED_IN_COOKIE = 'brainerce_logged_in';
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { cookies } from 'next/headers';
3
- import { validatePassword } from '@/lib/validation';
4
- import { checkCsrf } from '@/lib/csrf';
3
+ import { validatePassword } from '@/core/lib/validation';
4
+ import { checkCsrf } from '@/core/lib/csrf';
5
5
 
6
6
  const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
7
7
  /\/$/,
@@ -5,7 +5,7 @@
5
5
  // like customers/login and customers/register are the most important to cover.
6
6
  import { NextRequest, NextResponse } from 'next/server';
7
7
  import { cookies } from 'next/headers';
8
- import { checkCsrf } from '@/lib/csrf';
8
+ import { checkCsrf } from '@/core/lib/csrf';
9
9
 
10
10
  const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
11
11
  /\/$/,
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { Suspense, useEffect, useState, useRef } from 'react';
4
4
  import { useSearchParams } from 'next/navigation';
5
- import { useRouter } from '@/lib/navigation';
6
- import { useAuth } from '@/providers/store-provider';
7
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
- import { useTranslations } from '@/lib/translations';
5
+ import { useRouter } from '@/core/lib/navigation';
6
+ import { useAuth } from '@/core/providers/store-provider';
7
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
8
+ import { useTranslations } from '@/core/lib/translations';
9
9
 
10
10
  function OAuthCallbackContent() {
11
11
  const router = useRouter();
@@ -12,11 +12,11 @@ import * as React from 'react';
12
12
  import type { Metadata } from 'next';
13
13
  import { notFound } from 'next/navigation';
14
14
  import Link from 'next/link';
15
- import Image from 'next/image';
15
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
16
16
 
17
- import { getServerClient } from '@/lib/brainerce';
18
- import { sanitizeHtml } from '@/lib/sanitize';
19
- import { decodeSlug } from '@/lib/utils';
17
+ import { getServerClient } from '@/core/lib/brainerce';
18
+ import { sanitizeHtml } from '@/core/lib/sanitize';
19
+ import { decodeSlug } from '@/core/lib/utils';
20
20
 
21
21
  <% if (i18nEnabled) { %>
22
22
  type PageProps = {
@@ -10,9 +10,9 @@
10
10
  import * as React from 'react';
11
11
  import type { Metadata } from 'next';
12
12
  import Link from 'next/link';
13
- import Image from 'next/image';
13
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
14
14
 
15
- import { getServerClient } from '@/lib/brainerce';
15
+ import { getServerClient } from '@/core/lib/brainerce';
16
16
 
17
17
  <% if (i18nEnabled) { %>
18
18
  type PageProps = {
@@ -1,178 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
4
- import { Link } from '@/lib/navigation';
5
- import type {
6
- CartRecommendationsResponse,
7
- CartUpgradesResponse,
8
- CartBundlesResponse,
9
- } from 'brainerce';
10
- import { getClient } from '@/lib/brainerce';
11
- import { useCart } from '@/providers/store-provider';
12
- import { CartItem } from '@/components/cart/cart-item';
13
- import { CartUpgradeBanner } from '@/components/cart/cart-upgrade-banner';
14
- import { CartBundleOfferCard } from '@/components/cart/cart-bundle-offer';
15
- import { CartSummary } from '@/components/cart/cart-summary';
16
- import { CouponInput } from '@/components/cart/coupon-input';
17
- import { CartNudges } from '@/components/cart/cart-nudges';
18
- import { FreeShippingBar } from '@/components/cart/free-shipping-bar';
19
- import { ReservationCountdown } from '@/components/cart/reservation-countdown';
20
- import { CartRecommendationSection } from '@/components/products/recommendation-section';
21
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
22
- import { useTranslations } from '@/lib/translations';
3
+ import { CartView } from '@/ui/cart/cart-view';
23
4
 
24
5
  export default function CartPage() {
25
- const { cart, cartLoading, refreshCart, itemCount } = useCart();
26
- const t = useTranslations('cart');
27
- const tc = useTranslations('common');
28
- const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
29
- const [upgrades, setUpgrades] = useState<CartUpgradesResponse | null>(null);
30
- const [bundles, setBundles] = useState<CartBundlesResponse | null>(null);
31
-
32
- // Load recommendations, upgrades, and bundles in a single request
33
- useEffect(() => {
34
- if (!cart?.id || cart.items.length === 0) {
35
- setCartRecs(null);
36
- setUpgrades(null);
37
- setBundles(null);
38
- return;
39
- }
40
- const client = getClient();
41
- client
42
- .getCart(cart.id, { include: ['recommendations', 'upgrades', 'bundles'] })
43
- .then((enriched) => {
44
- setCartRecs(enriched.recommendations ?? null);
45
- setUpgrades(enriched.upgrades ?? null);
46
- setBundles(enriched.bundles ?? null);
47
- })
48
- .catch(() => {});
49
- }, [cart?.id, cart?.items.length]);
50
-
51
- if (cartLoading) {
52
- return (
53
- <div className="flex min-h-[60vh] items-center justify-center">
54
- <LoadingSpinner size="lg" />
55
- </div>
56
- );
57
- }
58
-
59
- // Empty cart state
60
- if (!cart || cart.items.length === 0) {
61
- return (
62
- <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
63
- <svg
64
- className="text-muted-foreground mx-auto mb-4 h-16 w-16"
65
- fill="none"
66
- viewBox="0 0 24 24"
67
- stroke="currentColor"
68
- >
69
- <path
70
- strokeLinecap="round"
71
- strokeLinejoin="round"
72
- strokeWidth={1.5}
73
- d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
74
- />
75
- </svg>
76
- <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
77
- <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
78
- <Link
79
- href="/products"
80
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
81
- >
82
- {tc('continueShopping')}
83
- </Link>
84
- </div>
85
- );
86
- }
87
-
88
- return (
89
- <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
90
- <h1 className="text-foreground mb-6 text-2xl font-bold">
91
- {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
92
- </h1>
93
-
94
- {/* Reservation countdown */}
95
- {cart.reservation?.hasReservation && (
96
- <ReservationCountdown reservation={cart.reservation} className="mb-6" />
97
- )}
98
-
99
- <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
100
- {/* Cart Items */}
101
- <div className="lg:col-span-2">
102
- {/* Nudges */}
103
- {cart.nudges && cart.nudges.length > 0 && (
104
- <CartNudges nudges={cart.nudges} className="mb-4" />
105
- )}
106
-
107
- {/* Cart items */}
108
- <div>
109
- {cart.items.map((item) => (
110
- <div key={item.id}>
111
- <CartItem item={item} onUpdate={refreshCart} />
112
- {upgrades?.upgrades?.[item.productId] && (
113
- <CartUpgradeBanner
114
- suggestion={upgrades.upgrades[item.productId]}
115
- cartItem={item}
116
- onUpgrade={refreshCart}
117
- className="mb-2 ms-24"
118
- />
119
- )}
120
- </div>
121
- ))}
122
- </div>
123
-
124
- {/* Bundle offers */}
125
- {bundles?.bundles && bundles.bundles.length > 0 && (
126
- <div className="mt-6 space-y-3">
127
- <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
128
- {bundles.bundles.map((offer) => (
129
- <CartBundleOfferCard
130
- key={offer.id}
131
- offer={offer}
132
- cartId={cart.id}
133
- onAdd={refreshCart}
134
- />
135
- ))}
136
- </div>
137
- )}
138
-
139
- {/* Coupon input */}
140
- <div className="border-border mt-6 border-t pt-4">
141
- <CouponInput cart={cart} onUpdate={refreshCart} />
142
- </div>
143
- </div>
144
-
145
- {/* Summary sidebar */}
146
- <div className="lg:col-span-1">
147
- <div className="bg-muted/50 border-border sticky top-24 rounded-lg border p-6">
148
- <FreeShippingBar className="mb-4" />
149
- <CartSummary />
150
-
151
- <Link
152
- href="/checkout"
153
- className="bg-primary text-primary-foreground mt-6 inline-flex w-full items-center justify-center rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90"
154
- >
155
- {t('proceedToCheckout')}
156
- </Link>
157
-
158
- <Link
159
- href="/products"
160
- className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
161
- >
162
- {tc('continueShopping')}
163
- </Link>
164
- </div>
165
- </div>
166
- </div>
167
-
168
- {/* Cross-sell recommendations */}
169
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
170
- <CartRecommendationSection
171
- title={t('youMightAlsoNeed')}
172
- items={cartRecs.recommendations}
173
- className="mt-10"
174
- />
175
- )}
176
- </div>
177
- );
6
+ return <CartView />;
178
7
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { Suspense, useEffect, useState, useCallback, useRef } from 'react';
4
4
  import { useSearchParams } from 'next/navigation';
5
- import Image from 'next/image';
6
- import { Link } from '@/lib/navigation';
5
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
6
+ import { Link } from '@/core/lib/navigation';
7
7
  import type {
8
8
  Checkout,
9
9
  ShippingRate,
@@ -14,9 +14,9 @@ import type {
14
14
  CheckoutCustomFieldDefinition,
15
15
  } from 'brainerce';
16
16
  import { formatPrice } from 'brainerce';
17
- import { getClient } from '@/lib/brainerce';
18
- import { useStoreInfo, useCart, useAuth } from '@/providers/store-provider';
19
- import { useCurrency } from '@/lib/use-currency';
17
+ import { getClient } from '@/core/lib/brainerce';
18
+ import { useStoreInfo, useCart, useAuth } from '@/core/providers/store-provider';
19
+ import { useCurrency } from '@/core/lib/use-currency';
20
20
  import { CheckoutForm } from '@/components/checkout/checkout-form';
21
21
  import { ShippingStep } from '@/components/checkout/shipping-step';
22
22
  import { PaymentStep } from '@/components/checkout/payment-step';
@@ -25,12 +25,12 @@ import { PickupStep } from '@/components/checkout/pickup-step';
25
25
  import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
26
26
  import { TaxDisplay } from '@/components/checkout/tax-display';
27
27
  import { OrderBumpCard } from '@/components/checkout/order-bump-card';
28
- import { CouponInput } from '@/components/cart/coupon-input';
29
- import { ReservationCountdown } from '@/components/cart/reservation-countdown';
30
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
31
- import { useTranslations } from '@/lib/translations';
32
- import { cn } from '@/lib/utils';
33
- import { isValidCheckoutId } from '@/lib/safe-redirect';
28
+ import { CouponInput } from '@/ui/cart/coupon-input';
29
+ import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
30
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
31
+ import { useTranslations } from '@/core/lib/translations';
32
+ import { cn } from '@/core/lib/utils';
33
+ import { isValidCheckoutId } from '@/core/lib/safe-redirect';
34
34
 
35
35
  type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
36
36
 
@@ -261,6 +261,7 @@ function CheckoutContent() {
261
261
  lastName: address.lastName,
262
262
  phone: address.phone,
263
263
  acceptsMarketing: consent.acceptsMarketing,
264
+ notes: address.notes,
264
265
  });
265
266
  setCheckout(updated);
266
267
  setStep('payment');
@@ -34,9 +34,9 @@
34
34
 
35
35
  import { useEffect, useMemo, useState } from 'react';
36
36
  import type { ContactFormPublic, ContactFormPublicField } from 'brainerce';
37
- import { getClient } from '@/lib/brainerce';
38
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
39
- import { useTranslations } from '@/lib/translations';
37
+ import { getClient } from '@/core/lib/brainerce';
38
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
39
+ import { useTranslations } from '@/core/lib/translations';
40
40
 
41
41
  type FieldValue = string | string[] | boolean;
42
42
 
@@ -11,8 +11,8 @@
11
11
  import * as React from 'react';
12
12
  import type { Metadata } from 'next';
13
13
 
14
- import { getServerClient } from '@/lib/brainerce';
15
- import { FaqSection } from '@/components/content/faq-section';
14
+ import { getServerClient } from '@/core/lib/brainerce';
15
+ import { FaqSection } from '@/ui/layout/faq-section';
16
16
  <% if (i18nEnabled) { %>
17
17
  type PageProps = {
18
18
  params: Promise<{ locale: string }>;
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { Link } from '@/lib/navigation';
5
- import { getClient } from '@/lib/brainerce';
6
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
- import { useTranslations } from '@/lib/translations';
4
+ import { Link } from '@/core/lib/navigation';
5
+ import { getClient } from '@/core/lib/brainerce';
6
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
7
+ import { useTranslations } from '@/core/lib/translations';
8
8
 
9
9
  export default function ForgotPasswordPage() {
10
10
  const t = useTranslations('auth');
@@ -1,14 +1,14 @@
1
1
  <% if (i18nEnabled) { %>
2
2
  import type { Metadata } from 'next';
3
3
  <%- fontImport %>
4
- import { StoreProvider } from '@/providers/store-provider';
5
- import { AnnouncementBar } from '@/components/content/announcement-bar';
6
- import { SiteHeader } from '@/components/content/site-header';
7
- import { SiteFooter } from '@/components/content/site-footer';
4
+ import { StoreProvider } from '@/core/providers/store-provider';
5
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
6
+ import { SiteHeader } from '@/ui/layout/site-header';
7
+ import { SiteFooter } from '@/ui/layout/site-footer';
8
8
  import { BrainerceBotWidget } from '@/components/brainerce-bot';
9
- import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
9
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
10
  import { getDirection, supportedLocales } from '@/i18n';
11
- import { getNonce } from '@/lib/nonce';
11
+ import { getNonce } from '@/core/lib/nonce';
12
12
  import '../globals.css';
13
13
 
14
14
  <%- fontVariable %>
@@ -91,11 +91,12 @@ export default async function RootLayout({
91
91
  merchant can toggle it per sales channel in the dashboard. */}
92
92
  <script
93
93
  defer
94
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_API_URL || 'https://api.brainerce.com'}/t.js`}
94
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
95
95
  data-channel={
96
96
  process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
97
97
  }
98
98
  nonce={nonce}
99
+ suppressHydrationWarning
99
100
  />
100
101
  </head>
101
102
  <body className={font.className}>
@@ -115,13 +116,13 @@ export default async function RootLayout({
115
116
  <% } else { %>
116
117
  import type { Metadata } from 'next';
117
118
  <%- fontImport %>
118
- import { StoreProvider } from '@/providers/store-provider';
119
- import { AnnouncementBar } from '@/components/content/announcement-bar';
120
- import { SiteHeader } from '@/components/content/site-header';
121
- import { SiteFooter } from '@/components/content/site-footer';
119
+ import { StoreProvider } from '@/core/providers/store-provider';
120
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
121
+ import { SiteHeader } from '@/ui/layout/site-header';
122
+ import { SiteFooter } from '@/ui/layout/site-footer';
122
123
  import { BrainerceBotWidget } from '@/components/brainerce-bot';
123
- import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
124
- import { getNonce } from '@/lib/nonce';
124
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
125
+ import { getNonce } from '@/core/lib/nonce';
125
126
  import './globals.css';
126
127
 
127
128
  <%- fontVariable %>
@@ -197,11 +198,12 @@ export default async function RootLayout({
197
198
  merchant can toggle it per sales channel in the dashboard. */}
198
199
  <script
199
200
  defer
200
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_API_URL || 'https://api.brainerce.com'}/t.js`}
201
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
201
202
  data-channel={
202
203
  process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
203
204
  }
204
205
  nonce={nonce}
206
+ suppressHydrationWarning
205
207
  />
206
208
  </head>
207
209
  <body className={font.className}>