@cimplify/cli 0.6.16 → 0.7.2

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 (138) hide show
  1. package/dist/{add-BBVSJ5ZJ.mjs → add-5NW7N5CJ.mjs} +1 -1
  2. package/dist/{chunk-GL2J7272.mjs → chunk-26ETGFW6.mjs} +1 -1
  3. package/dist/{chunk-F5EGUNQZ.mjs → chunk-CVQRL2VH.mjs} +81 -25
  4. package/dist/{chunk-KCKMGRII.mjs → chunk-ZVEQGQ2Z.mjs} +2 -2
  5. package/dist/dispatcher.mjs +9 -9
  6. package/dist/{doctor-T6NA3EMW.mjs → doctor-XLBDC6NO.mjs} +2 -2
  7. package/dist/{explain-22NV6OL6.mjs → explain-PKTL7ZR6.mjs} +4 -4
  8. package/dist/{introspect-QOW24PJF.mjs → introspect-FPBV3O4N.mjs} +2 -2
  9. package/dist/{list-JHUIIMC2.mjs → list-N7FKBQ3Y.mjs} +1 -1
  10. package/dist/{update-IOMLDQEN.mjs → update-ZC3JGKLI.mjs} +1 -1
  11. package/package.json +2 -2
  12. package/templates/storefront-auto/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  13. package/templates/storefront-auto/CLAUDE.md +3 -2
  14. package/templates/storefront-auto/app/categories/[slug]/page.tsx +39 -17
  15. package/templates/storefront-auto/app/collections/[slug]/page.tsx +39 -17
  16. package/templates/storefront-auto/app/llms.txt/route.ts +13 -9
  17. package/templates/storefront-auto/app/page.tsx +3 -6
  18. package/templates/storefront-auto/app/products/[slug]/page.tsx +51 -21
  19. package/templates/storefront-auto/app/shop/page.tsx +10 -8
  20. package/templates/storefront-auto/app/shop/shop-client.tsx +1 -1
  21. package/templates/storefront-auto/app/sitemap-page/page.tsx +13 -9
  22. package/templates/storefront-auto/bun.lock +2 -2
  23. package/templates/storefront-auto/components/footer.tsx +0 -1
  24. package/templates/storefront-auto/lib/site.config.ts +1 -1
  25. package/templates/storefront-auto/next.config.ts +6 -5
  26. package/templates/storefront-auto/package.json +1 -1
  27. package/templates/storefront-bakery/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  28. package/templates/storefront-bakery/AGENTS.md +4 -3
  29. package/templates/storefront-bakery/CLAUDE.md +3 -2
  30. package/templates/storefront-bakery/app/categories/[slug]/page.tsx +39 -17
  31. package/templates/storefront-bakery/app/collections/[slug]/page.tsx +39 -17
  32. package/templates/storefront-bakery/app/llms.txt/route.ts +13 -9
  33. package/templates/storefront-bakery/app/page.tsx +19 -9
  34. package/templates/storefront-bakery/app/shop/page.tsx +10 -8
  35. package/templates/storefront-bakery/app/shop/shop-client.tsx +1 -1
  36. package/templates/storefront-bakery/app/sitemap-page/page.tsx +13 -9
  37. package/templates/storefront-bakery/bun.lock +2 -2
  38. package/templates/storefront-bakery/components/footer.tsx +0 -1
  39. package/templates/storefront-bakery/lib/site.config.ts +1 -1
  40. package/templates/storefront-bakery/next.config.ts +6 -5
  41. package/templates/storefront-bakery/package.json +1 -1
  42. package/templates/storefront-fashion/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  43. package/templates/storefront-fashion/CLAUDE.md +3 -2
  44. package/templates/storefront-fashion/app/categories/[slug]/page.tsx +39 -17
  45. package/templates/storefront-fashion/app/collections/[slug]/page.tsx +39 -17
  46. package/templates/storefront-fashion/app/llms.txt/route.ts +13 -9
  47. package/templates/storefront-fashion/app/page.tsx +23 -10
  48. package/templates/storefront-fashion/app/products/[slug]/page.tsx +51 -21
  49. package/templates/storefront-fashion/app/shop/page.tsx +10 -8
  50. package/templates/storefront-fashion/app/shop/shop-client.tsx +1 -1
  51. package/templates/storefront-fashion/app/sitemap-page/page.tsx +13 -9
  52. package/templates/storefront-fashion/bun.lock +2 -2
  53. package/templates/storefront-fashion/components/footer.tsx +0 -1
  54. package/templates/storefront-fashion/lib/site.config.ts +1 -1
  55. package/templates/storefront-fashion/next.config.ts +6 -5
  56. package/templates/storefront-fashion/package.json +1 -1
  57. package/templates/storefront-grocery/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  58. package/templates/storefront-grocery/CLAUDE.md +3 -2
  59. package/templates/storefront-grocery/app/categories/[slug]/page.tsx +39 -17
  60. package/templates/storefront-grocery/app/collections/[slug]/page.tsx +39 -17
  61. package/templates/storefront-grocery/app/llms.txt/route.ts +13 -9
  62. package/templates/storefront-grocery/app/page.tsx +19 -9
  63. package/templates/storefront-grocery/app/shop/page.tsx +10 -8
  64. package/templates/storefront-grocery/app/shop/shop-client.tsx +1 -1
  65. package/templates/storefront-grocery/app/sitemap-page/page.tsx +13 -9
  66. package/templates/storefront-grocery/bun.lock +2 -2
  67. package/templates/storefront-grocery/components/footer.tsx +0 -1
  68. package/templates/storefront-grocery/lib/site.config.ts +1 -1
  69. package/templates/storefront-grocery/next.config.ts +6 -5
  70. package/templates/storefront-grocery/package.json +1 -1
  71. package/templates/storefront-pharmacy/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  72. package/templates/storefront-pharmacy/CLAUDE.md +3 -2
  73. package/templates/storefront-pharmacy/app/categories/[slug]/page.tsx +39 -17
  74. package/templates/storefront-pharmacy/app/collections/[slug]/page.tsx +39 -17
  75. package/templates/storefront-pharmacy/app/llms.txt/route.ts +13 -9
  76. package/templates/storefront-pharmacy/app/page.tsx +3 -6
  77. package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +51 -21
  78. package/templates/storefront-pharmacy/app/shop/page.tsx +10 -8
  79. package/templates/storefront-pharmacy/app/shop/shop-client.tsx +1 -1
  80. package/templates/storefront-pharmacy/app/sitemap-page/page.tsx +13 -9
  81. package/templates/storefront-pharmacy/bun.lock +2 -2
  82. package/templates/storefront-pharmacy/components/footer.tsx +0 -1
  83. package/templates/storefront-pharmacy/lib/site.config.ts +1 -1
  84. package/templates/storefront-pharmacy/next.config.ts +6 -5
  85. package/templates/storefront-pharmacy/package.json +1 -1
  86. package/templates/storefront-restaurant/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  87. package/templates/storefront-restaurant/CLAUDE.md +3 -2
  88. package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +39 -17
  89. package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +39 -17
  90. package/templates/storefront-restaurant/app/llms.txt/route.ts +13 -9
  91. package/templates/storefront-restaurant/app/page.tsx +19 -9
  92. package/templates/storefront-restaurant/app/reservations/page.tsx +7 -7
  93. package/templates/storefront-restaurant/app/shop/page.tsx +10 -8
  94. package/templates/storefront-restaurant/app/shop/shop-client.tsx +1 -1
  95. package/templates/storefront-restaurant/app/sitemap-page/page.tsx +13 -9
  96. package/templates/storefront-restaurant/bun.lock +2 -2
  97. package/templates/storefront-restaurant/components/footer.tsx +0 -1
  98. package/templates/storefront-restaurant/lib/site.config.ts +1 -1
  99. package/templates/storefront-restaurant/next.config.ts +6 -5
  100. package/templates/storefront-restaurant/package.json +1 -1
  101. package/templates/storefront-retail/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  102. package/templates/storefront-retail/CLAUDE.md +3 -2
  103. package/templates/storefront-retail/app/categories/[slug]/page.tsx +39 -17
  104. package/templates/storefront-retail/app/collections/[slug]/page.tsx +39 -17
  105. package/templates/storefront-retail/app/llms.txt/route.ts +13 -9
  106. package/templates/storefront-retail/app/page.tsx +23 -10
  107. package/templates/storefront-retail/app/products/[slug]/page.tsx +51 -21
  108. package/templates/storefront-retail/app/shop/page.tsx +10 -8
  109. package/templates/storefront-retail/app/shop/shop-client.tsx +1 -1
  110. package/templates/storefront-retail/app/sitemap-page/page.tsx +13 -9
  111. package/templates/storefront-retail/bun.lock +2 -2
  112. package/templates/storefront-retail/components/footer.tsx +0 -1
  113. package/templates/storefront-retail/lib/site.config.ts +1 -1
  114. package/templates/storefront-retail/next.config.ts +6 -5
  115. package/templates/storefront-retail/package.json +1 -1
  116. package/templates/storefront-services/.claude/skills/cimplify-storefront/SKILL.md +34 -18
  117. package/templates/storefront-services/CLAUDE.md +3 -2
  118. package/templates/storefront-services/app/book/page.tsx +7 -7
  119. package/templates/storefront-services/app/categories/[slug]/page.tsx +39 -17
  120. package/templates/storefront-services/app/collections/[slug]/page.tsx +39 -17
  121. package/templates/storefront-services/app/llms.txt/route.ts +13 -9
  122. package/templates/storefront-services/app/page.tsx +19 -9
  123. package/templates/storefront-services/app/shop/page.tsx +10 -8
  124. package/templates/storefront-services/app/shop/shop-client.tsx +1 -1
  125. package/templates/storefront-services/app/sitemap-page/page.tsx +13 -9
  126. package/templates/storefront-services/bun.lock +2 -2
  127. package/templates/storefront-services/components/footer.tsx +0 -1
  128. package/templates/storefront-services/lib/site.config.ts +1 -1
  129. package/templates/storefront-services/next.config.ts +6 -5
  130. package/templates/storefront-services/package.json +1 -1
  131. package/templates/storefront-auto/app/api/revalidate/route.ts +0 -5
  132. package/templates/storefront-bakery/app/api/revalidate/route.ts +0 -5
  133. package/templates/storefront-fashion/app/api/revalidate/route.ts +0 -5
  134. package/templates/storefront-grocery/app/api/revalidate/route.ts +0 -5
  135. package/templates/storefront-pharmacy/app/api/revalidate/route.ts +0 -5
  136. package/templates/storefront-restaurant/app/api/revalidate/route.ts +0 -5
  137. package/templates/storefront-retail/app/api/revalidate/route.ts +0 -5
  138. package/templates/storefront-services/app/api/revalidate/route.ts +0 -5
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@cimplify/cli",
5
- version: "0.6.16",
5
+ version: "0.7.2",
6
6
  description: "Cimplify CLI \u2014 deploy, manage env vars, link projects, and scaffold storefronts",
