@unbox-plus/cli 0.20.4

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 (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,257 @@
1
+ # Agente 17 — AEO (Answer Engine Optimization)
2
+
3
+ ## Objetivo
4
+
5
+ Preparar o storefront para ser **citado e recomendado por motores de resposta** (ChatGPT,
6
+ Claude, Perplexity, Google AI Overviews). Complementa o SEO clássico (agentes 11 e 14):
7
+ enquanto SEO otimiza pra ranquear em lista de links, AEO otimiza pra loja SER a resposta —
8
+ dados estruturados completos, conteúdo que responde perguntas diretamente e acesso liberado
9
+ aos crawlers de IA.
10
+
11
+ **Pré-requisitos:** a foundation já traz `app/sitemap.ts`, `app/robots.ts` (com os bots de
12
+ IA liberados) e JSON-LD Product inline em `app/(loja)/produto/[productSlug]/page.tsx`. Se o
13
+ agente 14 (SEO) já rodou, **leia o que ele fez antes de editar** — os módulos 3 e 6 estendem
14
+ os mesmos arquivos. Rode DEPOIS do briefing (15) e do QA visual (16): AEO em cima de
15
+ conteúdo placeholder é otimizar mentira.
16
+
17
+ ---
18
+
19
+ ## Apresentação ao usuário
20
+
21
+ Ao iniciar este agente, exiba o menu abaixo e pergunte quais módulos ativar:
22
+
23
+ ```
24
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
+ Unbox AI AEO Agent — Módulos disponíveis
26
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+
28
+ [1] llms.txt
29
+ JÁ EXISTE como rota (app/llms.txt/route.ts), montada do catálogo real:
30
+ produtos com preço, esgotados marcados, categorias e políticas. O que
31
+ falta é a APRESENTAÇÃo da marca (o que ela faz, para quem, diferenciais):
32
+ preencha NEXT_PUBLIC_SITE_DESCRIPTION e estenda lib/llms-txt.ts. Não
33
+ recrie o arquivo em public/: lá ele esconderia a rota e nasceria velho.
34
+ produtos-chave (nome, preço, link) e políticas —
35
+ gerada do catálogo, mesma fonte do sitemap
36
+ ⚙️ Sem dependências
37
+
38
+ [2] FAQ answer-friendly + FAQPage schema
39
+ Resposta direta de 40-60 palavras no 1º parágrafo
40
+ de cada pergunta + JSON-LD FAQPage
41
+ ⚙️ Requer: FAQ REAL da marca (verbatim — nunca inventar)
42
+
43
+ [3] Product schema enriquecido
44
+ shippingDetails, hasMerchantReturnPolicy, SKU/GTIN
45
+ quando houver — estende o JSON-LD da PDP
46
+ ⚙️ Requer: políticas reais de frete/troca da loja
47
+
48
+ [4] FAQ por produto na PDP
49
+ Bloco "Perguntas sobre este produto" + FAQPage por PDP,
50
+ derivado da ficha técnica real do produto
51
+ ⚙️ Requer: ficha técnica/atributos reais no catálogo
52
+
53
+ [5] Política de bots de resposta (conferência)
54
+ A foundation JÁ libera GPTBot, ClaudeBot, PerplexityBot
55
+ etc. — conferir/estender a lista em app/robots.ts
56
+ ⚙️ Sem dependências (decisão reversível da marca)
57
+
58
+ [6] Entidade da marca
59
+ Organization schema completo (sameAs com todas as redes)
60
+ + página Sobre estruturada para citação
61
+ ⚙️ Requer: links reais das redes (briefing/rodapé)
62
+
63
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
64
+ Opções: "all", números separados por vírgula (ex: 1,5,6), ou "none"
65
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
66
+ ```
67
+
68
+ Aguarde a resposta antes de qualquer implementação.
69
+
70
+ ---
71
+
72
+ ## Módulos — especificação técnica
73
+
74
+ ### [1] llms.txt
75
+
76
+ **Arquivo:** `app/llms.txt/route.ts` (novo)
77
+
78
+ Rota que devolve `text/plain` no padrão [llms.txt](https://llmstxt.org): um sumário da loja
79
+ em Markdown, pensado pra um LLM consumir em uma leitura. Gerar dinamicamente da MESMA fonte
80
+ do sitemap (catálogo via `withStoreClient()`; em modo mockup, `lib/enrichment/products.json`
81
+ — não pode quebrar o build sem credenciais):
82
+
83
+ ```ts
84
+ // app/llms.txt/route.ts
85
+ export async function GET() {
86
+ const body = [
87
+ `# ${nomeDaLoja}`,
88
+ ``,
89
+ `> ${descricaoDaLoja}`, // 1-2 frases: o que a loja vende e para quem (do briefing)
90
+ ``,
91
+ `## Categorias`,
92
+ ...tags.map((t) => `- [${t.name}](${URL}/categoria/${t.slug})`),
93
+ ``,
94
+ `## Produtos principais`,
95
+ ...produtos.slice(0, 20).map(
96
+ (p) => `- [${p.title}](${URL}/produto/${p.slug}) — R$ ${preco(p)}`
97
+ ),
98
+ ``,
99
+ `## Políticas`,
100
+ `- [Trocas e devoluções](${URL}/devolucoes)`,
101
+ `- [Termos](${URL}/termos)`,
102
+ ].join("\n");
103
+ return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
104
+ }
105
+ ```
106
+
107
+ Confira os caminhos REAIS das páginas de política desta loja antes de linkar (a foundation
108
+ traz `/devolucoes`, `/termos`, `/privacidade`; o briefing pode ter criado outras).
109
+
110
+ ⚠️ **A foundation já traz um `public/llms.txt` ESTÁTICO de partida** — e em Next.js o
111
+ arquivo em `public/` vence o route handler: a rota nova nunca responderia. Ao criar a rota
112
+ dinâmica, **apague `public/llms.txt`** no mesmo commit (uma fonte de verdade só). Se
113
+ preferir manter o estático, atualize-o à mão e NÃO crie a rota.
114
+
115
+ ### [2] FAQ answer-friendly + FAQPage schema
116
+
117
+ **Arquivos:** onde o FAQ da loja vive — na foundation, o bloco de FAQ da PDP
118
+ (`components/product/pdp/interactive.tsx`, `FaqList`) e/ou a página institucional que o
119
+ briefing tiver criado.
120
+
121
+ Answer engines extraem respostas curtas e autocontidas. Reestruture cada item pra que o
122
+ **primeiro parágrafo responda a pergunta em 40-60 palavras**, sem depender do contexto ao
123
+ redor ("Sim, trocamos em até 7 dias (CDC). Basta..." e não "Conforme mencionado acima...").
124
+ Detalhes vêm depois do parágrafo de resposta.
125
+
126
+ JSON-LD na página (sempre via `JSON.stringify`):
127
+
128
+ ```ts
129
+ const faqSchema = {
130
+ "@context": "https://schema.org",
131
+ "@type": "FAQPage",
132
+ mainEntity: faq.map((item) => ({
133
+ "@type": "Question",
134
+ name: item.pergunta,
135
+ acceptedAnswer: { "@type": "Answer", text: item.respostaDireta },
136
+ })),
137
+ };
138
+ ```
139
+
140
+ **Regra dura:** o conteúdo vem do FAQ REAL da marca (site atual ou briefing), verbatim no
141
+ sentido — pode reescrever a FORMA pra ficar direto, nunca inventar pergunta ou resposta.
142
+ Sem FAQ real → módulo não roda, vira pendência.
143
+
144
+ ### [3] Product schema enriquecido
145
+
146
+ **Arquivo:** `app/(loja)/produto/[productSlug]/page.tsx` (estender o objeto `jsonLd`
147
+ existente — **leia antes**; o QA de honestidade já condicionou o `aggregateRating` a
148
+ avaliações reais, não regrida isso).
149
+
150
+ Answer engines pesam muito frete e política de troca ao recomendar onde comprar. Adicionar
151
+ à `Offer`:
152
+
153
+ ```ts
154
+ offers: {
155
+ "@type": "Offer",
156
+ // ...campos existentes (price, availability, url)
157
+ shippingDetails: {
158
+ "@type": "OfferShippingDetails",
159
+ shippingDestination: { "@type": "DefinedRegion", addressCountry: "BR" },
160
+ // deliveryTime só se a loja tiver prazo padrão REAL declarado
161
+ },
162
+ hasMerchantReturnPolicy: {
163
+ "@type": "MerchantReturnPolicy",
164
+ applicableCountry: "BR",
165
+ returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
166
+ merchantReturnDays: 7, // CDC: 7 dias de arrependimento; ajuste se a política real for maior
167
+ returnMethod: "https://schema.org/ReturnByMail",
168
+ returnFees: "https://schema.org/FreeReturn", // só se for verdade
169
+ },
170
+ },
171
+ sku: p.sku ?? undefined,
172
+ gtin13: p.gtin ?? undefined, // só quando o campo existir no catálogo
173
+ ```
174
+
175
+ Valores de política **têm que refletir a política real** (páginas legais/briefing/decisão
176
+ registrada do lojista). Na dúvida, o mínimo legal (7 dias CDC) e um TODO pro humano
177
+ confirmar.
178
+
179
+ ### [4] FAQ por produto na PDP
180
+
181
+ **Arquivos:** `components/product/pdp/product-faq.tsx` (novo) + inserção na PDP + FAQPage
182
+ JSON-LD por produto.
183
+
184
+ Bloco "Perguntas sobre este produto" com 3-5 perguntas derivadas **da ficha técnica real**
185
+ (peso, medidas, uso, cuidados — o que existir em `lib/enrichment/products.json` ou nos
186
+ atributos do catálogo). Exemplo honesto: "Qual o peso do produto?" → resposta com o dado
187
+ real.
188
+
189
+ - JSON-LD FAQPage no nível da PDP com essas perguntas.
190
+ - **Nunca inventar** resposta que o dado não sustenta; produto sem ficha suficiente fica
191
+ sem o bloco (renderiza null).
192
+
193
+ ### [5] Política de bots de resposta (conferência)
194
+
195
+ **Arquivo:** `app/robots.ts`
196
+
197
+ A foundation JÁ libera os crawlers de IA (lista `AI_AGENTS`: GPTBot, OAI-SearchBot,
198
+ ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User,
199
+ Google-Extended, Applebot-Extended, Amazonbot, meta-externalagent, CCBot) com os mesmos
200
+ `disallow` de conta/carrinho/checkout/pedido/api. Neste módulo: confira que ninguém removeu
201
+ a lista, acrescente bots novos que surgirem, e registre no commit que a permissão é decisão
202
+ reversível da marca. Regra por userAgent específico SUBSTITUI a genérica — os `disallow`
203
+ precisam estar replicados em cada regra (a foundation já faz).
204
+
205
+ ### [6] Entidade da marca
206
+
207
+ **Arquivos:** `app/(loja)/page.tsx` (Organization + WebSite JÁ existem: estenda com sameAs/logo, não duplique) +
208
+ página Sobre.
209
+
210
+ Answer engines montam um "cartão" da marca cruzando fontes. Ajude:
211
+
212
+ ```ts
213
+ const orgSchema = {
214
+ "@context": "https://schema.org",
215
+ "@type": "Organization",
216
+ name: nomeDaLoja,
217
+ url: siteUrl,
218
+ logo: `${siteUrl}/brand/logo.svg`,
219
+ description: descricaoDaMarca, // 1 frase, do briefing
220
+ sameAs: [instagramUrl /* todas as redes REAIS do rodapé/briefing */],
221
+ };
222
+ ```
223
+
224
+ Se o agente 14 (módulo 1) já criou um Organization schema, **estenda** o existente com
225
+ `description` e `sameAs` completos — nunca duplique o bloco.
226
+
227
+ Página **Sobre**: garanta que existe (crie em `app/(loja)/sobre/page.tsx` se o briefing
228
+ tiver o conteúdo) e que o primeiro parágrafo define a marca de forma autocontida (quem é, o
229
+ que vende, desde quando, diferencial) — é o parágrafo que um answer engine cita. Conteúdo
230
+ do briefing (Entendimento da Marca / Posicionamento); nada inventado.
231
+
232
+ ---
233
+
234
+ ## Fluxo do agente
235
+
236
+ 1. **Apresentar o menu** e aguardar a escolha.
237
+ 2. **Verificar pré-requisitos:** `app/robots.ts` e `app/sitemap.ts` existem; ler o que os
238
+ agentes 11/14 já implementaram (e o que o QA de honestidade já condicionou no JSON-LD).
239
+ 3. **Implementar em ordem numérica** — módulos independentes entre si.
240
+ 4. **Ao final:** listar o que foi implementado, com os TODOs que exigem confirmação humana
241
+ (política de troca real, GTIN, redes sociais faltantes) — pendências no formato do
242
+ briefing (bloqueiam o go-live, não o build).
243
+
244
+ ---
245
+
246
+ ## Regras
247
+
248
+ - **Você nunca inventa dado** (regra de ouro da loja): FAQ verbatim; políticas de
249
+ frete/troca reais; ficha técnica real. O que não houver, não entra — vira pendência
250
+ listada. Decisão de manter algo sem fonte é do LOJISTA, registrada
251
+ (`marca/honestidade-permitido.txt`).
252
+ - **Nunca duplicar** schema que 11/14 já emitem — leia os arquivos antes de editar.
253
+ - `<script type="application/ld+json">` sempre via `JSON.stringify()` (XSS com interpolação
254
+ direta).
255
+ - `llms.txt` e `robots.ts` devem funcionar em modo mockup (catálogo de exemplo) sem quebrar
256
+ o build — rode `npm run build` ao final.
257
+ - Textos em português do Brasil, no tom de voz do briefing (sem travessão na copy).
@@ -0,0 +1,72 @@
1
+ import type { Metadata } from "next";
2
+ import Link from "next/link";
3
+ import { CaretRight } from "@phosphor-icons/react/dist/ssr";
4
+ import { getCatalog, getTopTags } from "@/lib/queries";
5
+ import { PAGE_SIZE } from "@/lib/catalog";
6
+ import { buildTagMap, mapCatalogItems } from "@/lib/catalog-map";
7
+ import { ProductGrid } from "@/components/catalog/product-grid";
8
+ import { Pager } from "@/components/catalog/pager";
9
+ import { EmptyState } from "@/components/empty-state";
10
+ import { SearchBox } from "@/components/search-box";
11
+ import { mockupOr } from "@/lib/mockup";
12
+ import { DataLayerReady } from "@/components/analytics/data-layer-ready";
13
+
14
+ export const metadata: Metadata = { title: "Busca", robots: { index: false } };
15
+
16
+ type SP = { q?: string; page?: string };
17
+
18
+ export default async function BuscaPage({ searchParams }: { searchParams: Promise<SP> }) {
19
+ const sp = await searchParams;
20
+ const q = (sp.q ?? "").trim();
21
+ const page = Math.max(1, parseInt(sp.page ?? "1", 10) || 1);
22
+ const offset = (page - 1) * PAGE_SIZE;
23
+
24
+ const [catalog, tags] = await Promise.all([
25
+ q
26
+ ? mockupOr(getCatalog({ first: PAGE_SIZE, offset, searchText: q }), ({ nodes: [], totalCount: 0 } as any), "busca/getCatalog")
27
+ : Promise.resolve({ nodes: [], totalCount: 0 } as any),
28
+ mockupOr(getTopTags(), [], "busca/getTopTags"),
29
+ ]);
30
+
31
+ const items = mapCatalogItems(catalog.nodes ?? [], buildTagMap(tags as any[]));
32
+ const total = catalog.totalCount ?? 0;
33
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
34
+ const hrefForPage = (p: number) => `/busca?q=${encodeURIComponent(q)}${p > 1 ? `&page=${p}` : ""}`;
35
+
36
+ return (
37
+ <div className="store-layout full-bleed bg-white text-[var(--store-ink)]">
38
+ <DataLayerReady pageType="search" />
39
+ <div className="mx-auto max-w-[1240px] px-4 py-7 sm:px-6">
40
+ {/* breadcrumb */}
41
+ <div className="flex items-center gap-2 text-[13px] font-medium text-[var(--store-muted)]">
42
+ <Link href="/" className="no-underline hover:text-[var(--store-ink)]">Início</Link>
43
+ <CaretRight className="text-[11px]" />
44
+ <span className="font-semibold text-[var(--store-ink)]">Busca</span>
45
+ </div>
46
+
47
+ <h1 className="font-display mt-4 text-[28px] font-extrabold leading-[1.12]">Buscar produtos</h1>
48
+ <SearchBox defaultValue={q} className="mt-4 max-w-xl" />
49
+
50
+ <div className="mt-7">
51
+ {!q ? (
52
+ <EmptyState title="Digite o que procura" description="Busque pelo nome do produto ou categoria." />
53
+ ) : items.length === 0 ? (
54
+ <EmptyState title={`Nada encontrado para “${q}”`} description="Tente outros termos ou explore o catálogo completo.">
55
+ <Link href="/produtos" className="font-display inline-flex h-11 items-center rounded-xl bg-[var(--store-primary,#18181B)] px-5 text-[14px] font-bold text-white no-underline transition-colors hover:bg-[var(--store-primary-dark,#09090B)]">
56
+ Ver todos os produtos
57
+ </Link>
58
+ </EmptyState>
59
+ ) : (
60
+ <>
61
+ <p className="mb-4 text-sm text-[var(--store-muted)]">
62
+ <b className="text-[var(--store-ink)]">{total}</b> resultado(s) para “{q}”
63
+ </p>
64
+ <ProductGrid items={items} />
65
+ <Pager page={page} totalPages={totalPages} hrefForPage={hrefForPage} />
66
+ </>
67
+ )}
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,57 @@
1
+ // Passo 2 do funil (/carrinho/oferta): o cliente distribui a quantidade escolhida entre os
2
+ // produtos, escolhe a frequência de envio (assinatura REAL da política da loja, se houver)
3
+ // e segue pro checkout. Tela split sem o chrome do site. Produtos e preços do catálogo.
4
+ import type { Metadata } from "next";
5
+ import { Suspense } from "react";
6
+ import { getCatalog, getTopTags, getShopData } from "@/lib/queries";
7
+ import { buildTagMap, mapCatalogItems } from "@/lib/catalog-map";
8
+ import { ProductPicker, type PickerSubscription } from "@/components/landing/product-picker";
9
+ import { mockupOr } from "@/lib/mockup";
10
+
11
+ export const revalidate = 300;
12
+ export const metadata: Metadata = { title: "Monte seu pedido", robots: { index: false } };
13
+
14
+ const isCombo = (s: string) => /kit|combo/i.test(s);
15
+
16
+ export default async function CarrinhoOfertaPage() {
17
+ const [catalog, tags, shop] = await Promise.all([
18
+ mockupOr(getCatalog({ first: 100 }), ({ nodes: [] as any[] }), "carrinho-oferta/getCatalog"),
19
+ mockupOr(getTopTags(), [], "carrinho-oferta/getTopTags"),
20
+ mockupOr(getShopData(), null, "carrinho-oferta/getShopData"),
21
+ ]);
22
+ const nodes = (catalog.nodes ?? []) as any[];
23
+ const items = mapCatalogItems(nodes, buildTagMap(tags as any[]));
24
+
25
+ // Mesma seleção da landing: combos → ofertas → destaques (o herói vem primeiro).
26
+ let list = items.filter((it) => it.categories.some(isCombo));
27
+ if (list.length < 2) {
28
+ const deals = items.filter((it) => it.oldPrice != null);
29
+ list = deals.length >= 2 ? deals : items.slice(0, 8);
30
+ }
31
+ list = list.slice(0, 8);
32
+
33
+ // Assinatura: política REAL da loja + produtos que permitem recorrência.
34
+ const recurrableIds = nodes
35
+ .map((n: any) => n.product ?? n)
36
+ .filter((p: any) => p?.recurrenceAllowed)
37
+ .map((p: any) => String(p.productId));
38
+ const policy = (shop as any)?.recurringOrdersPolicy;
39
+ const subscription: PickerSubscription | null =
40
+ policy?.enabled && (policy.allowedFrequencies?.length ?? 0) > 0 && recurrableIds.length > 0
41
+ ? {
42
+ percentOff: policy.pricingPolicy?.type === "PERCENTAGE_OFF" ? policy.pricingPolicy.value : null,
43
+ frequencies: (policy.allowedFrequencies ?? []).map((f: any) => ({ id: f._id, title: f.title })),
44
+ }
45
+ : null;
46
+
47
+ return (
48
+ <Suspense>
49
+ <ProductPicker
50
+ products={list}
51
+ shopName={(shop as any)?.name ?? "Nossa loja"}
52
+ subscription={subscription}
53
+ recurrableIds={recurrableIds}
54
+ />
55
+ </Suspense>
56
+ );
57
+ }
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import Image from "next/image";
6
+ import { useRouter } from "next/navigation";
7
+ import { Trash2, Tag, ArrowRight } from "@/lib/icons";
8
+ import { useCart } from "@/components/cart/cart-provider";
9
+ import { QuantityStepper } from "@/components/quantity-stepper";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Input } from "@/components/ui/input";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Separator } from "@/components/ui/separator";
14
+ import { EmptyState } from "@/components/empty-state";
15
+ import { formatBRL } from "@/lib/format";
16
+ import { goToCheckout } from "@/lib/checkout-nav";
17
+ import { trackViewCart, trackPageType } from "@/lib/analytics";
18
+
19
+ export default function CarrinhoPage() {
20
+ const router = useRouter();
21
+ const { cart, updateQty, remove, applyCoupon, removeCoupon, appliedCoupon, loading } = useCart();
22
+ const [code, setCode] = React.useState("");
23
+ const items = cart?.items ?? [];
24
+ const hasDiscount = !!appliedCoupon || (!!cart?.summary.discountTotal && cart.summary.discountTotal !== "R$0,00");
25
+
26
+ // dataLayerReady(cart) + view_cart — uma vez por carregamento com item dentro.
27
+ const trackedRef = React.useRef(false);
28
+ React.useEffect(() => {
29
+ if (trackedRef.current || !cart?.items?.length) return;
30
+ trackedRef.current = true;
31
+ const list = cart.items.filter((i) => !i.isBonus).map((i) => ({ id: i.productId, name: i.title, variant: i.variantTitle, price: i.unitPrice, quantity: i.quantity }));
32
+ trackPageType("cart", list);
33
+ trackViewCart(list, cart.summary.totalAmount);
34
+ }, [cart]);
35
+
36
+ if (!cart || items.length === 0) {
37
+ return (
38
+ <EmptyState title="Seu carrinho está vazio" description="Que tal explorar nossos produtos?">
39
+ <Button nativeButton={false} render={<Link href="/produtos" />}>Ver produtos</Button>
40
+ </EmptyState>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div>
46
+ <h1 className="mb-6 text-2xl font-bold">Seu carrinho</h1>
47
+ <div className="grid gap-8 lg:grid-cols-[1fr_340px]">
48
+ {/* Itens */}
49
+ <ul className="divide-y rounded-xl border">
50
+ {items.map((it) => (
51
+ <li key={it.id} className="flex gap-4 p-4">
52
+ <div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-md bg-muted">
53
+ {it.thumbnail ? (
54
+ <Image src={it.thumbnail} alt={it.title} fill sizes="80px" className="object-cover" />
55
+ ) : (
56
+ <div className="flex h-full items-center justify-center text-[10px] text-muted-foreground">sem foto</div>
57
+ )}
58
+ </div>
59
+ <div className="flex flex-1 flex-col">
60
+ <div className="flex items-start justify-between gap-3">
61
+ <div>
62
+ <p className="font-medium">{it.title}</p>
63
+ {it.variantTitle && <p className="text-sm text-muted-foreground">{it.variantTitle}</p>}
64
+ <div className="mt-1 flex flex-wrap gap-1.5">
65
+ {it.isRecurring && (
66
+ <Badge variant="outline">
67
+ Assinatura{cart.recurringFrequencyTitle ? ` · ${cart.recurringFrequencyTitle}` : ""}
68
+ </Badge>
69
+ )}
70
+ {it.isBonus && <Badge>🎁 Brinde</Badge>}
71
+ </div>
72
+ </div>
73
+ <div className="text-right">
74
+ <p className="font-medium tabular-nums">{it.isBonus ? "Grátis" : it.displayUnitPrice}</p>
75
+ {!it.isBonus && <p className="text-xs text-muted-foreground">cada</p>}
76
+ </div>
77
+ </div>
78
+ {!it.isBonus && (
79
+ <div className="mt-3 flex items-center justify-between gap-3">
80
+ <div className="flex items-center gap-2">
81
+ <QuantityStepper value={it.quantity} onChange={(q) => updateQty(it.id, q)} disabled={loading} />
82
+ <Button variant="ghost" size="sm" onClick={() => remove(it.id)} disabled={loading}>
83
+ <Trash2 /> Remover
84
+ </Button>
85
+ </div>
86
+ {it.quantity > 1 && (
87
+ <span className="text-sm font-medium tabular-nums">{formatBRL(it.unitPrice * it.quantity)}</span>
88
+ )}
89
+ </div>
90
+ )}
91
+ </div>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+
96
+ {/* Resumo */}
97
+ <aside className="h-fit rounded-xl border p-5">
98
+ <h2 className="mb-3 font-semibold">Resumo</h2>
99
+
100
+ {/* Cupom */}
101
+ <form
102
+ className="mb-4"
103
+ onSubmit={async (e) => {
104
+ e.preventDefault();
105
+ if (code.trim() && (await applyCoupon(code.trim()))) setCode("");
106
+ }}
107
+ >
108
+ {hasDiscount ? (
109
+ <div className="flex items-center justify-between rounded-md bg-muted px-3 py-2 text-sm">
110
+ <span className="flex items-center gap-1.5">
111
+ <Tag className="size-3.5" /> {appliedCoupon?.code ?? "Desconto aplicado"}
112
+ </span>
113
+ {appliedCoupon?.discountId && (
114
+ <button type="button" className="text-muted-foreground hover:text-destructive" onClick={() => removeCoupon()}>
115
+ remover
116
+ </button>
117
+ )}
118
+ </div>
119
+ ) : (
120
+ <div className="flex gap-2">
121
+ <Input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Cupom de desconto" aria-label="Cupom" />
122
+ <Button type="submit" variant="outline" disabled={loading || !code.trim()}>Aplicar</Button>
123
+ </div>
124
+ )}
125
+ </form>
126
+
127
+ <Separator className="my-3" />
128
+ <dl className="space-y-1.5 text-sm">
129
+ <Row label="Subtotal" value={cart.summary.itemTotal} />
130
+ {cart.summary.discountTotal && cart.summary.discountTotal !== "R$0,00" && (
131
+ <Row label="Desconto" value={`- ${cart.summary.discountTotal}`} accent />
132
+ )}
133
+ <Row label="Frete" value={cart.summary.shippingTotal ?? "calculado no checkout"} />
134
+ </dl>
135
+ <Separator className="my-3" />
136
+ <div className="flex items-center justify-between text-base font-semibold">
137
+ <span>Total</span>
138
+ <span>{cart.summary.total ?? "—"}</span>
139
+ </div>
140
+
141
+ <Button className="mt-4 w-full" size="lg" onClick={() => goToCheckout(router)}>
142
+ Finalizar compra <ArrowRight />
143
+ </Button>
144
+ <Button variant="ghost" className="mt-2 w-full" nativeButton={false} render={<Link href="/produtos" />}>
145
+ Continuar comprando
146
+ </Button>
147
+ </aside>
148
+ </div>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ function Row({ label, value, accent }: { label: string; value?: string; accent?: boolean }) {
154
+ return (
155
+ <div className="flex justify-between">
156
+ <dt className="text-muted-foreground">{label}</dt>
157
+ <dd className={`tabular-nums ${accent ? "font-medium text-[var(--store-primary)]" : ""}`}>{value ?? "—"}</dd>
158
+ </div>
159
+ );
160
+ }
@@ -0,0 +1,31 @@
1
+ // ═══ POR QUE ESTE LAYOUT EXISTE (não é organização de código) ═══
2
+ // Ele valida a existência da categoria e chama notFound() ANTES da página. Sem isso a
3
+ // rota devolvia HTTP 200 com a tela de 404 dentro — um soft-404, que faz o Google
4
+ // indexar página vazia, valida rota quebrada em qualquer checagem por status e
5
+ // contamina destino de 301 e canonical.
6
+ //
7
+ // A causa não é óbvia: o notFound() da page.tsx SEMPRE esteve lá e não adiantava. O
8
+ // loading.tsx deste segmento envolve a página num <Suspense>; o shell fica pronto na
9
+ // hora e é enviado com 200, e quando o notFound() acontece o status já foi. O layout
10
+ // renderiza ACIMA dessa fronteira, então aqui o 404 ainda pode ser emitido.
11
+ // Medido: mesma página, só trocando a presença do loading.tsx → 404 vira 200.
12
+ //
13
+ // getTopTags é React cache(): chamar aqui e na página não duplica requisição.
14
+ import { notFound } from "next/navigation";
15
+ import { getTopTags } from "@/lib/queries";
16
+ import { mockupOr } from "@/lib/mockup";
17
+
18
+ export default async function CategoriaLayout({
19
+ children,
20
+ params,
21
+ }: {
22
+ children: React.ReactNode;
23
+ params: Promise<{ tagSlug: string }>;
24
+ }) {
25
+ const { tagSlug } = await params;
26
+ // mockupOr: falha da API com credenciais RELANÇA — nunca vira 404 cacheado.
27
+ const tags = await mockupOr(getTopTags(), [], `categoria/${tagSlug}`);
28
+ const alvo = decodeURIComponent(tagSlug);
29
+ if (!(tags as { slug?: string }[]).some((t) => t.slug === alvo)) notFound();
30
+ return <>{children}</>;
31
+ }
@@ -0,0 +1,17 @@
1
+ import { CatalogGridSkeleton } from "@/components/catalog/grid-skeleton";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div>
6
+ <div className="mb-3 h-5 w-56 animate-pulse rounded bg-muted" />
7
+ <div className="mb-5 flex flex-wrap items-center justify-between gap-3">
8
+ <div className="space-y-2">
9
+ <div className="h-7 w-48 animate-pulse rounded bg-muted" />
10
+ <div className="h-4 w-24 animate-pulse rounded bg-muted" />
11
+ </div>
12
+ <div className="h-9 w-40 animate-pulse rounded-md bg-muted" />
13
+ </div>
14
+ <CatalogGridSkeleton count={8} />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,49 @@
1
+ import type { Metadata } from "next";
2
+ import { notFound } from "next/navigation";
3
+ import { getCatalog, getTopTags } from "@/lib/queries";
4
+ import { buildTagMap, buildCategories, mapCatalogItems } from "@/lib/catalog-map";
5
+ import { CatalogClient } from "@/components/catalog/catalog-client";
6
+ import { mockupOr } from "@/lib/mockup";
7
+ import { DataLayerReady } from "@/components/analytics/data-layer-ready";
8
+
9
+ export const revalidate = 300;
10
+
11
+ async function findTag(slug: string, tags: any[]) {
12
+ const decoded = decodeURIComponent(slug);
13
+ return (tags as any[]).find((t) => t.slug === decoded);
14
+ }
15
+
16
+ export async function generateMetadata({ params }: { params: Promise<{ tagSlug: string }> }): Promise<Metadata> {
17
+ const { tagSlug } = await params;
18
+ const tags = await mockupOr(getTopTags(), [], "categoria/getTopTags");
19
+ const tag = await findTag(tagSlug, tags as any[]);
20
+ if (!tag) return { title: "Categoria" };
21
+ return { title: tag.displayTitle || tag.name, description: tag.description ?? undefined, alternates: { canonical: `/categoria/${encodeURIComponent(tag.slug)}` } };
22
+ }
23
+
24
+ export default async function CategoryPage({ params }: { params: Promise<{ tagSlug: string }> }) {
25
+ const { tagSlug } = await params;
26
+ const [catalog, tags] = await Promise.all([
27
+ mockupOr(getCatalog({ first: 100 }), ({ nodes: [] as any[] }), "categoria/getCatalog"),
28
+ mockupOr(getTopTags(), [], "categoria/getTopTags"),
29
+ ]);
30
+ const tag = await findTag(tagSlug, tags as any[]);
31
+ if (!tag) notFound();
32
+
33
+ const tagMap = buildTagMap(tags as any[]);
34
+ const itens = mapCatalogItems(catalog.nodes ?? [], tagMap);
35
+ // EDITOR: a categoria REAPROVEITA o container "catalogo" de /produtos (mesma faixa de confiança, mesmo
36
+ // cabeçalho de kits, mesmo aviso de lista vazia) sem mandar na ordem dele: `layout={false}` faz o
37
+ // manifesto desta página sair com `semLayout`, e o painel explica que a ordem se edita em /produtos.
38
+ return (
39
+ <>
40
+ <DataLayerReady pageType="category" products={itens.slice(0, 12).map((i) => ({ id: i.productId, name: i.title, price: i.price }))} />
41
+ <CatalogClient
42
+ items={itens}
43
+ categories={buildCategories(tags as any[])}
44
+ initialCategory={tag.displayTitle || tag.name}
45
+ layout={false}
46
+ />
47
+ </>
48
+ );
49
+ }