@decocms/start 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,1013 @@
1
+ ---
2
+ name: deco-tanstack-storefront-patterns
3
+ description: Runtime patterns, fixes, and learnings for Deco storefronts running on TanStack Start/React/Cloudflare Workers. Covers nested sections, dev cache control, workerEntry guards, section loaders, Fresh-to-React JSX porting, VTEX API resilience, node:async_hooks client bundle leak, SliderJS DOM timing, cart button loading state, server functions for VTEX actions, DOM manipulation conflicts with React, TypeScript patterns, loader cache/cacheKey module exports via createCachedLoaderFromModule, and the full multi-layer VTEX cache architecture. Also covers: setup.ts import order for server functions (client-nav 404 fix), globalThis backing for module-level state (split-module RPC problem), async rendering/deferred sections with IntersectionObserver, loadDeferredSection POST instead of GET (431 fix), section ordering with index stamping (mergeSections sort-based), layout section caching (5min TTL for Header/Footer), analytics hydration mismatch fix (useScript + suppressHydrationWarning), and vite.config.ts requirements for published @decocms/start packages (esbuildOptions jsx automatic + virtual module fallback + resolve.dedupe). Use when debugging or enhancing a deco-start storefront after the initial migration.
4
+ ---
5
+
6
+ # Deco TanStack Storefront Patterns
7
+
8
+ Patterns and fixes discovered while porting and running `espacosmart-storefront` on the `@decocms/start` + TanStack Start stack. These apply to **any** Deco site after the initial migration.
9
+
10
+ ## When to Use This Skill
11
+
12
+ - Debugging runtime errors in a deco-start storefront
13
+ - Porting sections that use nested sections (`{ Component, props }`)
14
+ - Configuring dev mode vs production cache behavior
15
+ - Fixing Cloudflare Worker / miniflare errors
16
+ - Making VTEX API calls resilient to 404s
17
+ - Finding and fixing remaining Fresh/Preact patterns in React components
18
+
19
+ ---
20
+
21
+ ## 1. Nested Sections (`{ Component, props }`)
22
+
23
+ ### Problem
24
+
25
+ In `deco-cx/deco` (Fresh), nested sections arrived as `{ Component: FunctionRef, props: {...} }`. In TanStack, the CMS engine resolves sections to `{ __resolveType: "site/sections/X.tsx", ...props }` — the `Component` property is a string key, not a function reference.
26
+
27
+ Using `<section.Component {...section.props} />` in React treats the string as an HTML tag name, causing: `Error: Invalid tag: site/sections/Product/BuyTogether.tsx`
28
+
29
+ ### Solution (two layers)
30
+
31
+ **Engine (`deco-start/src/cms/resolve.ts`)** — `normalizeNestedSections`:
32
+ After resolving top-level section props, recursively walks all props and converts any nested section (objects with `__resolveType` pointing to a registered section) from:
33
+ ```
34
+ { __resolveType: "site/sections/X.tsx", ...props }
35
+ ```
36
+ to:
37
+ ```
38
+ { Component: "site/sections/X.tsx", props: { ...normalizedProps } }
39
+ ```
40
+
41
+ This preserves the same `{ Component, props }` shape as Fresh.
42
+
43
+ **Renderer (`deco-start/src/hooks/DecoPageRenderer.tsx`)** — `SectionRenderer` / `SectionList`:
44
+ Components that accept nested sections import from `@decocms/start/hooks`:
45
+
46
+ ```typescript
47
+ import { SectionRenderer, SectionList, type Section } from "@decocms/start/hooks";
48
+
49
+ // Single nested section
50
+ <SectionRenderer section={children} />
51
+
52
+ // Array of nested sections
53
+ <SectionList sections={sectionChildrens} />
54
+ ```
55
+
56
+ `SectionRenderer` handles both:
57
+ - **String `Component`** → lazy lookup in section registry
58
+ - **Function `Component`** → direct render (legacy Fresh compat)
59
+
60
+ ### How to Find Affected Sections
61
+
62
+ ```bash
63
+ rg '<\w+\.Component' src/sections/ src/components/
64
+ ```
65
+
66
+ Any `<section.Component {...section.props} />` or `<children.Component {...children.props} />` must be replaced with `<SectionRenderer section={section} />` or `<SectionList sections={sections} />`.
67
+
68
+ ### Common Affected Patterns
69
+
70
+ | Pattern | Files |
71
+ |---------|-------|
72
+ | `children.Component` | Container, GridItem, NotFoundChallenge |
73
+ | `sectionChildrens.map(s => <s.Component>)` | Grid, Flex |
74
+ | `section.Component` | ShelfWithImage, ProductMain (sectionDefaultPage, sectionHouseCatalog) |
75
+ | `notFoundSections.map(s => <s.Component>)` | search/NotFound |
76
+
77
+ ---
78
+
79
+ ## 2. Nested Section Loaders Don't Run
80
+
81
+ ### Problem
82
+
83
+ Section loaders (`export const loader`) registered via `registerSectionLoaders()` only run for **top-level** sections (those directly on the CMS page). Nested sections rendered via `SectionRenderer` don't have their loaders executed — they get raw CMS props without enrichment.
84
+
85
+ ### Symptom
86
+
87
+ A nested section crashes because it expects loader-enriched props (e.g., `buyTogetherPricesSimulation` is `undefined`).
88
+
89
+ ### Fix
90
+
91
+ Add a guard at the top of the component:
92
+
93
+ ```typescript
94
+ function BuyTogether({ products, buyTogetherPricesSimulation, ...rest }) {
95
+ if (!products?.length || !buyTogetherPricesSimulation) return null;
96
+ // ...
97
+ }
98
+ ```
99
+
100
+ For full support, either:
101
+ 1. Register the nested section in `registerSectionLoaders()` AND make the section loader registry run recursively on normalized nested sections
102
+ 2. Integrate the nested section's data fetching into the parent section's loader
103
+
104
+ ---
105
+
106
+ ## 3. Dev Mode Cache Control
107
+
108
+ ### Architecture — 4 cache layers
109
+
110
+ | # | Layer | Where | Dev | Prod |
111
+ |---|-------|-------|-----|------|
112
+ | 1 | **Cloudflare Cache API** | Edge Worker (`workerEntry.ts`) | `caches` doesn't exist → skip | Full Cache API with segments |
113
+ | 2 | **Server-side SWR** | SSR in-memory (`createCachedLoader`) | `NODE_ENV=development` → bypass | SWR with maxAge per loader |
114
+ | 3 | **TanStack Query** | Client (`__root.tsx`) | `staleTime: 0` via `import.meta.env.DEV` | `staleTime: 30_000` |
115
+ | 4 | **TanStack Router** | Client SPA nav (`routeCacheDefaults`) | `isDevMode()` → `{staleTime:0, gcTime:0}` | Profile-based (1-5 min) |
116
+
117
+ ### Key Configuration
118
+
119
+ **`.env`** — Do NOT set `DECO_CACHE_DISABLE=true` (dangerous if deployed to prod). Vite automatically sets `NODE_ENV=development` in dev.
120
+
121
+ **`__root.tsx`** — Use `import.meta.env.DEV` for client-side:
122
+ ```typescript
123
+ new QueryClient({
124
+ defaultOptions: {
125
+ queries: {
126
+ staleTime: import.meta.env.DEV ? 0 : 30_000,
127
+ gcTime: import.meta.env.DEV ? 0 : 5 * 60_000,
128
+ refetchOnWindowFocus: import.meta.env.DEV,
129
+ },
130
+ },
131
+ })
132
+ ```
133
+
134
+ **`cachedLoader.ts`** — Inline env detection (Cloudflare Worker can't resolve cross-file imports from linked packages):
135
+ ```typescript
136
+ const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
137
+ const isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
138
+ if (policy === "no-store" || isDev) return loaderFn;
139
+ ```
140
+
141
+ **`routeCacheDefaults()`** — Same inline pattern (no `import { isDevMode }` — fails in Worker runtime).
142
+
143
+ ### Critical: Do NOT use `import { isDevMode } from "./env"` in SDK files
144
+
145
+ Cloudflare Worker's module runner (`workers/runner-worker.js`) can't resolve relative imports from linked packages. Always inline the env detection:
146
+
147
+ ```typescript
148
+ // WRONG — crashes with "ReferenceError: isDevMode is not defined"
149
+ import { isDevMode } from "./env";
150
+
151
+ // CORRECT — inline detection
152
+ const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
153
+ const isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 4. Cloudflare Cache API Guards (`workerEntry.ts`)
159
+
160
+ ### Problem
161
+
162
+ `caches.default` (Cloudflare Cache API) doesn't exist in local dev (miniflare/wrangler dev). Accessing it throws: `internal error; reference = ...`
163
+
164
+ ### Fix
165
+
166
+ Guard all cache operations:
167
+
168
+ ```typescript
169
+ const cache = typeof caches !== "undefined"
170
+ ? (caches as unknown as { default?: Cache }).default ?? null
171
+ : null;
172
+
173
+ // cache.match
174
+ if (cache) {
175
+ try {
176
+ const cached = await cache.match(cacheKey);
177
+ // ...
178
+ } catch { /* Cache API unavailable */ }
179
+ }
180
+
181
+ // cache.put
182
+ if (cache) {
183
+ try {
184
+ ctx.waitUntil(cache.put(cacheKey, toStore));
185
+ } catch { /* skip */ }
186
+ }
187
+
188
+ // cache.delete (purge)
189
+ if (!cache) {
190
+ return Response.json({ purged: [], total: 0, note: "Cache API unavailable" });
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## 5. VTEX API Resilience
197
+
198
+ ### Problem
199
+
200
+ VTEX cross-selling endpoints (e.g., `/crossselling/{id}/showtogether`) return 404 for products without related items. An unhandled 404 crashes the entire section loader, causing the PDP to fail.
201
+
202
+ ### Fix
203
+
204
+ Wrap optional VTEX calls with `.catch(() => fallback)`:
205
+
206
+ ```typescript
207
+ const showTogetherPromise = getCrossSelling(id, "showtogether").catch(() => []);
208
+ ```
209
+
210
+ ### General Pattern for Section Loaders
211
+
212
+ Section loaders that call multiple APIs should use `Promise.allSettled` or individual `.catch()` to prevent one failure from killing the entire page.
213
+
214
+ ---
215
+
216
+ ## 6. Fresh/Preact Remnants to Fix
217
+
218
+ ### `f-partial` / `f-client-nav` attributes
219
+
220
+ Fresh/HTMX-specific attributes. Replace with TanStack Router navigation:
221
+
222
+ ```typescript
223
+ // BEFORE (Fresh)
224
+ <button f-partial="/products/variant" f-client-nav>
225
+
226
+ // AFTER (TanStack)
227
+ <a href={relative(url)} />
228
+ // or
229
+ const navigate = useNavigate();
230
+ <button onClick={() => navigate({ to: relative(url) })} />
231
+ ```
232
+
233
+ ### `MouseEvent` vs `React.MouseEvent`
234
+
235
+ React 19 requires `React.MouseEvent<HTMLButtonElement>` instead of native `MouseEvent` in onClick handlers.
236
+
237
+ ### `product.url` absolute URLs
238
+
239
+ VTEX returns absolute URLs (e.g., `https://secure.store.com.br/product/p`). Always wrap with `relative()`:
240
+
241
+ ```typescript
242
+ href={relative(product.url) ?? product.url}
243
+ ```
244
+
245
+ ### SVG attribute casing
246
+
247
+ `stroke-linecap` → `strokeLinecap`, `stroke-linejoin` → `strokeLinejoin`, `fetchpriority` → `fetchPriority`
248
+
249
+ ### `selected` on `<option>`
250
+
251
+ React uses `<select defaultValue={val}>` instead of `<option selected>`.
252
+
253
+ ### `isMobile` / custom props on DOM elements
254
+
255
+ React warns about unknown DOM props. Filter them before spreading onto native elements:
256
+
257
+ ```typescript
258
+ const { isMobile, productMain, ...htmlProps } = props;
259
+ return <div {...htmlProps} />;
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 7. `DecoPageRenderer` Consolidation
265
+
266
+ The `DecoPageRenderer` should come from `@decocms/start/hooks`, not be duplicated in the site:
267
+
268
+ ```typescript
269
+ // src/components/DecoPageRenderer.tsx
270
+ export { DecoPageRenderer } from "@decocms/start/hooks";
271
+ ```
272
+
273
+ This ensures the lazy cache is shared between `DecoPageRenderer` (top-level sections) and `SectionRenderer` (nested sections).
274
+
275
+ ---
276
+
277
+ ## 8. Vite Cache Stale Data
278
+
279
+ ### Symptom
280
+
281
+ Code changes don't take effect after restarting the dev server.
282
+
283
+ ### Fix
284
+
285
+ ```bash
286
+ pkill -f "vite dev"
287
+ rm -rf node_modules/.vite .wrangler
288
+ bun run dev # or npx vite dev
289
+ ```
290
+
291
+ When using `file:` dependencies (local linked packages), Vite's pre-bundling cache can serve stale modules. Always clear after changes to linked packages.
292
+
293
+ ---
294
+
295
+ ## 9. Section Registry Debug
296
+
297
+ If a section doesn't render, add this guard in `DecoPageRenderer`:
298
+
299
+ ```typescript
300
+ lazy(async () => {
301
+ const mod = await loader();
302
+ if (!mod?.default) {
303
+ console.error(`[DecoSection] "${key}" has no default export`, Object.keys(mod ?? {}));
304
+ return { default: () => null };
305
+ }
306
+ return mod;
307
+ })
308
+ ```
309
+
310
+ This logs the exact section key that's broken instead of crashing with "Element type is invalid".
311
+
312
+ ---
313
+
314
+ ## 10. `node:async_hooks` Leak to Client Bundle
315
+
316
+ ### Problem
317
+
318
+ `src/apps/site.ts` imports `RequestContext` from `@decocms/start/sdk/requestContext`, which uses `AsyncLocalStorage` from `node:async_hooks`. When client components import from `site.ts` (e.g., for `AppContext` type or `_platform` constant), the bundler pulls `node:async_hooks` into the client bundle:
319
+
320
+ ```
321
+ Uncaught (in promise) Error: Module "node:async_hooks" has been externalized for browser compatibility
322
+ ```
323
+
324
+ ### Root Cause
325
+
326
+ A single file (`site.ts`) exports both:
327
+ - **Client-safe** things: `Platform` type, `_platform` constant, `AppContext` type
328
+ - **Server-only** things: `getAppContext()` function that uses `RequestContext` (AsyncLocalStorage)
329
+
330
+ Client components importing `{ AppContext }` from `~/apps/site.ts` transitively import the server-only code.
331
+
332
+ ### Fix — Split into Two Files
333
+
334
+ **`src/apps/site.types.ts`** (client-safe):
335
+ ```typescript
336
+ import type { Device } from "~/sdk/useDevice";
337
+
338
+ export type Platform = "vtex";
339
+ export const _platform: Platform = "vtex";
340
+
341
+ export interface AppContext {
342
+ request: Request;
343
+ device: Device;
344
+ }
345
+ ```
346
+
347
+ **`src/apps/site.ts`** (server-only):
348
+ ```typescript
349
+ import { RequestContext } from "@decocms/start/sdk/requestContext";
350
+ import { detectDevice } from "~/sdk/useDevice";
351
+
352
+ export type { Platform, AppContext } from "./site.types";
353
+ export { _platform } from "./site.types";
354
+
355
+ export function getAppContext(req?: Request) {
356
+ const request = req ?? RequestContext.value;
357
+ return { request, device: detectDevice(request) };
358
+ }
359
+ ```
360
+
361
+ **All client components**: import from `site.types.ts`:
362
+ ```typescript
363
+ // BEFORE
364
+ import { AppContext } from "~/apps/site.ts";
365
+
366
+ // AFTER
367
+ import type { AppContext } from "~/apps/site.types.ts";
368
+ ```
369
+
370
+ ### Discovery Command
371
+
372
+ ```bash
373
+ rg 'from.*~/apps/site' src/components/ src/sections/ src/sdk/ --glob '*.{tsx,ts}' -l
374
+ ```
375
+
376
+ ---
377
+
378
+ ## 11. SliderJS DOM Timing (requestAnimationFrame Retry)
379
+
380
+ ### Problem
381
+
382
+ `SliderJS.tsx` uses vanilla JS `document.querySelector` inside `useEffect` to find slider elements. In TanStack Start with Suspense/lazy sections, the DOM elements may not exist when the effect runs — `setup()` fails silently.
383
+
384
+ ### Symptom
385
+
386
+ Slider arrows/dots don't work, autoplay doesn't start, but no console errors. The `setup()` function returns early because `root` is `null`.
387
+
388
+ ### Fix — rAF Retry Loop
389
+
390
+ ```typescript
391
+ function Slider({ rootId, scroll, interval, infinite }: Props) {
392
+ useEffect(() => {
393
+ let cleanup: (() => void) | undefined;
394
+ let retries = 0;
395
+ const maxRetries = 20;
396
+
397
+ const trySetup = () => {
398
+ cleanup = setup({ rootId, scroll, interval, infinite });
399
+ if (cleanup) return;
400
+
401
+ retries++;
402
+ if (retries < maxRetries) {
403
+ requestAnimationFrame(trySetup);
404
+ }
405
+ };
406
+
407
+ requestAnimationFrame(trySetup);
408
+
409
+ return () => cleanup?.();
410
+ }, [rootId, scroll, interval, infinite]);
411
+
412
+ return <div data-slider-controller-js />;
413
+ }
414
+ ```
415
+
416
+ Uses `requestAnimationFrame` (not `setTimeout`) because rAF fires after the browser has painted — the DOM is guaranteed to be up to date.
417
+
418
+ ### Why Not Convert to React Refs
419
+
420
+ SliderJS relies on data-attributes (`data-slider`, `data-slide`, `data-dot`) and `IntersectionObserver` on items. Converting entirely to React refs would require rewriting every section that uses `<Slider.Root>`, `<Slider.Item>`, `<Slider.Dot>`. The rAF retry is a pragmatic fix that preserves the existing API surface.
421
+
422
+ ---
423
+
424
+ ## 12. Cart Button Loading State Block
425
+
426
+ ### Problem
427
+
428
+ `CartButtonVTEX.tsx` reads `loading` from `useCart()`, which starts as `true` during initialization. If the cart fetch fails silently or takes too long, the button stays disabled and the user cannot open the cart drawer.
429
+
430
+ ### Fix — Separate Data Loading from UI Interactivity
431
+
432
+ The cart icon/button always needs to be clickable (to open the drawer). The loading state should only affect the badge/total display:
433
+
434
+ ```typescript
435
+ return (
436
+ <Button
437
+ currency={currency}
438
+ loading={false} // Button always clickable
439
+ total={(total - discounts) / 100}
440
+ items={items.map((item, index) =>
441
+ itemToAnalyticsItem({ ...item, coupon }, index)
442
+ )}
443
+ />
444
+ );
445
+ ```
446
+
447
+ ### General Pattern
448
+
449
+ For UI elements with dual purpose (data display + action trigger), don't let data loading block the action:
450
+
451
+ ```typescript
452
+ // BAD — loading blocks everything
453
+ <button disabled={isLoading} onClick={openDrawer}>
454
+
455
+ // GOOD — loading only affects display, button always works
456
+ <button onClick={openDrawer}>
457
+ {isLoading ? <CartIcon count={0} /> : <CartIcon count={items.length} />}
458
+ </button>
459
+ ```
460
+
461
+ ---
462
+
463
+ ## 13. Server Functions for VTEX Actions
464
+
465
+ ### Problem
466
+
467
+ In Fresh/Deno, VTEX API calls (newsletter signup, MasterData writes, shipping simulation) happened server-side via form actions or inline `fetch()`. In TanStack Start on Cloudflare Workers, client-side `fetch()` to VTEX APIs hits CORS issues, and form actions cause full reloads.
468
+
469
+ ### Solution — `createServerFn`
470
+
471
+ ```typescript
472
+ // src/lib/vtex-actions-server.ts
473
+ import { createServerFn } from "@tanstack/react-start";
474
+
475
+ export const createDocument = createServerFn({ method: "POST" })
476
+ .handler(async (ctx) => {
477
+ const { entity, dataForm } = ctx.data;
478
+ const resp = await fetch(
479
+ `https://${ACCOUNT}.vtexcommercestable.com.br/api/dataentities/${entity}/documents`,
480
+ {
481
+ method: "POST",
482
+ headers: {
483
+ "Content-Type": "application/json",
484
+ "VtexIdclientAutCookie": VTEX_AUTH_TOKEN,
485
+ },
486
+ body: JSON.stringify(dataForm),
487
+ }
488
+ );
489
+ if (!resp.ok) throw new Error(`VTEX ${resp.status}`);
490
+ return resp.json();
491
+ });
492
+
493
+ export const simulateShipping = createServerFn({ method: "POST" })
494
+ .handler(async (ctx) => {
495
+ const { items, postalCode, country } = ctx.data;
496
+ const resp = await fetch(
497
+ `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForms/simulation`,
498
+ {
499
+ method: "POST",
500
+ headers: { "Content-Type": "application/json" },
501
+ body: JSON.stringify({ items, postalCode, country }),
502
+ }
503
+ );
504
+ return resp.json();
505
+ });
506
+ ```
507
+
508
+ ### Pattern: Every VTEX Write Must Be a Server Function
509
+
510
+ | Operation | Server Function | Why |
511
+ |-----------|----------------|-----|
512
+ | Newsletter signup | `createDocument` | MasterData write needs auth |
513
+ | Shipping simulation | `simulateShipping` | CORS on checkout API |
514
+ | Cart add/remove | `addItems` / `updateItems` | Already in `useCart` |
515
+ | Coupon apply | `addCouponsToCart` | OrderForm mutation |
516
+
517
+ ---
518
+
519
+ ## 14. DOM Manipulation Conflicts with React
520
+
521
+ ### Problem
522
+
523
+ Fresh-era components use `document.querySelector` and `element.checked = true` to control UI, bypassing React's rendering:
524
+
525
+ ```typescript
526
+ // BAD — React doesn't know about this state change
527
+ const cartCheckbox = document.querySelector('.drawer-end .drawer-toggle');
528
+ if (cartCheckbox) cartCheckbox.checked = true;
529
+ ```
530
+
531
+ ### Fix — Use Signals/State
532
+
533
+ ```typescript
534
+ import { useUI } from "~/sdk/useUI";
535
+ const { displayCart } = useUI();
536
+ displayCart.value = true;
537
+ ```
538
+
539
+ ### Discovery Command
540
+
541
+ ```bash
542
+ rg 'document\.(querySelector|getElementById)' src/components/ --glob '*.{tsx,ts}' -l
543
+ rg '\.checked\s*=' src/components/ --glob '*.{tsx,ts}'
544
+ rg '\.classList\.(add|remove|toggle)' src/components/ --glob '*.{tsx,ts}'
545
+ ```
546
+
547
+ ### Acceptable DOM Manipulation
548
+
549
+ - **Analytics scripts**: Fire-and-forget `addEventListener` for click tracking
550
+ - **Third-party library init**: `Autodesk.Viewing.Initializer` in `useEffect`
551
+ - **Scroll operations**: `element.scrollTo()`, `element.scrollIntoView()`
552
+ - **Focus management**: `element.focus()`, `element.blur()`
553
+
554
+ ---
555
+
556
+ ## 15. TypeScript Patterns Post-Migration
557
+
558
+ ### `productId` Type Coercion
559
+
560
+ VTEX loaders return `productID` as `string | undefined`. Cart APIs require `string`:
561
+
562
+ ```typescript
563
+ const id = product.productID ?? "";
564
+ ```
565
+
566
+ ### Explicit Typing for Nullable Values
567
+
568
+ ```typescript
569
+ // PROBLEM: TypeScript infers 'null'
570
+ let noInterestValue = null;
571
+
572
+ // FIX: explicit typing
573
+ let noInterestValue: string | null = null;
574
+ ```
575
+
576
+ ### `React.MouseEvent` vs `MouseEvent`
577
+
578
+ ```typescript
579
+ // BEFORE (native DOM)
580
+ onClick={(e: MouseEvent) => { ... }}
581
+
582
+ // AFTER (React)
583
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => { ... }}
584
+ ```
585
+
586
+ ---
587
+
588
+ ## 16. Loader `cache` / `cacheKey` Module Exports
589
+
590
+ ### Problem
591
+
592
+ In `deco-cx/apps`, loaders declare their caching policy as module exports:
593
+
594
+ ```typescript
595
+ // deco-cx/apps pattern
596
+ export const cache = "stale-while-revalidate";
597
+ export const cacheKey = (props, req, ctx) =>
598
+ JSON.stringify(props) + `:sc=${ctx.salesChannel}`;
599
+ export default async function myLoader(props) { ... }
600
+ ```
601
+
602
+ `@decocms/start`'s `createCachedLoader` previously only accepted inline options.
603
+
604
+ ### Solution — `createCachedLoaderFromModule`
605
+
606
+ New utility in `@decocms/start/sdk/cachedLoader`:
607
+
608
+ ```typescript
609
+ import { createCachedLoaderFromModule, type LoaderModule } from "@decocms/start/sdk/cachedLoader";
610
+
611
+ // Import the loader module (not just the default export)
612
+ import * as myLoaderModule from "./loaders/myLoader";
613
+
614
+ const cached = createCachedLoaderFromModule("myLoader", myLoaderModule, {
615
+ policy: "stale-while-revalidate",
616
+ maxAge: 60_000, // fallback if module doesn't declare cache
617
+ });
618
+ ```
619
+
620
+ ### `LoaderModule` Interface
621
+
622
+ ```typescript
623
+ interface LoaderModule<TProps = any, TResult = any> {
624
+ default: (props: TProps) => Promise<TResult>;
625
+ cache?: CachePolicy | { maxAge: number };
626
+ cacheKey?: (props: TProps) => string | null;
627
+ }
628
+ ```
629
+
630
+ ### Priority
631
+
632
+ 1. Module `cache` export overrides the defaults `policy`
633
+ 2. Module `cacheKey` export overrides the default `keyFn`
634
+ 3. If module has `cache: { maxAge: 120_000 }`, policy is automatically `stale-while-revalidate` with that TTL
635
+ 4. If `cacheKey` returns `null`, falls back to `JSON.stringify(props)`
636
+
637
+ ### When to Use
638
+
639
+ - Porting loaders from `deco-cx/apps` that already have `export const cache = ...`
640
+ - Creating new loaders that need custom cache keys (e.g., segment-aware caching)
641
+ - When different instances of the same loader type need different cache policies
642
+
643
+ ---
644
+
645
+ ## 17. Multi-Layer VTEX Cache Architecture
646
+
647
+ ### Current Cache Stack (as of March 2026)
648
+
649
+ ```
650
+ ┌─────────────────────────────────────────────────────────┐
651
+ │ Cloudflare Edge Cache (workerEntry.ts) │
652
+ │ Profile-based: 1min (search) → 1 day (static) │
653
+ ├─────────────────────────────────────────────────────────┤
654
+ │ TanStack Router staleTime (client) │
655
+ │ Profile-based: 60s (search) → 5min (product/static) │
656
+ ├─────────────────────────────────────────────────────────┤
657
+ │ Layout Resolution Cache (resolve.ts) │
658
+ │ 5 min TTL — full CMS section output │
659
+ ├─────────────────────────────────────────────────────────┤
660
+ │ Layout Section Loader Cache (sectionLoaders.ts) │
661
+ │ 5 min TTL — section loader enrichment │
662
+ ├─────────────────────────────────────────────────────────┤
663
+ │ createCachedLoader SWR (cachedLoader.ts) │
664
+ │ 30-120s TTL per loader — commerce data │
665
+ ├─────────────────────────────────────────────────────────┤
666
+ │ vtexCachedFetch / fetchWithCache (fetchCache.ts) │
667
+ │ 3 min TTL — raw HTTP JSON responses, LRU 500 │
668
+ ├─────────────────────────────────────────────────────────┤
669
+ │ In-flight dedup (all layers) │
670
+ │ Concurrent calls share same Promise │
671
+ └─────────────────────────────────────────────────────────┘
672
+ ```
673
+
674
+ ### Key Insight
675
+
676
+ Each layer serves a different purpose:
677
+ - **Edge cache**: Avoids hitting the Worker at all
678
+ - **Router staleTime**: Avoids client→server roundtrip
679
+ - **Layout caches**: Avoids re-resolving Header/Footer sections
680
+ - **Loader SWR**: Avoids re-running commerce data transformations
681
+ - **Fetch SWR**: Avoids re-hitting VTEX APIs
682
+ - **Inflight dedup**: Avoids duplicate concurrent requests at any layer
683
+
684
+ The layers are **complementary**, not redundant. A request might hit the loader cache (miss, 60s expired) but still get a fetch cache hit (3 min fresh).
685
+
686
+ ---
687
+
688
+ ## 18. Setup Import Order in `server.ts`
689
+
690
+ ### Problem
691
+
692
+ When navigating client-side (SPA), TanStack Start calls `loadCmsPage` as a server function. If `./setup` isn't imported before `createStartHandler`, the section registry, block data, and VTEX loaders are empty when the server function runs — resulting in a 404 or blank page on client navigation even though SSR works fine.
693
+
694
+ ### Fix
695
+
696
+ ```typescript
697
+ // server.ts — import setup FIRST, before createStartHandler
698
+ import "./setup";
699
+ import { createStartHandler, defaultStreamingHandler } from "@tanstack/react-start/server";
700
+ import { getRouterManifest } from "@tanstack/react-start/router-manifest";
701
+ import { createRouter } from "./router";
702
+
703
+ export default createStartHandler({
704
+ createRouter,
705
+ getRouterManifest,
706
+ })(defaultStreamingHandler);
707
+ ```
708
+
709
+ ### Why
710
+
711
+ TanStack Start compiles `createServerFn()` into split modules with isolated Vite instances. Module-level state from `setup.ts` (section registry, blocks, commerce loaders) doesn't persist across the split unless explicitly initialized before the handler is created.
712
+
713
+ ---
714
+
715
+ ## 19. globalThis Backing for Module-Level State
716
+
717
+ ### Problem
718
+
719
+ TanStack Start compiles `createServerFn()` calls into "split modules" — each server function runs in a separate Vite module instance. Module-level `let` or `const` variables (`blockData`, `commerceLoaders`, `registry`, etc.) start empty in each RPC call because they're declared in a different instance than the one that ran `setup.ts`.
720
+
721
+ ### Symptom
722
+
723
+ - Sections render fine on first SSR load
724
+ - On client navigation (TanStack Router), pages return empty or 404
725
+ - `blockData` is empty, loaders return `undefined`
726
+
727
+ ### Fix — Back State with `globalThis.__deco`
728
+
729
+ All singleton module-level state in `@decocms/start` is backed by `globalThis.__deco`:
730
+
731
+ ```typescript
732
+ // src/cms/loader.ts
733
+ declare global { var __deco: { blockData?: ...; revision?: string; ... } }
734
+ if (!globalThis.__deco) globalThis.__deco = {};
735
+
736
+ let _blockData: BlockData = globalThis.__deco.blockData ?? {};
737
+ export function setBlocks(data: BlockData) {
738
+ globalThis.__deco.blockData = data;
739
+ _blockData = data;
740
+ }
741
+ ```
742
+
743
+ Files affected:
744
+
745
+ | File | State backed by globalThis |
746
+ |------|---------------------------|
747
+ | `cms/loader.ts` | `blockData`, `revision` |
748
+ | `cms/resolve.ts` | `commerceLoaders`, `customMatchers`, `initCallback`, `initialized`, `asyncConfig` |
749
+ | `cms/registry.ts` | `registry`, `sectionOptions` |
750
+ | `cms/sectionLoaders.ts` | `loaderRegistry` (Map), `layoutSections` (Set) |
751
+
752
+ ---
753
+
754
+ ## 20. Async Rendering / Deferred Sections
755
+
756
+ ### Architecture
757
+
758
+ `setAsyncRenderingConfig()` controls which sections are rendered synchronously (eager) vs deferred via `IntersectionObserver`:
759
+
760
+ ```typescript
761
+ // setup.ts
762
+ import { setAsyncRenderingConfig } from "@decocms/start";
763
+
764
+ setAsyncRenderingConfig({
765
+ respectCmsLazy: true, // honor Lazy wrapper in CMS config
766
+ foldThreshold: 2, // first N sections always eager
767
+ alwaysEager: [ // specific sections always eager (by __resolveType)
768
+ "site/sections/Header.tsx",
769
+ "site/sections/Footer.tsx",
770
+ ],
771
+ });
772
+ ```
773
+
774
+ ### How `shouldDeferSection()` Works
775
+
776
+ | Condition | Result |
777
+ |-----------|--------|
778
+ | Section has `Lazy` wrapper in CMS | Deferred (if `respectCmsLazy: true`) |
779
+ | Section is in `alwaysEager` list | Always eager |
780
+ | Section is a layout section (Header/Footer) | Always eager |
781
+ | Section index < `foldThreshold` | Eager |
782
+ | Request is from a bot (UA detection) | All sections eager |
783
+
784
+ ### `loadDeferredSection` — POST not GET
785
+
786
+ The server function uses `POST` (not `GET`) because section props (images, text, arrays) serialized as query params exceed the 431 "Request Header Fields Too Large" limit.
787
+
788
+ ```typescript
789
+ // cmsRoute.ts
790
+ export const loadDeferredSection = createServerFn({ method: "POST" })
791
+ .handler(async (ctx) => {
792
+ const { data } = ctx;
793
+ return resolveDeferredSection(data);
794
+ });
795
+ ```
796
+
797
+ ### `DeferredSectionWrapper` in `DecoPageRenderer.tsx`
798
+
799
+ Uses `IntersectionObserver` with `rootMargin: "300px"` to load sections before they enter the viewport:
800
+
801
+ ```typescript
802
+ function DeferredSectionWrapper({ deferred, loadDeferredSectionFn }) {
803
+ const ref = useRef<HTMLDivElement>(null);
804
+ const [section, setSection] = useState(null);
805
+
806
+ useEffect(() => {
807
+ const observer = new IntersectionObserver(
808
+ ([entry]) => {
809
+ if (entry.isIntersecting) {
810
+ observer.disconnect();
811
+ loadDeferredSectionFn(deferred).then(setSection);
812
+ }
813
+ },
814
+ { rootMargin: "300px" }
815
+ );
816
+ if (ref.current) observer.observe(ref.current);
817
+ return () => observer.disconnect();
818
+ }, []);
819
+
820
+ return section
821
+ ? <SectionRenderer section={section} />
822
+ : <div ref={ref} style={{ minHeight: "1px" }} />;
823
+ }
824
+ ```
825
+
826
+ ---
827
+
828
+ ## 21. Section Ordering Fix — Index Stamping
829
+
830
+ ### Problem (v0.16.3 → v0.16.4)
831
+
832
+ `mergeSections()` (which interleaves eager + deferred sections for rendering) used slot-filling — trying to fill deferred "slots" by index into the original CMS array. When multiple eager sections resolved from a single CMS entry (e.g., a shelf with 0 products), the slot indices diverged and sections rendered out of order or disappeared.
833
+
834
+ ### Root Cause
835
+
836
+ The original design assumed: 1 CMS entry → 1 rendered section. In practice, loaders can return multiple or zero sections.
837
+
838
+ ### Fix — Stamp `index` on Each Eager Section
839
+
840
+ In `resolve.ts`, after running section loaders, stamp each resolved section with its original CMS position:
841
+
842
+ ```typescript
843
+ // Each resolved eager section gets its original flat CMS index
844
+ for (const eagerly of eagerSections) {
845
+ eagerly.then((s) => {
846
+ if (s) s.index = currentFlatIndex;
847
+ });
848
+ currentFlatIndex++;
849
+ }
850
+ ```
851
+
852
+ In `mergeSections()`, sort by `index` instead of slot-filling:
853
+
854
+ ```typescript
855
+ function mergeSections(
856
+ sections: ResolvedSection[],
857
+ deferredSections: DeferredSection[]
858
+ ): Array<{ type: "resolved"; section: ResolvedSection } | { type: "deferred"; deferred: DeferredSection }> {
859
+ const all = [
860
+ ...sections.map(s => ({ type: "resolved" as const, index: s.index ?? 0, section: s })),
861
+ ...deferredSections.map(d => ({ type: "deferred" as const, index: d.index, deferred: d })),
862
+ ];
863
+ return all.sort((a, b) => a.index - b.index);
864
+ }
865
+ ```
866
+
867
+ ---
868
+
869
+ ## 22. Layout Section Caching
870
+
871
+ ### Problem
872
+
873
+ Header and Footer re-execute their full commerce loaders (navigation menu, cart, promotions) on every page navigation. These sections are identical across all pages but were resolved fresh each time.
874
+
875
+ ### Solution — `resolvedLayoutCache`
876
+
877
+ In `resolve.ts`, layout sections (registered via `registerLayoutSection()`) are cached for 5 minutes:
878
+
879
+ ```typescript
880
+ const resolvedLayoutCache = new Map<string, { result: ResolvedSection; timestamp: number }>();
881
+ const LAYOUT_CACHE_TTL = 5 * 60 * 1000; // 5 min
882
+
883
+ async function resolveLayoutSection(key: string, ...): Promise<ResolvedSection> {
884
+ const cached = resolvedLayoutCache.get(key);
885
+ if (cached && Date.now() - cached.timestamp < LAYOUT_CACHE_TTL) {
886
+ return cached.result;
887
+ }
888
+ const result = await resolveSection(key, ...);
889
+ resolvedLayoutCache.set(key, { result, timestamp: Date.now() });
890
+ return result;
891
+ }
892
+ ```
893
+
894
+ ### Registration
895
+
896
+ ```typescript
897
+ // setup.ts
898
+ import { registerLayoutSection } from "@decocms/start";
899
+ registerLayoutSection("site/sections/Header.tsx");
900
+ registerLayoutSection("site/sections/Footer.tsx");
901
+ ```
902
+
903
+ ---
904
+
905
+ ## 23. Analytics Hydration Mismatch Fix
906
+
907
+ ### Problem
908
+
909
+ `Analytics.tsx` used `useScriptAsDataURI` which converts a function to a string via `fn.toString()`. The string representation differs between server-side and client-side bundles (minification, whitespace), causing a hydration mismatch and CLS.
910
+
911
+ ### Fix
912
+
913
+ Replace with `useScript` + `dangerouslySetInnerHTML` + `suppressHydrationWarning`:
914
+
915
+ ```typescript
916
+ // BEFORE — causes hydration mismatch
917
+ const src = useScriptAsDataURI(sendAnalytics, { endpoint: "/analytics" });
918
+ return <script async src={src} />;
919
+
920
+ // AFTER — suppresses hydration comparison for this element
921
+ function Analytics({ endpoint }: Props) {
922
+ const script = useScript(sendAnalytics, { endpoint });
923
+ return (
924
+ <script
925
+ dangerouslySetInnerHTML={{ __html: script }}
926
+ suppressHydrationWarning
927
+ />
928
+ );
929
+ }
930
+ ```
931
+
932
+ ---
933
+
934
+ ## 24. Vite Config for Published `@decocms/start` Packages
935
+
936
+ ### Problem
937
+
938
+ When `@decocms/start` is a published npm package (not `file:` reference), three issues emerge:
939
+
940
+ 1. **`React is not defined` in SSR deps**: The esbuild pre-bundler uses the classic JSX runtime by default — `React.createElement(...)` without importing React.
941
+ 2. **`tanstack-start-injected-head-scripts:v` in client bundle**: `router-manifest.js` (from `@tanstack/start-server-core`) ends up in the client bundle via import chain. This virtual module is registered only in the SSR environment.
942
+ 3. **Module deduplication**: Without `resolve.dedupe`, Vite may resolve multiple instances of TanStack packages.
943
+
944
+ ### Required `vite.config.ts`
945
+
946
+ ```typescript
947
+ import { defineConfig } from "vite";
948
+
949
+ export default defineConfig({
950
+ plugins: [
951
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
952
+ tanstackStart({ server: { entry: "server" } }),
953
+ react({ babel: { plugins: [["babel-plugin-react-compiler", { target: "19" }]] } }),
954
+ tailwindcss(),
955
+
956
+ // Fix #2: register no-op fallback for the server-only virtual module
957
+ // in the client environment
958
+ {
959
+ name: "deco-tanstack-client-virtual-fallback",
960
+ enforce: "post",
961
+ resolveId(id) {
962
+ if (id === "tanstack-start-injected-head-scripts:v") return `\0${id}`;
963
+ },
964
+ load(id) {
965
+ if (id === "\0tanstack-start-injected-head-scripts:v")
966
+ return "export const injectedHeadScripts = undefined;";
967
+ },
968
+ },
969
+ ],
970
+ optimizeDeps: {
971
+ // Fix #1: use automatic JSX runtime so esbuild adds react/jsx-runtime
972
+ // imports when pre-bundling @decocms/start source files
973
+ esbuildOptions: { jsx: "automatic" },
974
+ },
975
+ resolve: {
976
+ // Fix #3: force single instance for TanStack packages
977
+ dedupe: [
978
+ "@tanstack/react-start",
979
+ "@tanstack/react-router",
980
+ "@tanstack/react-start-server",
981
+ "@tanstack/start-server-core",
982
+ "@tanstack/start-client-core",
983
+ "@tanstack/start-plugin-core",
984
+ "@tanstack/start-storage-context",
985
+ "react",
986
+ "react-dom",
987
+ ],
988
+ },
989
+ });
990
+ ```
991
+
992
+ ### Root Causes
993
+
994
+ - **Fix #1 (`esbuildOptions.jsx: "automatic"`)**: `@decocms/start` ships raw `.tsx` source. Vite's esbuild pre-bundler uses the classic transform by default. `jsx: "automatic"` makes it emit `import { jsx } from "react/jsx-runtime"` instead of `React.createElement`.
995
+ - **Fix #2 (virtual module fallback)**: The import chain `cmsRoute.ts → @tanstack/react-start → @tanstack/start-server-core → router-manifest.js` reaches the client bundle. The `tanstack-start-injected-head-scripts:v` virtual is registered with `applyToEnvironment: server` only.
996
+ - **Fix #3 (dedupe)**: Prevents duplicate TanStack instances when hoisting from peer deps.
997
+
998
+ ---
999
+
1000
+ ## Related Skills
1001
+
1002
+ | Skill | Purpose |
1003
+ |-------|---------|
1004
+ | `deco-to-tanstack-migration` | Initial migration playbook (imports, signals, architecture) |
1005
+ | `deco-tanstack-navigation` | SPA navigation patterns (`<Link>`, `useNavigate`, `loaderDeps`) |
1006
+ | `deco-islands-migration` | Eliminating the `src/islands/` directory |
1007
+ | `deco-edge-caching` | Edge caching with `createDecoWorkerEntry` |
1008
+ | `deco-vtex-fetch-cache` | SWR fetch cache for VTEX APIs (`fetchWithCache`, `vtexCachedFetch`) |
1009
+ | `deco-api-call-dedup` | API call dedup, batching, PLP filtering patterns |
1010
+ | `deco-cms-layout-caching` | Layout section caching in CMS resolve |
1011
+ | `deco-typescript-fixes` | TypeScript error patterns and fixes |
1012
+ | `deco-storefront-test-checklist` | Context-aware QA checklist generation |
1013
+ | `deco-startup-analysis` | Analyzing startup logs for issues |