7
7
  keywords: [
8
8
  "cimplify",
@@ -47,7 +47,7 @@ var package_default = {
47
47
  vitest: "^4.1.5"
48
48
  },
49
49
  dependencies: {
50
- "@cimplify/sdk": "^0.49.2"
50
+ "@cimplify/sdk": "^0.54.0"
51
51
  }
52
52
  };
53
53
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { TEMPLATES } from './chunk-F5EGUNQZ.mjs';
3
- import { package_default } from './chunk-KCKMGRII.mjs';
2
+ import { TEMPLATES } from './chunk-CVQRL2VH.mjs';
3
+ import { package_default } from './chunk-ZVEQGQ2Z.mjs';
4
4
 
5
5
  // src/dispatcher.ts
6
6
  var VERSION = package_default.version ?? "unknown";
@@ -138,16 +138,16 @@ var COMMANDS = {
138
138
  logs: () => import('./logs-YNN2PQ24.mjs'),
139
139
  status: () => import('./status-JSYXM5RT.mjs'),
140
140
  dev: () => import('./dev-ONW2S77K.mjs'),
141
- introspect: () => import('./introspect-QOW24PJF.mjs'),
141
+ introspect: () => import('./introspect-FPBV3O4N.mjs'),
142
142
  inspect: () => import('./inspect-CGYX4DDF.mjs'),
143
- doctor: () => import('./doctor-T6NA3EMW.mjs'),
144
- explain: () => import('./explain-22NV6OL6.mjs'),
143
+ doctor: () => import('./doctor-XLBDC6NO.mjs'),
144
+ explain: () => import('./explain-PKTL7ZR6.mjs'),
145
145
  assets: () => import('./assets-74SK63TR.mjs'),
146
146
  repo: () => import('./repo-KNQMSPVV.mjs'),
147
- list: () => import('./list-JHUIIMC2.mjs'),
148
- add: () => import('./add-BBVSJ5ZJ.mjs'),
149
- update: () => import('./update-IOMLDQEN.mjs'),
150
- upgrade: () => import('./update-IOMLDQEN.mjs'),
147
+ list: () => import('./list-N7FKBQ3Y.mjs'),
148
+ add: () => import('./add-5NW7N5CJ.mjs'),
149
+ update: () => import('./update-ZC3JGKLI.mjs'),
150
+ upgrade: () => import('./update-ZC3JGKLI.mjs'),
151
151
  "auth-step-up": () => import('./auth-step-up-BIUYQJP6.mjs')
152
152
  };
153
153
  var COMMAND_PREFIXES = {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { gatherIntrospection } from './chunk-GL2J7272.mjs';
2
+ import { gatherIntrospection } from './chunk-26ETGFW6.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-KCKMGRII.mjs';
5
+ import './chunk-ZVEQGQ2Z.mjs';
6
6
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
7
7
  import { ApiClient } from './chunk-MAOO6ZZ5.mjs';
8
8
  import { readAuthOrNull } from './chunk-UBAI443T.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-KCKMGRII.mjs';
2
+ import { package_default } from './chunk-ZVEQGQ2Z.mjs';
3
3
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
4
4
  import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -17,7 +17,7 @@ var TOPICS = [
17
17
  "title": "Customizing a template",
18
18
  "description": "When and how to eject SDK components into your project",
19
19
  "source_url": "https://cimplify.dev/docs/templates/customizing",
20
- "body": '## The customisation tiers\n\n \n \n\n| Tier | When | Cost |\n| --- | --- | --- |\n| Edit `brand.ts` | Copy, links, contact, hero, FAQ, policies | Free updates from the SDK forever |\n| Edit `globals.css @theme` | Palette, radius, fonts | Free updates from the SDK forever |\n| `classNames` overrides | Restyle a slot inside an SDK component | Free updates; no fork |\n| Eject a component | Restructure JSX, change behaviour, add merchant logic | You own the file; no auto-updates |\n| Extend `BrandSchema` | Industry-specific fields (lookbook, booking policy) | Type-checked, validated, schema co-evolves |\n\n \n\n \n\n## Edit content via `brand.ts`\n\n \n\nHardcoding a string in a page or component is the wrong move. Hoist it into the brand object first; everything that reads from `brand` stays consistent across pages, sitemap, and metadata.\n\n \n \n\n```tsx\n// \u274C wrong: hardcoded in a component\n<h1>Akua\'s Bakery</h1>\n\n// \u2705 right: sourced from the brand contract\n<h1>{brand.hero.title}</h1>\n```\n\n \n\n \n\n## Eject a component\n\n \n\nThe SDK ships ~67 ejectable components in its registry (cards, selectors, full pages, primitives). Eject when a `classNames` override can\'t reach the part you need to change, or when you\'re restructuring beyond what the component\'s props support.\n\n \n \n\n```bash\n# Browse the registry\ncimplify list\n\n# Eject one\ncimplify add cart-summary\n# \u2192 writes ./components/cart-summary.tsx, owned by you\n\n# Replace the SDK import in your page\n# import { CartSummary } from "@cimplify/sdk/react"\n# import { CartSummary } from "@/components/cart-summary"\n```\n\n \n \n\nOnce ejected, the file is yours. SDK upgrades won\'t touch it; you trade off auto-bugfixes for full control.\n\n \n \n\n**Don\'t reinvent the customizer.** Variant selection, add-on math, bundle pricing, and composite mode-switching took many iterations to get right. Eject `variant-selector`, `composite-selector`, `bundle-selector`, or `add-on-selector` and re-style; don\'t rewrite the cart payload contract. See `AGENTS.md` for the full doctrine.\n\n \n\n \n\n## Extend the brand schema\n\n \n\nIndustry-specific fields belong on the schema, not in scattered component props. Use `BrandSchema.extend` so your additions get the same boot-time validation as the canonical fields.\n\n \n \n\n```ts title="lib/schema.ts (services template)"\n\nexport const ServicesBrandSchema = BrandSchema.extend({\n bookingPolicy: z.object({\n cancellationWindowHours: z.number().int().positive(),\n depositPercent: z.number().int().min(0).max(100),\n rescheduleNoticeHours: z.number().int().positive(),\n }),\n});\n\nexport type ServicesBrand = z.infer<typeof ServicesBrandSchema>;\n```\n\n \n \n \n\n```ts title="lib/brand.ts"\n\nexport const brand: ServicesBrand = {\n // \u2026all the standard Brand fields\u2026\n bookingPolicy: {\n cancellationWindowHours: 24,\n depositPercent: 25,\n rescheduleNoticeHours: 12,\n },\n};\n```\n\n \n \n \n\n```ts title="__tests__/brand.test.ts: assert against the extended schema"\n\ndescribe("brand", () => {\n it("conforms to the services brand contract", () => {\n expect(ServicesBrandSchema.safeParse(brand).success).toBe(true);\n });\n});\n```\n\n \n\n \n\n## Add a new section\n\n \n\n1. Build the section as a Server Component in `components/`.\n2. Read merchant-specific copy from `brand`; extend the schema if the field isn\'t there.\n3. Wrap any client interactivity in a `*-client.tsx` island behind `<Suspense>` to keep the chrome cached.\n4. Compose into the page (`app/page.tsx`, `app/products/[slug]/page.tsx`, \u2026).\n5. Run `bun run check`.\n\n \n\n## Wire a Server Action\n\n \n \n\n```ts\n"use server";\n\nexport async function cancelMyOrder(orderId: string) {\n const r = await getServerClient().orders.cancel(orderId, "customer requested");\n if (!r.ok) return { ok: false as const, message: r.error.message };\n\n await revalidateOrders();\n return { ok: true as const };\n}\n```\n\n \n\n \n\n## Don\'ts\n\n \n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` to silence a warning. Wrap in `<Suspense>` instead.\n- Use `unstable_cache`. Next 16\'s canonical primitive is `\'use cache\'`.\n- Bypass `getServerClient()` by instantiating `createCimplifyClient` directly in a Server Component; you\'ll lose per-request memoisation.\n\n \n\n## Next\n\n \n \n- [**Brand schema**](/docs/templates/brand)\n Field reference and a full example\n\n \n- [**Testing**](/docs/testing)\n Catch ejections that broke the contract\n'
20
+ "body": '## The customisation tiers\n\n \n \n\n| Tier | When | Cost |\n| --- | --- | --- |\n| Edit `brand.ts` | Copy, links, contact, hero, FAQ, policies | Free updates from the SDK forever |\n| Edit `globals.css @theme` | Palette, radius, fonts | Free updates from the SDK forever |\n| `classNames` overrides | Restyle a slot inside an SDK component | Free updates; no fork |\n| Eject a component | Restructure JSX, change behaviour, add merchant logic | You own the file; no auto-updates |\n| Extend `BrandSchema` | Industry-specific fields (lookbook, booking policy) | Type-checked, validated, schema co-evolves |\n\n \n\n \n\n## Edit content via `brand.ts`\n\n \n\nHardcoding a string in a page or component is the wrong move. Hoist it into the brand object first; everything that reads from `brand` stays consistent across pages, sitemap, and metadata.\n\n \n \n\n```tsx\n// \u274C wrong: hardcoded in a component\n<h1>Akua\'s Bakery</h1>\n\n// \u2705 right: sourced from the brand contract\n<h1>{brand.hero.title}</h1>\n```\n\n \n\n \n\n## Eject a component\n\n \n\nThe SDK ships ~67 ejectable components in its registry (cards, selectors, full pages, primitives). Eject when a `classNames` override can\'t reach the part you need to change, or when you\'re restructuring beyond what the component\'s props support.\n\n \n \n\n```bash\n# Browse the registry\ncimplify list\n\n# Eject one\ncimplify add cart-summary\n# \u2192 writes ./components/cart-summary.tsx, owned by you\n\n# Replace the SDK import in your page\n# import { CartSummary } from "@cimplify/sdk/react"\n# import { CartSummary } from "@/components/cart-summary"\n```\n\n \n \n\nOnce ejected, the file is yours. SDK upgrades won\'t touch it; you trade off auto-bugfixes for full control.\n\n \n \n\n**Don\'t reinvent the customizer.** Variant selection, add-on math, bundle pricing, and composite mode-switching took many iterations to get right. Eject `variant-selector`, `composite-selector`, `bundle-selector`, or `add-on-selector` and re-style; don\'t rewrite the cart payload contract. See `AGENTS.md` for the full doctrine.\n\n \n\n \n\n## Extend the brand schema\n\n \n\nIndustry-specific fields belong on the schema, not in scattered component props. Use `BrandSchema.extend` so your additions get the same boot-time validation as the canonical fields.\n\n \n \n\n```ts title="lib/schema.ts (services template)"\n\nexport const ServicesBrandSchema = BrandSchema.extend({\n bookingPolicy: z.object({\n cancellationWindowHours: z.number().int().positive(),\n depositPercent: z.number().int().min(0).max(100),\n rescheduleNoticeHours: z.number().int().positive(),\n }),\n});\n\nexport type ServicesBrand = z.infer<typeof ServicesBrandSchema>;\n```\n\n \n \n \n\n```ts title="lib/brand.ts"\n\nexport const brand: ServicesBrand = {\n // \u2026all the standard Brand fields\u2026\n bookingPolicy: {\n cancellationWindowHours: 24,\n depositPercent: 25,\n rescheduleNoticeHours: 12,\n },\n};\n```\n\n \n \n \n\n```ts title="__tests__/brand.test.ts: assert against the extended schema"\n\ndescribe("brand", () => {\n it("conforms to the services brand contract", () => {\n expect(ServicesBrandSchema.safeParse(brand).success).toBe(true);\n });\n});\n```\n\n \n\n \n\n## Add a new section\n\n \n\n1. Build the section as a Server Component in `components/`.\n2. Read merchant-specific copy from `brand`; extend the schema if the field isn\'t there.\n3. Wrap any client interactivity in a `*-client.tsx` island behind `<Suspense>` to keep the chrome cached.\n4. Compose into the page (`app/page.tsx`, `app/products/[slug]/page.tsx`, \u2026).\n5. Run `bun run check`.\n\n \n\n## Wire a Server Action\n\n \n \n\n```ts\n"use server";\n\nexport async function cancelMyOrder(orderId: string) {\n const r = await getServerClient().orders.cancel(orderId, "customer requested");\n if (!r.ok) return { ok: false as const, message: r.error.message };\n\n await revalidateOrders();\n return { ok: true as const };\n}\n```\n\n \n\n \n\n## Don\'ts\n\n \n\n- Hardcode strings in pages or components.\n- Enable `cacheComponents: true` in `next.config.ts`. Cimplify storefronts run on Cloudflare Workers; Cache Components\' postponed-state serializer routinely exceeds the runtime\'s 128MB zlib limit and pages 5xx. Stay on the ISR Previous Model the templates ship with.\n- Use `\'use cache\'` / `cacheTag` / `cacheLife` directives. Same reason \u2014 they require Cache Components, which we don\'t enable. Use `export const revalidate` per page + `cacheOptions: { revalidate, tags }` on SDK reads.\n- Bypass `getServerClient()` by instantiating `createCimplifyClient` directly in a Server Component; you\'ll lose per-request memoisation.\n\n \n\n## Next\n\n \n \n- [**Brand schema**](/docs/templates/brand)\n Field reference and a full example\n\n \n- [**Testing**](/docs/testing)\n Catch ejections that broke the contract\n'
21
21
  },
22
22
  {
23
23
  "name": "products",
@@ -157,7 +157,7 @@ var TOPICS = [
157
157
  "title": "Revalidation",
158
158
  "description": "Canonical cimplify:* cache-tag vocabulary + revalidate helpers",
159
159
  "source_url": "https://cimplify.dev/docs/sdk/revalidation",
160
- "body": 'Cimplify storefronts cache catalogue reads with Next 16\'s `\'use cache\'` + `cacheTag`. When the merchant edits a product, collection, or brand asset, Cimplify POSTs to your storefront\'s `/api/revalidate` with the canonical tag set, and your handler calls `revalidateTag(tag)` for each.\n\nThis page is the canonical contract between Cimplify and your storefront. Use the typed helpers \u2014 never hand-write tag strings \u2014 so the scheme stays in one place.\n\n## Tags\n\nAll tags are namespaced under `cimplify:` so they can\'t collide with consumer tags. Build them via `tags` from `@cimplify/sdk/server`:\n\n```ts\n\n// In a cached server function:\nasync function getProducts() {\n "use cache";\n cacheTag(tags.products());\n cacheLife("hours");\n // \u2026\n}\n```\n\n### Catalogue\n\n| Helper | Tag |\n| --- | --- |\n| `tags.products()` | `cimplify:products` |\n| `tags.product(id)` | `cimplify:product:{id}` |\n| `tags.categories()` | `cimplify:categories` |\n| `tags.category(id)` | `cimplify:category:{id}` |\n| `tags.categoryProducts(id)` | `cimplify:category:{id}:products` |\n| `tags.collections()` | `cimplify:collections` |\n| `tags.collection(id)` | `cimplify:collection:{id}` |\n| `tags.collectionProducts(id)` | `cimplify:collection:{id}:products` |\n| `tags.tag(name)` | `cimplify:tag:{name}` (product tag, e.g. "vegan") |\n| `tags.addons()` | `cimplify:addons` |\n| `tags.addon(id)` | `cimplify:addon:{id}` |\n| `tags.stock()` | `cimplify:stock` |\n| `tags.stockFor(productId)` | `cimplify:stock:{productId}` |\n\n### Brand / business\n\n| Helper | Tag |\n| --- | --- |\n| `tags.business()` | `cimplify:business` |\n| `tags.brand()` | `cimplify:brand` |\n| `tags.locations()` | `cimplify:locations` |\n| `tags.location(id)` | `cimplify:location:{id}` |\n| `tags.locale()` | `cimplify:locale` |\n\n### Pricing / subscriptions\n\n| Helper | Tag |\n| --- | --- |\n| `tags.pricing()` | `cimplify:pricing` |\n| `tags.subscriptions()` | `cimplify:subscriptions` |\n| `tags.subscription(id)` | `cimplify:subscription:{id}` |\n\n### Customer-scoped (Server Actions only)\n\n| Helper | Tag |\n| --- | --- |\n| `tags.orders(customerId)` | `cimplify:orders:{customerId}` |\n| `tags.order(id)` | `cimplify:order:{id}` |\n\n## Granularity\n\nTag with **both** a broad and a precise tag on every read so either-flavour invalidation works:\n\n```ts\ncacheTag(tags.products(), tags.product(id));\n```\n\nThe broad tag (`products`) catches "I changed something product-related, invalidate everything." The precise tag (`product:{id}`) catches "I changed exactly this one product, invalidate only its entries."\n\n## Revalidating from a Server Action\n\n```ts\n\nexport async function saveProduct(id: string, data: ProductInput) {\n await client.catalogue.updateProduct(id, data);\n await revalidateProduct(id);\n}\n```\n\nHelpers:\n\n| Helper | Invalidates |\n| --- | --- |\n| `revalidateProducts()` | `products` |\n| `revalidateProduct(id)` | `product:{id}` + `products` |\n| `revalidateCategories()` | `categories` |\n| `revalidateCategory(id)` | `category:{id}` + `category:{id}:products` + `categories` |\n| `revalidateCollections()` | `collections` |\n| `revalidateCollection(id)` | `collection:{id}` + `collection:{id}:products` + `collections` |\n| `revalidateBusiness()` | `business` |\n| `revalidateBrand()` | `brand` |\n| `revalidateLocations()` | `locations` |\n| `revalidateLocation(id)` | `location:{id}` + `locations` |\n| `revalidatePricing()` | `pricing` + `products` |\n| `revalidateAddOns()` | `addons` |\n| `revalidateAddOn(id)` | `addon:{id}` + `addons` |\n| `revalidateSubscriptions()` | `subscriptions` |\n| `revalidateSubscription(id)` | `subscription:{id}` + `subscriptions` |\n| `revalidateStock(productId?)` | `stock:{productId}` + `stock` (or just `stock` if no id) |\n| `revalidateByTag(tag)` | escape hatch for a raw tag |\n\n## The Cimplify \u2192 storefront contract\n\nWhen a merchant edits something in the dashboard, Cimplify POSTs to your storefront\'s `/api/revalidate` with the matching tag set:\n\n```http\nPOST /api/revalidate\nx-cimplify-timestamp: 1716480000000\nx-cimplify-signature: sha256=<hex>\ncontent-type: application/json\n\n{ "tags": ["cimplify:products", "cimplify:product:p_abc123"] }\n```\n\nYour handler verifies the HMAC (shared secret in `CIMPLIFY_REVALIDATE_SECRET`), iterates `revalidateTag(tag)` for each, and returns 200. The route handler ships in every Cimplify storefront template \u2014 you don\'t write it.\n\nThe tags Cimplify sends use the **exact strings** from this table. If you\'ve tagged your caches with the helpers above, your storefront stays in sync automatically.\n\n## Dynamic routes \u2014 tag by **ID**, never by slug\n\nCimplify dispatches revalidation tags **keyed by database ID**, not by URL slug. A `/products/[slug]` page that caches itself under `tags.product(slug)` will never be invalidated by an edit \u2014 the tag the storefront wrote doesn\'t match the tag Cimplify fires.\n\nAlways resolve the slug to a record first, then tag with `record.id`. Next 16 lets you call `cacheTag` **after** an `await`, so this is one function, not two:\n\n```ts title="app/products/[slug]/page.tsx \u2014 correct"\n\nasync function getProduct(slug: string) {\n "use cache";\n cacheLife(CACHE_LIFE_DEFAULT);\n\n const r = await getServerClient().catalogue.getProductBySlug(slug);\n if (!r.ok) {\n // 404s tagged at the collection level so deleting + re-creating works.\n cacheTag(tags.products());\n return null;\n }\n\n // Tag with the resolved ID so revalidateProduct(id) from Cimplify hits.\n cacheTag(tags.product(r.value.id), tags.products());\n return r.value;\n}\n```\n\nThe same pattern applies to `[slug]` routes for categories, collections, addons, locations, and so on \u2014 always tag with the resolved `record.id`, never with the URL slug.\n\n### Why this matters\n\n- **Renames don\'t orphan.** The cache key is keyed on the slug (the function argument), so a renamed product gets a fresh cache entry under the new slug. The old slug-keyed entry is invalidated via the `tags.products()` collection tag and will 404 naturally.\n- **IDs are stable.** Cimplify\'s bus emits `ProductUpdated { id }` events; the slug isn\'t on the event payload, and adding it would create races with concurrent rename. Keeping the tag scheme ID-only keeps the contract simple.\n- **You get instant freshness.** With Cimplify\'s tag-cache + CDN-purge in place, a `CACHE_LIFE_DEFAULT` (`"max"`) entry can sit cached for days and still flip fresh within ~1s of an edit \u2014 but only if the tag actually matches.\n\n## `cacheLife` \u2014 use the SDK constants\n\nImport `CACHE_LIFE_DEFAULT` from `@cimplify/sdk/server` instead of passing the literal `"max"` to `cacheLife(...)`. Two reasons:\n\n- **Type-safe.** Next\'s `cacheLife(profile: string)` overload accepts any string \u2014 `cacheLife("maxx")` is silently treated as a custom-profile lookup at runtime. The SDK export is `"max" as const`, so a typo fails at the import site.\n- **Cimplify can change the recommendation later** without sweeping every storefront. Bump the SDK; templates pick up the new default on next deploy.\n\n| Use case | Constant | Resolves to |\n| --- | --- | --- |\n| Cached read with real data | `CACHE_LIFE_DEFAULT` | `"max"` (stale 5min, revalidate 30d, never expires) |\n| Cached read returned empty/error \u2014 **don\'t lock it in** | `CACHE_LIFE_PROBE` | `"seconds"` (stale 30s, revalidate 1s, expires 1min) |\n\nAnything Cimplify owns invalidates on-demand within ~1s, so short timer-based profiles aren\'t needed for Cimplify data. Override only when caching third-party APIs Cimplify can\'t see (a weather widget, an external CMS) \u2014 for those, pick a timer-based profile yourself.\n\n## Don\'t cache emptiness\n\nThe combination of long `cacheLife` + on-demand-only invalidation creates a real failure mode: **if a render produces empty results because of a transient backend hiccup, that empty render gets cached for the full `CACHE_LIFE_DEFAULT` window.** No timer expires it; no bus event invalidates it (because nothing actually changed in the database). Every visitor sees an empty storefront until someone manually triggers a revalidation.\n\nThe fix is a two-line guard in every cached fetch \u2014 branch on whether the result has real data, and only commit to `CACHE_LIFE_DEFAULT` when it does:\n\n```ts title="app/shop/page.tsx \u2014 listings"\n CACHE_LIFE_DEFAULT,\n CACHE_LIFE_PROBE,\n getServerClient,\n tags,\n} from "@cimplify/sdk/server";\n\nasync function getProducts() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n\n const r = await getServerClient().catalogue.getProducts();\n // Empty or failed? Don\'t lock the empty render in for "max" \u2014 a brief\n // backend blip would otherwise sit cached for the full window.\n if (!r.ok || r.value.items.length === 0) {\n cacheLife(CACHE_LIFE_PROBE);\n return r;\n }\n cacheLife(CACHE_LIFE_DEFAULT);\n return r;\n}\n```\n\nThe tag still gets attached either way, so a real `revalidateTag(...)` from Cimplify still drops the entry the moment the underlying data changes. The probe profile just means: if the merchant doesn\'t trigger an invalidation, the cache self-heals within a minute the next time someone visits.\n\nApply this pattern to **every** `\'use cache\'` helper that hits Cimplify \u2014 detail pages, listings, brand reads, location lookups. It\'s cheap and makes the cache impossible to poison with a transient empty.\n\n## Tag taxonomy is locked\n\nThe vocabulary above is the v1 contract. Adding new tag helpers is non-breaking; removing or renaming any of them bumps the SDK major version. Agents that bake these strings into prompts can pin to v0.x of `@cimplify/sdk`.\n'
160
+ "body": "Cimplify storefronts cache catalogue reads with Next 16's ISR (`export const revalidate = N` + `next.tags` via the SDK's `cacheOptions`). When a merchant edits a product, collection, or brand asset, the Cimplify eviction pipeline directly purges the matching R2 entries on your storefront's worker and the corresponding URLs from the Cloudflare edge \u2014 no `/api/revalidate` webhook round-trip required.\n\nThis page is the canonical contract between Cimplify and your storefront. Use the typed helpers \u2014 never hand-write tag strings \u2014 so the scheme stays in one place.\n\n## Why ISR, not Cache Components\n\nCimplify storefronts deploy to Cloudflare Workers (via opennext-cloudflare). Next 16's Cache Components feature (the `'use cache'` directive) requires Node-specific event-loop atomicity and serializes a postponed state that routinely exceeds CF Workers' 128MB zlib limit \u2014 pages 5xx with `RangeError: maxOutputLength`. We use Next 16's [Previous Model](https://nextjs.org/docs/app/guides/caching-without-cache-components): `export const revalidate` at the page level, `cacheOptions: { revalidate, tags }` on SDK reads (forwarded as `fetch().next.{revalidate,tags}`), and `revalidateTag` for invalidation. Stable on every runtime; what every Cimplify template ships with. Keep `cacheComponents` **off** in `next.config.ts`.\n\n## Tags\n\nAll tags are namespaced under `cimplify:` so they can't collide with consumer tags. Build them via `tags` from `@cimplify/sdk/server`:\n\n```ts\n\nexport const revalidate = 3600;\n\nasync function getProducts() {\n const r = await getServerClient().catalogue.getProducts(\n { limit: 24 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n return r.ok ? r.value.items : [];\n}\n```\n\n### Catalogue\n\n| Helper | Tag |\n| --- | --- |\n| `tags.products()` | `cimplify:products` |\n| `tags.product(id)` | `cimplify:product:{id}` |\n| `tags.categories()` | `cimplify:categories` |\n| `tags.category(id)` | `cimplify:category:{id}` |\n| `tags.categoryProducts(id)` | `cimplify:category:{id}:products` |\n| `tags.collections()` | `cimplify:collections` |\n| `tags.collection(id)` | `cimplify:collection:{id}` |\n| `tags.collectionProducts(id)` | `cimplify:collection:{id}:products` |\n| `tags.tag(name)` | `cimplify:tag:{name}` (product tag, e.g. \"vegan\") |\n| `tags.addons()` | `cimplify:addons` |\n| `tags.addon(id)` | `cimplify:addon:{id}` |\n| `tags.stock()` | `cimplify:stock` |\n| `tags.stockFor(productId)` | `cimplify:stock:{productId}` |\n\n### Brand / business\n\n| Helper | Tag |\n| --- | --- |\n| `tags.business()` | `cimplify:business` |\n| `tags.brand()` | `cimplify:brand` |\n| `tags.locations()` | `cimplify:locations` |\n| `tags.location(id)` | `cimplify:location:{id}` |\n| `tags.locale()` | `cimplify:locale` |\n\n### Pricing / subscriptions\n\n| Helper | Tag |\n| --- | --- |\n| `tags.pricing()` | `cimplify:pricing` |\n| `tags.subscriptions()` | `cimplify:subscriptions` |\n| `tags.subscription(id)` | `cimplify:subscription:{id}` |\n\n### Customer-scoped (Server Actions only)\n\n| Helper | Tag |\n| --- | --- |\n| `tags.orders(customerId)` | `cimplify:orders:{customerId}` |\n| `tags.order(id)` | `cimplify:order:{id}` |\n\n## Granularity\n\nTag with **both** a broad and a precise tag on every read so either-flavour invalidation works:\n\n```ts\ncacheOptions: { revalidate: 3600, tags: [tags.products(), tags.product(id)] }\n```\n\nThe broad tag (`products`) catches \"I changed something product-related, invalidate everything.\" The precise tag (`product:{id}`) catches \"I changed exactly this one product, invalidate only its entries.\"\n\n## Revalidating from a Server Action\n\n```ts\n\nexport async function saveProduct(id: string, data: ProductInput) {\n await client.catalogue.updateProduct(id, data);\n await revalidateProduct(id);\n}\n```\n\nHelpers:\n\n| Helper | Invalidates |\n| --- | --- |\n| `revalidateProducts()` | `products` |\n| `revalidateProduct(id)` | `product:{id}` + `products` |\n| `revalidateCategories()` | `categories` |\n| `revalidateCategory(id)` | `category:{id}` + `category:{id}:products` + `categories` |\n| `revalidateCollections()` | `collections` |\n| `revalidateCollection(id)` | `collection:{id}` + `collection:{id}:products` + `collections` |\n| `revalidateBusiness()` | `business` |\n| `revalidateBrand()` | `brand` |\n| `revalidateLocations()` | `locations` |\n| `revalidateLocation(id)` | `location:{id}` + `locations` |\n| `revalidatePricing()` | `pricing` + `products` |\n| `revalidateAddOns()` | `addons` |\n| `revalidateAddOn(id)` | `addon:{id}` + `addons` |\n| `revalidateSubscriptions()` | `subscriptions` |\n| `revalidateSubscription(id)` | `subscription:{id}` + `subscriptions` |\n| `revalidateStock(productId?)` | `stock:{productId}` + `stock` (or just `stock` if no id) |\n| `revalidateByTag(tag)` | escape hatch for a raw tag |\n\n## The Cimplify \u2192 storefront eviction contract\n\nWhen a merchant edits something in the dashboard, Cimplify's bus emits an event (`ProductUpdated`, `CategoryChanged`, etc.). The Rust EvictionDispatcher resolves which storefronts serve the affected business, derives the resource tags, and HMAC-POSTs to a central tag-cache worker on `*.workers.dev`. That worker:\n\n1. Looks up the matching cache entries in a `PathIndex` Durable Object (populated as your storefront writes ISR caches)\n2. Deletes the R2 entries directly so opennext can't serve from them\n3. Calls Cloudflare's `cache_purge` API for the corresponding URLs on the merchant's hostname\n4. Marks the tags stale in the shared `DOShardedTagCache` so any in-flight read sees the invalidation\n\nNet effect: an edit in the dashboard purges your storefront's caches **globally** in ~1\u20133 seconds, with no webhook round-trip into your worker. The R2 + edge purge happens entirely on Cimplify-owned infrastructure that has bindings to your storefront's R2 bucket and to the CF zone.\n\nThe tags Cimplify dispatches use the **exact strings** from the tables above. If you've tagged your `cacheOptions.tags` with the helpers above, your storefront stays in sync automatically.\n\n## Dynamic routes \u2014 tag by **ID**, never by slug\n\nCimplify dispatches revalidation tags **keyed by database ID**, not by URL slug. A `/products/[slug]` page that caches itself under `tags.product(slug)` will never be invalidated by an edit \u2014 the tag the storefront wrote doesn't match the tag Cimplify fires.\n\nResolve the slug to a record first, then tag with `record.id`:\n\n```ts title=\"app/products/[slug]/page.tsx \u2014 correct\"\n\nexport const revalidate = 3600;\n\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) return [{ slug: \"__placeholder__\" }];\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n const { slug } = await params;\n\n // First resolve slug \u2192 product. Tag the resolution read with the\n // collection-level tag so adds/removes invalidate it. This call hits the\n // SDK with a fallback-broad tag.\n const r = await getServerClient().catalogue.getProductBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) notFound();\n\n // Now re-fetch with the resolved ID so the per-product tag is keyed on the\n // stable identifier \u2014 revalidateProduct(id) from Cimplify will hit this\n // entry on the next mutation.\n const detailed = await getServerClient().catalogue.getProduct(\n r.value.id,\n { cacheOptions: { revalidate: 3600, tags: [tags.product(r.value.id), tags.products()] } },\n );\n\n if (!detailed.ok) notFound();\n return <ProductDetail product={detailed.value} />;\n}\n```\n\nThe same pattern applies to `[slug]` routes for categories, collections, addons, locations, and so on \u2014 always tag with the resolved `record.id`, never with the URL slug.\n\n### Why this matters\n\n- **Renames don't orphan.** The cache key includes the slug (path-based), so a renamed product gets a fresh cache entry under the new slug. The old slug-keyed entry is invalidated via the `tags.products()` collection tag and will 404 naturally.\n- **IDs are stable.** Cimplify's bus emits `ProductUpdated { id }` events; the slug isn't on the event payload, and adding it would create races with concurrent rename. Keeping the tag scheme ID-only keeps the contract simple.\n- **You get instant freshness.** With Cimplify's eviction pipeline (R2 delete + CDN purge), a 1-hour `revalidate` entry can sit cached and still flip fresh within ~3s of an edit \u2014 but only if the tag actually matches.\n\n## SDK timeout\n\nThe SDK's default per-call timeout is **5 seconds**. Picked so a single slow upstream call can't exhaust the 30s render budget \u2014 a hung `getCollections()` used to take the entire page render with it. With the 5s default, a slow call fails fast, `Result.ok` becomes `false`, the page renders with empty data, and opennext caches that partial render so subsequent hits are instant.\n\nOverride only for genuinely-large bulk reads (admin tooling, batch jobs):\n\n```ts\n\nconst adminClient = createCimplifyClient({\n secretKey: process.env.CIMPLIFY_SECRET_KEY,\n timeout: 30_000, // 30s for a 10k-row export\n});\n```\n\n## Don't cache empty responses as if they were real\n\nIf a render produces empty results because of a transient backend hiccup, that empty render gets cached for the full `revalidate` window \u2014 and every visitor sees an empty storefront until someone manually invalidates.\n\nTwo defences:\n\n**1.** The SDK's 5s timeout above stops a slow call from hanging the whole render, but the result is still `Result.ok = false`. Branch on that and short-circuit to a not-found / soft state rather than committing the empty render to cache:\n\n```ts title=\"app/categories/[slug]/page.tsx\"\nasync function getCategory(slug: string) {\n const r = await getServerClient().catalogue.getCategoryBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.categories()] } },\n );\n // Distinguish a real 404 from a transient SDK error. Only `notFound()` on\n // the real 404; for transient errors, render a soft loading state so the\n // cache doesn't lock in an incorrect \"page couldn't load\" for an hour.\n if (!r.ok) {\n if (r.error.code === \"NOT_FOUND\") notFound();\n return { soft: true as const };\n }\n return { soft: false as const, category: r.value };\n}\n```\n\n**2.** Use the eviction-pipeline path for catalogue invalidation rather than relying on `revalidate` window expiry. A real edit in the dashboard purges the cache within seconds; the `revalidate` value is a backstop, not the primary refresh mechanism.\n\n## Tag taxonomy is locked\n\nThe vocabulary above is the v1 contract. Adding new tag helpers is non-breaking; removing or renaming any of them bumps the SDK major version. Agents that bake these strings into prompts can pin to v0.x of `@cimplify/sdk`.\n"
161
161
  },
162
162
  {
163
163
  "name": "inspect-suggestions",
@@ -171,7 +171,7 @@ var TOPICS = [
171
171
  "title": "Agent prompts",
172
172
  "description": "Copy-paste prompts for common Cimplify workflows",
173
173
  "source_url": "https://cimplify.dev/docs/agent-prompts",
174
- "body": 'These prompts are deliberately terse. Each one names the canonical doc the agent should read first, the exact commands or [MCP tools](/docs/mcp) to call, and the exit condition. Replace `<angle-bracket>` placeholders before running.\n\nEvery prompt assumes the agent has either:\n\n- the [Cimplify CLI](/docs/cli) installed and `cimplify login` complete, **or**\n- an MCP connection to `https://api.cimplify.io/mcp` ([setup](/docs/mcp#connect))\n\n## Ship a new storefront end-to-end\n\n```text title="Scaffold \u2192 deploy \u2192 custom domain"\nSet up a Cimplify <retail> storefront called <my-store> and deploy it to <shop.example.com>.\n\nSource of truth: https://cimplify.dev/docs/tldr\n- Use the MCP tools at api.cimplify.io/mcp when available; otherwise shell `cimplify <cmd> --json`.\n- After every command, echo the JSON envelope.\n- Stop on the first non-zero exit code and name it from https://cimplify.dev/docs/cli#exit-codes.\n- Poll deployment status with cimplify_get_deployment_status (or `cimplify status --json`); never `sleep`.\n- Idempotent \u2014 re-running any step is safe.\n```\n\n## Rebrand an existing storefront\n\n```text title="Apply a new brand to a scaffolded template"\nRebrand the storefront in <./my-store> to match these tokens:\n name: <Acme Coffee>\n primary: <#0F172A>\n accent: <#F59E0B>\n fontDisplay: <"Playfair Display">\n fontBody: <"Inter">\n\nRules (https://cimplify.dev/docs/templates/brand):\n- Single source of truth is `lib/brand.ts`. Update it first.\n- Mirror color tokens into `app/globals.css` `@theme` block. Do not hard-code colors elsewhere.\n- Do not edit ejected SDK components; they pick up tokens automatically.\n- Run `bun run check:brand` after edits; fix any schema errors it reports.\n- Verify by opening `http://localhost:3000` (start `bun dev` if not running).\n- Commit with message `brand: <name>`. Do not deploy.\n```\n\n## Add a homepage section\n\n```text title="New Server Component section, cache-tagged"\nAdd a `<Stories>` section to the homepage of the storefront in <./my-store>.\n\nPattern (mirrors the Server Component sections under `app/_sections/` in the scaffolded template):\n- Create `app/_sections/stories.tsx` as an async Server Component with `"use cache"` and `cacheTag(tags.collections())` from `@cimplify/sdk/server`.\n- Source data from `client.catalogue.getCollection({ slug: "stories" })`.\n- Render with the existing `<SectionHeader>` and `<ProductCard>` SDK components (do not eject).\n- Insert into `app/page.tsx` between the hero and the featured collection.\n- Run `bun run check` \u2014 must pass typecheck, lint, brand, cart-flow, contract.\n- Do not modify `lib/brand.ts` or any file under `components/ui/`.\n```\n\n## Migrate to a new SDK version\n\n```text title="SDK upgrade with breaking-change scan"\nUpgrade the storefront in <./my-store> from `@cimplify/sdk@<0.44.x>` to `@cimplify/sdk@<latest>`.\n\nProcess:\n- Read the release notes for the target range (`npm view @cimplify/sdk versions --json` to list, then `npm view @cimplify/sdk@<version>` per release).\n- Bump the pin in `package.json`, run `bun install`.\n- For each breaking change called out in the changelog, grep the project for the old symbol and apply the documented replacement.\n- Run `bun run check`. If `check:contract` fails, the SDK \u2194 mock contract drifted \u2014 stop and report.\n- Do not run `cimplify deploy`. End by printing a summary of every file you changed and every breaking change you applied.\n```\n\n## Add a custom checkout payment method\n\n```text title="Wire an additional payment option"\nAdd <mobile_money> as a checkout payment option in the storefront at <./my-store>.\n\nReference: https://cimplify.dev/docs/api-reference/checkout (flat ProcessArgs body \u2014 top-level fields, not nested).\n- The body shape is the contract \u2014 never wrap fields.\n- Eject the existing `<PaymentMethodPicker>` component with `cimplify add payment-method-picker`.\n- Add the option in the ejected picker, wiring the provider-specific body (e.g. `mobile_money_details: { phone_number, provider }`).\n- Validate the body against the typed `ProcessArgs` from `@cimplify/sdk`.\n- Run `bun run check:cart` \u2014 the add\u2192checkout flow must still pass.\n```\n\n## Provision a CI deploy key\n\n```text title="One-time setup for headless deploys"\nCreate a CI-scoped API key for the Cimplify project linked at <./my-store>, and write the GitHub Actions snippet that uses it.\n\nSteps:\n- Confirm `.cimplify/project.json` exists; if not, fail with NOT_LINKED (exit 4).\n- Print the dashboard URL the human needs to visit to create the key (https://app.cimplify.io/settings/developer).\n- Once the user provides `dk_live_\u2026`, store it as a repo secret named `CIMPLIFY_API_KEY` (instruct, do not perform).\n- Write `.github/workflows/deploy.yml` that runs on push to main: install CLI via the curl one-liner, `cimplify deploy --prod --api-key "$CIMPLIFY_API_KEY" --yes --json`.\n- Print the exit-code legend from https://cimplify.dev/docs/cli#exit-codes so the workflow can map failures.\n```\n\n## Diagnose a deploy that didn\'t go live\n\n```text title="Triage a failed or stuck deployment"\nThe latest deploy of the project at <./my-store> isn\'t live. Find out why.\n\nTriage path:\n- `cimplify status --json` \u2014 what\'s the latest deployment_id and status?\n- If status is `failed`, `cimplify logs --deployment <id> --json | tail -200`. Quote the first error line you see.\n- If status is `queued` or `building` for >5 minutes, the build is stuck \u2014 try `cimplify rollback <previous-deploy-id>`.\n- If status is `superseded` (exit 2), a newer push raced it; usually fine \u2014 confirm with `cimplify status` again.\n- If domains misroute, `cimplify domains ls --json` \u2014 confirm the verified+attached domain.\n- Report: deployment_id, status, the failing log line (if any), and the recommended next action. Do not auto-rollback without confirming.\n```\n\n## Add a hero image and reference it in the homepage\n\n```text title="Upload brand assets \u2192 use them in source"\nAdd the image at <./Downloads/hero.jpg> as the homepage hero for the storefront in <./my-store>.\n\nReference: https://cimplify.dev/docs/cli/assets\n- `cp <./Downloads/hero.jpg> public/hero/main.jpg`\n- `cimplify assets upload public/hero/ --folder hero --json` \u2014 captures the manifest entry for `hero/main.jpg`.\n- Edit `app/page.tsx`: replace the hero `<Image>` `src` with `assetUrl("hero/main.jpg")` from `@cimplify/sdk`.\n- Do NOT touch existing `<Image>` calls that point at Cloudinary or other external hosts \u2014 the loader passes them through unchanged.\n- Run `bun run check`. Open `http://localhost:3000`; verify the hero image renders.\n- Commit `cimplify-assets.json` along with the source change so the manifest is reviewable.\n```\n\n## Reset a sandbox storefront to a clean state\n\n```text title="Wipe and re-seed for repeatable demos"\nReset the local mock state for the storefront in <./my-store>.\n\n- Stop any running `bun dev` process.\n- `cimplify mock reset` (clears the in-process mock backing store).\n- `cimplify mock seed <retail>` (re-seeds with the chosen industry fixture).\n- `bun dev` to restart both servers.\n- Verify: `curl localhost:8787/v1/catalogue/products | jq \'.data | length\'` returns the expected seed count (~24 for retail).\n- Do not touch the linked cloud project; this is local-only.\n```\n\n## How these are built\n\nEvery prompt above is structured the same way so you can adapt them:\n\n1. **Goal** \u2014 one sentence, with `<angle-bracket>` placeholders for the moving parts.\n2. **Source of truth** \u2014 the canonical doc URL the agent reads first.\n3. **Rules** \u2014 operational guardrails: which tools to call, what to echo, when to stop.\n4. **Exit condition** \u2014 what counts as success, what counts as a halt.\n\nIf you build a prompt that should live here, open a PR against `content/docs/agent-prompts.mdx`. Keep them under ~12 lines; if a workflow needs more, it belongs in a dedicated doc page and the prompt should link to it.\n'
174
+ "body": 'These prompts are deliberately terse. Each one names the canonical doc the agent should read first, the exact commands or [MCP tools](/docs/mcp) to call, and the exit condition. Replace `<angle-bracket>` placeholders before running.\n\nEvery prompt assumes the agent has either:\n\n- the [Cimplify CLI](/docs/cli) installed and `cimplify login` complete, **or**\n- an MCP connection to `https://api.cimplify.io/mcp` ([setup](/docs/mcp#connect))\n\n## Ship a new storefront end-to-end\n\n```text title="Scaffold \u2192 deploy \u2192 custom domain"\nSet up a Cimplify <retail> storefront called <my-store> and deploy it to <shop.example.com>.\n\nSource of truth: https://cimplify.dev/docs/tldr\n- Use the MCP tools at api.cimplify.io/mcp when available; otherwise shell `cimplify <cmd> --json`.\n- After every command, echo the JSON envelope.\n- Stop on the first non-zero exit code and name it from https://cimplify.dev/docs/cli#exit-codes.\n- Poll deployment status with cimplify_get_deployment_status (or `cimplify status --json`); never `sleep`.\n- Idempotent \u2014 re-running any step is safe.\n```\n\n## Rebrand an existing storefront\n\n```text title="Apply a new brand to a scaffolded template"\nRebrand the storefront in <./my-store> to match these tokens:\n name: <Acme Coffee>\n primary: <#0F172A>\n accent: <#F59E0B>\n fontDisplay: <"Playfair Display">\n fontBody: <"Inter">\n\nRules (https://cimplify.dev/docs/templates/brand):\n- Single source of truth is `lib/brand.ts`. Update it first.\n- Mirror color tokens into `app/globals.css` `@theme` block. Do not hard-code colors elsewhere.\n- Do not edit ejected SDK components; they pick up tokens automatically.\n- Run `bun run check:brand` after edits; fix any schema errors it reports.\n- Verify by opening `http://localhost:3000` (start `bun dev` if not running).\n- Commit with message `brand: <name>`. Do not deploy.\n```\n\n## Add a homepage section\n\n```text title="New Server Component section, cache-tagged"\nAdd a `<Stories>` section to the homepage of the storefront in <./my-store>.\n\nPattern (mirrors the Server Component sections under `app/_sections/` in the scaffolded template):\n- Create `app/_sections/stories.tsx` as an async Server Component. Pass `cacheOptions: { revalidate: 3600, tags: [tags.collections()] }` to the SDK read (from `@cimplify/sdk/server`). Do NOT use `\'use cache\'` / `cacheTag` / `cacheLife` \u2014 Cimplify storefronts run on CF Workers where Cache Components is broken; use the ISR Previous Model.\n- Source data from `client.catalogue.getCollection({ slug: "stories" })`.\n- Render with the existing `<SectionHeader>` and `<ProductCard>` SDK components (do not eject).\n- Insert into `app/page.tsx` between the hero and the featured collection.\n- Run `bun run check` \u2014 must pass typecheck, lint, brand, cart-flow, contract.\n- Do not modify `lib/brand.ts` or any file under `components/ui/`.\n```\n\n## Migrate to a new SDK version\n\n```text title="SDK upgrade with breaking-change scan"\nUpgrade the storefront in <./my-store> from `@cimplify/sdk@<0.44.x>` to `@cimplify/sdk@<latest>`.\n\nProcess:\n- Read the release notes for the target range (`npm view @cimplify/sdk versions --json` to list, then `npm view @cimplify/sdk@<version>` per release).\n- Bump the pin in `package.json`, run `bun install`.\n- For each breaking change called out in the changelog, grep the project for the old symbol and apply the documented replacement.\n- Run `bun run check`. If `check:contract` fails, the SDK \u2194 mock contract drifted \u2014 stop and report.\n- Do not run `cimplify deploy`. End by printing a summary of every file you changed and every breaking change you applied.\n```\n\n## Add a custom checkout payment method\n\n```text title="Wire an additional payment option"\nAdd <mobile_money> as a checkout payment option in the storefront at <./my-store>.\n\nReference: https://cimplify.dev/docs/api-reference/checkout (flat ProcessArgs body \u2014 top-level fields, not nested).\n- The body shape is the contract \u2014 never wrap fields.\n- Eject the existing `<PaymentMethodPicker>` component with `cimplify add payment-method-picker`.\n- Add the option in the ejected picker, wiring the provider-specific body (e.g. `mobile_money_details: { phone_number, provider }`).\n- Validate the body against the typed `ProcessArgs` from `@cimplify/sdk`.\n- Run `bun run check:cart` \u2014 the add\u2192checkout flow must still pass.\n```\n\n## Provision a CI deploy key\n\n```text title="One-time setup for headless deploys"\nCreate a CI-scoped API key for the Cimplify project linked at <./my-store>, and write the GitHub Actions snippet that uses it.\n\nSteps:\n- Confirm `.cimplify/project.json` exists; if not, fail with NOT_LINKED (exit 4).\n- Print the dashboard URL the human needs to visit to create the key (https://app.cimplify.io/settings/developer).\n- Once the user provides `dk_live_\u2026`, store it as a repo secret named `CIMPLIFY_API_KEY` (instruct, do not perform).\n- Write `.github/workflows/deploy.yml` that runs on push to main: install CLI via the curl one-liner, `cimplify deploy --prod --api-key "$CIMPLIFY_API_KEY" --yes --json`.\n- Print the exit-code legend from https://cimplify.dev/docs/cli#exit-codes so the workflow can map failures.\n```\n\n## Diagnose a deploy that didn\'t go live\n\n```text title="Triage a failed or stuck deployment"\nThe latest deploy of the project at <./my-store> isn\'t live. Find out why.\n\nTriage path:\n- `cimplify status --json` \u2014 what\'s the latest deployment_id and status?\n- If status is `failed`, `cimplify logs --deployment <id> --json | tail -200`. Quote the first error line you see.\n- If status is `queued` or `building` for >5 minutes, the build is stuck \u2014 try `cimplify rollback <previous-deploy-id>`.\n- If status is `superseded` (exit 2), a newer push raced it; usually fine \u2014 confirm with `cimplify status` again.\n- If domains misroute, `cimplify domains ls --json` \u2014 confirm the verified+attached domain.\n- Report: deployment_id, status, the failing log line (if any), and the recommended next action. Do not auto-rollback without confirming.\n```\n\n## Add a hero image and reference it in the homepage\n\n```text title="Upload brand assets \u2192 use them in source"\nAdd the image at <./Downloads/hero.jpg> as the homepage hero for the storefront in <./my-store>.\n\nReference: https://cimplify.dev/docs/cli/assets\n- `cp <./Downloads/hero.jpg> public/hero/main.jpg`\n- `cimplify assets upload public/hero/ --folder hero --json` \u2014 captures the manifest entry for `hero/main.jpg`.\n- Edit `app/page.tsx`: replace the hero `<Image>` `src` with `assetUrl("hero/main.jpg")` from `@cimplify/sdk`.\n- Do NOT touch existing `<Image>` calls that point at Cloudinary or other external hosts \u2014 the loader passes them through unchanged.\n- Run `bun run check`. Open `http://localhost:3000`; verify the hero image renders.\n- Commit `cimplify-assets.json` along with the source change so the manifest is reviewable.\n```\n\n## Reset a sandbox storefront to a clean state\n\n```text title="Wipe and re-seed for repeatable demos"\nReset the local mock state for the storefront in <./my-store>.\n\n- Stop any running `bun dev` process.\n- `cimplify mock reset` (clears the in-process mock backing store).\n- `cimplify mock seed <retail>` (re-seeds with the chosen industry fixture).\n- `bun dev` to restart both servers.\n- Verify: `curl localhost:8787/v1/catalogue/products | jq \'.data | length\'` returns the expected seed count (~24 for retail).\n- Do not touch the linked cloud project; this is local-only.\n```\n\n## How these are built\n\nEvery prompt above is structured the same way so you can adapt them:\n\n1. **Goal** \u2014 one sentence, with `<angle-bracket>` placeholders for the moving parts.\n2. **Source of truth** \u2014 the canonical doc URL the agent reads first.\n3. **Rules** \u2014 operational guardrails: which tools to call, what to echo, when to stop.\n4. **Exit condition** \u2014 what counts as success, what counts as a halt.\n\nIf you build a prompt that should live here, open a PR against `content/docs/agent-prompts.mdx`. Keep them under ~12 lines; if a workflow needs more, it belongs in a dedicated doc page and the prompt should link to it.\n'
175
175
  },
176
176
  {
177
177
  "name": "seeds",
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-GL2J7272.mjs';
2
+ export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-26ETGFW6.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-KCKMGRII.mjs';
5
+ import './chunk-ZVEQGQ2Z.mjs';
6
6
  import './chunk-C4M3DXKC.mjs';
7
7
  import './chunk-UBAI443T.mjs';
8
8
  import './chunk-E2T2SBP5.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { REGISTRY_INDEX } from './chunk-F5EGUNQZ.mjs';
2
+ import { REGISTRY_INDEX } from './chunk-CVQRL2VH.mjs';
3
3
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
4
4
  import { CliError, CLI_ERROR_CODE, info, bold, dim, green, result } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-KCKMGRII.mjs';
2
+ import { package_default } from './chunk-ZVEQGQ2Z.mjs';
3
3
  import { promptYesNo } from './chunk-ITAFAORS.mjs';
4
4
  import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
5
5
  import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/cli",
3
- "version": "0.6.16",
3
+ "version": "0.7.2",
4
4
  "description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -45,6 +45,6 @@
45
45
  "vitest": "^4.1.5"
46
46
  },
47
47
  "dependencies": {
48
- "@cimplify/sdk": "^0.49.2"
48
+ "@cimplify/sdk": "^0.54.0"
49
49
  }
50
50
  }
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  name: cimplify-storefront
3
- description: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.
3
+ description: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts`.
4
4
  ---
5
5
 
6
6
  # Cimplify Storefront skill
7
7
 
8
- You're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file ↔ brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.
8
+ You're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file ↔ brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all eight.
9
9
 
10
10
  ## The contract — never break
11
11
 
12
12
  1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface — don't hardcode it in a page or component.
13
13
  2. **`app/globals.css` `@theme { … }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.
14
- 3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife("hours")`. Don't downgrade them to `"use client"` to avoid an issue fix the issue.
15
- 4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.
16
- 5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.
14
+ 3. **Pages use ISR**, not Cache Components. Each page sets `export const revalidate = <seconds>` and reads from the SDK with `cacheOptions: { revalidate, tags }`. Don't add `'use cache'` / `cacheTag` / `cacheLife` — they require Node-specific runtime guarantees Cloudflare Workers doesn't provide, and their postponed state blows past CF's 128MB zlib limit.
15
+ 4. **`cacheComponents` stays OFF** in `next.config.ts`. The deploy target is Cloudflare Workers via opennext.
16
+ 5. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.
17
17
  6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.
18
18
  7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.
19
19
 
@@ -57,7 +57,7 @@ Don't touch any other file. If the rebrand needs content not in the schema, add
57
57
 
58
58
  1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).
59
59
  2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.
60
- 3. Wrap interactive bits in `<Suspense fallback={…}>` so the cached chrome streams.
60
+ 3. Wrap interactive bits in `<Suspense fallback={…}>` so static chrome streams first.
61
61
  4. Compose into the page.
62
62
 
63
63
  ### Wire a Server Action that mutates data
@@ -72,24 +72,39 @@ export async function createProduct(input: ProductInput) {
72
72
  }
73
73
  ```
74
74
 
75
- After every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.
75
+ After every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`. These fire eviction events the central tag-cache worker picks up — it drops R2 entries and purges Cloudflare's edge.
76
76
 
77
- ### Add a Server Component data fetch
77
+ ### Add a Server Component data fetch (ISR)
78
78
 
79
79
  ```ts
80
- import { cacheTag, cacheLife } from "next/cache";
81
80
  import { getServerClient, tags } from "@cimplify/sdk/server";
82
81
 
83
- async function getX() {
84
- "use cache";
85
- cacheTag(tags.products());
86
- cacheLife("hours");
87
- const r = await getServerClient().catalogue.getProducts({ limit: 24 });
88
- if (!r.ok) throw new Error(r.error.message);
82
+ export const revalidate = 3600; // page-level baseline
83
+
84
+ async function getProducts() {
85
+ const r = await getServerClient().catalogue.getProducts({
86
+ limit: 24,
87
+ cacheOptions: { revalidate: 3600, tags: [tags.products()] },
88
+ });
89
+ if (!r.ok) {
90
+ // Soft-render on transient errors so the page degrades gracefully
91
+ // instead of hard-failing with React #419. Only call notFound() on
92
+ // genuine 404 from origin.
93
+ if (r.error.code === "NOT_FOUND") notFound();
94
+ return [];
95
+ }
89
96
  return r.value.items;
90
97
  }
91
98
  ```
92
99
 
100
+ For `[slug]` routes, add `generateStaticParams` with a placeholder fallback so the page is statically prerenderable even with no upstream data yet:
101
+
102
+ ```ts
103
+ export async function generateStaticParams() {
104
+ return [{ slug: "__placeholder__" }];
105
+ }
106
+ ```
107
+
93
108
  ### Eject an SDK component for deeper customization
94
109
 
95
110
  Try `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:
@@ -116,10 +131,11 @@ cimplify domains add my-store.com
116
131
  ## Pitfalls — explicit ❌ list
117
132
 
118
133
  - ❌ Hardcoding any visible string in a page or component. Always `brand.X`.
119
- - ❌ Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.
120
- - ❌ Using `unstable_cache` — Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.
134
+ - ❌ Adding `'use cache'`, `cacheTag`, `cacheLife`, or enabling `cacheComponents: true`. We deploy to Cloudflare Workers; postponed state exceeds the 128MB zlib limit. Use `cacheOptions` on SDK reads + `export const revalidate` per page.
135
+ - ❌ Using `unstable_cache` — removed in Next 16. Use SDK `cacheOptions` instead.
121
136
  - ❌ Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component — loses per-request memoization.
122
137
  - ❌ Mutating data without calling the matching `revalidate*` helper.
138
+ - ❌ Calling `notFound()` on transient SDK errors — causes React #419 mid-stream. Check the `Result.error.code === "NOT_FOUND"` first; otherwise soft-render a skeleton.
123
139
  - ❌ Adding an `app/error.tsx` handler that calls `reset()` without logging — silently swallowed errors hide bugs.
124
140
  - ❌ Running `bun test` and reporting failures. Use `bun run test:run` (vitest).
125
141
  - ❌ Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) — they're real.
@@ -132,7 +148,7 @@ cimplify domains add my-store.com
132
148
  | Architectural rules | `AGENTS.md` at project root + this skill |
133
149
  | Running locally | `bun dev` (boots mock + Next together) |
134
150
  | Switching mock seed | edit `dev:mock` in `package.json` |
135
- | Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |
151
+ | Full SDK reference | `cimplify.dev/sdk/optimization`, `cimplify.dev/sdk/server` |
136
152
 
137
153
  ## What to do when the user asks something out-of-scope
138
154
 
@@ -17,6 +17,7 @@ That covers ~95% of what merchants ask for. For anything else, follow the `cimpl
17
17
  ## Don't do
18
18
 
19
19
  - Hardcode strings in pages or components.
20
- - Disable `cacheComponents: true` in `next.config.ts`.
21
- - Use `unstable_cache` Next 16's canonical primitive is `'use cache'`.
20
+ - Enable `cacheComponents: true` in `next.config.ts` — we're on Cloudflare Workers, where `'use cache'` postponed state blows past CF's 128MB zlib limit. This template stays on Next 16's "Previous Model" (ISR): `export const revalidate` per page + `cacheOptions: { revalidate, tags }` on SDK reads.
21
+ - Add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use the SDK's `cacheOptions` instead.
22
+ - Use `unstable_cache` — it's gone in Next 16. Use SDK `cacheOptions` or `fetch.next.{revalidate,tags}`.
22
23
  - Run `bun test` (Bun's `vi` shim is incomplete) — use `bun run test:run`.
@@ -2,9 +2,8 @@ import type { Metadata } from "next";
2
2
  import { Suspense } from "react";
3
3
  import Link from "next/link";
4
4
  import { notFound } from "next/navigation";
5
- import { cacheTag, cacheLife } from "next/cache";
6
5
  import {
7
- CACHE_LIFE_DEFAULT, getServerClient,
6
+ getServerClient,
8
7
  tags,
9
8
  type Category,
10
9
  type Product,
@@ -12,26 +11,46 @@ import {
12
11
  import { ListingClient } from "./listing-client";
13
12
  import { brand } from "@/lib/brand";
14
13
 
14
+ // See app/products/[slug]/page.tsx for the rationale on generateStaticParams.
15
+ export async function generateStaticParams() {
16
+ const r = await getServerClient().catalogue.getCategories();
17
+ if (!r.ok || r.value.length === 0) {
18
+ return [{ slug: "__placeholder__" }];
19
+ }
20
+ return r.value.map((c) => ({ slug: c.slug ?? c.id }));
21
+ }
22
+
23
+ export const revalidate = 3600;
24
+
15
25
  interface CategoryData {
16
26
  category: Category;
17
27
  products: Product[];
18
28
  }
19
29
 
20
- async function getCategory(slug: string): Promise<CategoryData | null> {
21
- "use cache";
22
- cacheTag(tags.categories());
23
- cacheLife(CACHE_LIFE_DEFAULT);
30
+ type CategoryResult =
31
+ | { ok: true; data: CategoryData }
32
+ | { ok: false; code: string };
24
33
 
34
+ async function getCategory(slug: string): Promise<CategoryResult> {
25
35
  const client = getServerClient();
26
- const catRes = await client.catalogue.getCategoryBySlug(slug);
27
- if (!catRes.ok) return null;
36
+ const catRes = await client.catalogue.getCategoryBySlug(slug, {
37
+ cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
38
+ });
39
+ if (!catRes.ok) return { ok: false, code: catRes.error.code };
28
40
 
29
- cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));
30
- const r = await client.catalogue.getCategoryProducts(catRes.value.id);
41
+ const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {
42
+ cacheOptions: {
43
+ revalidate: 3600,
44
+ tags: [
45
+ tags.category(catRes.value.id),
46
+ tags.categoryProducts(catRes.value.id),
47
+ ],
48
+ },
49
+ });
31
50
  const products = r.ok
32
51
  ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
33
52
  : [];
34
- return { category: catRes.value, products };
53
+ return { ok: true, data: { category: catRes.value, products } };
35
54
  }
36
55
 
37
56
  export async function generateMetadata({
@@ -40,8 +59,9 @@ export async function generateMetadata({
40
59
  params: Promise<{ slug: string }>;
41
60
  }): Promise<Metadata> {
42
61
  const { slug } = await params;
43
- const data = await getCategory(slug);
44
- if (!data) return {};
62
+ const result = await getCategory(slug);
63
+ if (!result.ok) return {};
64
+ const data = result.data;
45
65
  return {
46
66
  title: `${data.category.name} — ${brand.name}`,
47
67
  description: data.category.description ?? undefined,
@@ -66,10 +86,12 @@ async function CategoryContent({
66
86
  params: Promise<{ slug: string }>;
67
87
  }) {
68
88
  const { slug } = await params;
69
- const data = await getCategory(slug);
70
- if (!data) notFound();
71
-
72
- const { category, products } = data;
89
+ const result = await getCategory(slug);
90
+ if (!result.ok) {
91
+ if (result.code === "NOT_FOUND") notFound();
92
+ return <CategorySkeleton />;
93
+ }
94
+ const { category, products } = result.data;
73
95
  return (
74
96
  <>
75
97
  <section className="bg-foreground text-background relative overflow-hidden">
@@ -2,9 +2,8 @@ import type { Metadata } from "next";
2
2
  import { Suspense } from "react";
3
3
  import Link from "next/link";
4
4
  import { notFound } from "next/navigation";
5
- import { cacheTag, cacheLife } from "next/cache";
6
5
  import {
7
- CACHE_LIFE_DEFAULT, getServerClient,
6
+ getServerClient,
8
7
  tags,
9
8
  type Collection,
10
9
  type Product,
@@ -12,26 +11,46 @@ import {
12
11
  import { ListingClient } from "./listing-client";
13
12
  import { brand } from "@/lib/brand";
14
13
 
14
+ // See app/products/[slug]/page.tsx for the rationale on generateStaticParams.
15
+ export async function generateStaticParams() {
16
+ const r = await getServerClient().catalogue.getCollections();
17
+ if (!r.ok || r.value.length === 0) {
18
+ return [{ slug: "__placeholder__" }];
19
+ }
20
+ return r.value.map((c) => ({ slug: c.slug ?? c.id }));
21
+ }
22
+
23
+ export const revalidate = 3600;
24
+
15
25
  interface CollectionData {
16
26
  collection: Collection;
17
27
  products: Product[];
18
28
  }
19
29
 
20
- async function getCollection(slug: string): Promise<CollectionData | null> {
21
- "use cache";
22
- cacheTag(tags.collections());
23
- cacheLife(CACHE_LIFE_DEFAULT);
30
+ type CollectionResult =
31
+ | { ok: true; data: CollectionData }
32
+ | { ok: false; code: string };
24
33
 
34
+ async function getCollection(slug: string): Promise<CollectionResult> {
25
35
  const client = getServerClient();
26
- const colRes = await client.catalogue.getCollectionBySlug(slug);
27
- if (!colRes.ok) return null;
36
+ const colRes = await client.catalogue.getCollectionBySlug(slug, {
37
+ cacheOptions: { revalidate: 3600, tags: [tags.collections()] },
38
+ });
39
+ if (!colRes.ok) return { ok: false, code: colRes.error.code };
28
40
 
29
- cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));
30
- const r = await client.catalogue.getCollectionProducts(colRes.value.id);
41
+ const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {
42
+ cacheOptions: {
43
+ revalidate: 3600,
44
+ tags: [
45
+ tags.collection(colRes.value.id),
46
+ tags.collectionProducts(colRes.value.id),
47
+ ],
48
+ },
49
+ });
31
50
  const products = r.ok
32
51
  ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
33
52
  : [];
34
- return { collection: colRes.value, products };
53
+ return { ok: true, data: { collection: colRes.value, products } };
35
54
  }
36
55
 
37
56
  export async function generateMetadata({
@@ -40,8 +59,9 @@ export async function generateMetadata({
40
59
  params: Promise<{ slug: string }>;
41
60
  }): Promise<Metadata> {
42
61
  const { slug } = await params;
43
- const data = await getCollection(slug);
44
- if (!data) return {};
62
+ const result = await getCollection(slug);
63
+ if (!result.ok) return {};
64
+ const data = result.data;
45
65
  return {
46
66
  title: `${data.collection.name} — ${brand.name}`,
47
67
  description: data.collection.description ?? undefined,
@@ -66,10 +86,12 @@ async function CollectionContent({
66
86
  params: Promise<{ slug: string }>;
67
87
  }) {
68
88
  const { slug } = await params;
69
- const data = await getCollection(slug);
70
- if (!data) notFound();
71
-
72
- const { collection, products } = data;
89
+ const result = await getCollection(slug);
90
+ if (!result.ok) {
91
+ if (result.code === "NOT_FOUND") notFound();
92
+ return <CollectionSkeleton />;
93
+ }
94
+ const { collection, products } = result.data;
73
95
  return (
74
96
  <>
75
97
  <section className="bg-foreground text-background relative overflow-hidden">
@@ -1,18 +1,22 @@
1
- import { cacheTag, cacheLife } from "next/cache";
2
- import { CACHE_LIFE_DEFAULT, getServerClient, tags, type Product } from "@cimplify/sdk/server";
1
+ import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
3
2
  import { brand } from "@/lib/brand";
4
3
  import { getSiteUrl } from "@/lib/site-url";
5
4
 
6
- async function buildLlmsTxt(SITE_URL: string): Promise<string> {
7
- "use cache";
8
- cacheTag(tags.products(), tags.categories(), tags.collections());
9
- cacheLife(CACHE_LIFE_DEFAULT);
5
+ export const revalidate = 3600;
10
6
 
7
+ async function buildLlmsTxt(SITE_URL: string): Promise<string> {
11
8
  const client = getServerClient();
12
9
  const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
13
- client.catalogue.getProducts({ limit: 500 }),
14
- client.catalogue.getCategories(),
15
- client.catalogue.getCollections(),
10
+ client.catalogue.getProducts(
11
+ { limit: 500 },
12
+ { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },
13
+ ),
14
+ client.catalogue.getCategories({
15
+ cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
16
+ }),
17
+ client.catalogue.getCollections({
18
+ cacheOptions: { revalidate: 3600, tags: [tags.collections()] },
19
+ }),
16
20
  ]);
17
21
 
18
22
  const products: Product[] = productsRes.ok ? productsRes.value.items : [];
@@ -1,7 +1,6 @@
1
1
  import type { Metadata } from "next";
2
2
  import { Suspense } from "react";
3
- import { cacheTag, cacheLife } from "next/cache";
4
- import { CACHE_LIFE_DEFAULT, getServerClient, tags, type Product } from "@cimplify/sdk/server";
3
+ import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
5
4
  import { AutoHero } from "@/components/auto-hero";
6
5
  import { FitmentFinder } from "@/components/fitment-finder";
7
6
  import { TrustBar } from "@/components/trust-bar";
@@ -16,11 +15,9 @@ export const metadata: Metadata = {
16
15
  description: brand.description,
17
16
  };
18
17
 
19
- async function getHomeData() {
20
- "use cache";
21
- cacheTag(tags.products());
22
- cacheLife(CACHE_LIFE_DEFAULT);
18
+ export const revalidate = 3600;
23
19
 
20
+ async function getHomeData() {
24
21
  const client = getServerClient();
25
22
  const productsRes = await client.catalogue.getProducts({ limit: 12 });
26
23
  const allProducts: Product[] = productsRes.ok ? productsRes.value.items : [];