@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
+ # deco-cx/deco vs @decocms/start -- Second-Pass Gap Analysis
2
+
3
+ > Deep audit of `deco-cx/deco` + `deco-cx/apps` vs `@decocms/start` + `@decocms/apps`.
4
+ > Covers architecture decisions, commerce gaps, SEO, schema/admin, and utilities.
5
+ > Each gap includes proposed solutions, trade-offs, and a recommendation.
6
+ >
7
+ > Last updated: March 6, 2026 (Tier 0 + Tier 1 + Tier 2 implemented)
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [Part 1 -- Architecture Decisions](#part-1----architecture-decisions)
14
+ - [A. Section Data Ownership](#a-section-data-ownership)
15
+ - [B. Partial Section Re-rendering](#b-partial-section-re-rendering)
16
+ - [C. Vary / Segment / CDN Cache Coherence](#c-vary--segment--cdn-cache-coherence)
17
+ - [D. RequestContext via AsyncLocalStorage](#d-requestcontext-via-asynclocalstorage)
18
+ - [Part 2 -- Commerce Platform Gaps](#part-2----commerce-platform-gaps)
19
+ - [E. VTEX Middleware](#e-vtex-middleware)
20
+ - [F. VTEX Proxy Routes](#f-vtex-proxy-routes)
21
+ - [G. Product Extension Pipeline](#g-product-extension-pipeline)
22
+ - [H. Missing VTEX Loaders/Actions](#h-missing-vtex-loadersactions)
23
+ - [I. Missing Shopify Capabilities](#i-missing-shopify-capabilities)
24
+ - [Part 3 -- SEO / Infrastructure](#part-3----seo--infrastructure)
25
+ - [J. Sitemap Generation](#j-sitemap-generation)
26
+ - [K. Redirect System](#k-redirect-system)
27
+ - [L. SEO Components / JSON-LD](#l-seo-components--json-ld)
28
+ - [M. Deferred / Lazy Section Rendering](#m-deferred--lazy-section-rendering)
29
+ - [Part 4 -- Schema / Admin](#part-4----schema--admin)
30
+ - [N. Schema Generation Improvements](#n-schema-generation-improvements)
31
+ - [O. Admin Live Preview / Hot Reload](#o-admin-live-preview--hot-reload)
32
+ - [Part 5 -- Components / Utilities](#part-5----components--utilities)
33
+ - [P. Optimized Media Components](#p-optimized-media-components)
34
+ - [Q. Client-side State Hooks](#q-client-side-state-hooks)
35
+ - [R. Missing Matchers](#r-missing-matchers)
36
+ - [S. Minor Utilities](#s-minor-utilities)
37
+ - [Part 6 -- Prioritized Roadmap](#part-6----prioritized-roadmap)
38
+
39
+ ---
40
+
41
+ ## Part 1 -- Architecture Decisions
42
+
43
+ These are the structural questions that affect everything else.
44
+
45
+ ---
46
+
47
+ ### A. Section Data Ownership
48
+
49
+ **The question:** Should sections own their data fetching (deco model), or should loaders be wired externally (current model)?
50
+
51
+ #### How deco does it
52
+
53
+ In `deco-cx/deco`, a section file can export three things:
54
+
55
+ ```typescript
56
+ // blocks/section.ts lifecycle
57
+ export const loader = (props, req, ctx) => fetchProducts(props);
58
+ export const action = (props, req, ctx) => updateCart(props);
59
+ export default function MySection(loaderResult) { /* render */ }
60
+ ```
61
+
62
+ The block system dispatches by HTTP method: `GET` calls `loader`, non-GET calls `action`. It also supports **object-shaped loaders** where each key resolves independently and concurrently:
63
+
64
+ ```typescript
65
+ export const loader = {
66
+ products: (props, req, ctx) => fetchProducts(props),
67
+ banners: (props, req, ctx) => fetchBanners(props),
68
+ };
69
+ ```
70
+
71
+ #### How we do it today
72
+
73
+ Sections are pure React components. Data fetching is wired externally in `setup.ts` via `registerCommerceLoaders()`. The CMS decofile embeds `__resolveType` references inside section props, and `resolveDecoPage()` walks the tree, detects these references, calls the registered loader, and replaces the block with actual data before the component ever sees it.
74
+
75
+ ```
76
+ [CMS decofile] [setup.ts] [resolveDecoPage]
77
+ Section props with Commerce loaders Walks the tree,
78
+ __resolveType: "vtex/..." ---> registered by key ---> calls loaders,
79
+ returns resolved props
80
+ |
81
+ v
82
+ [React Component]
83
+ Receives plain data
84
+ ```
85
+
86
+ #### Do variants need section loaders?
87
+
88
+ **No.** Looking at the actual decofile (`pages-home-*.json`), the `sections` array is wrapped in `website/flags/multivariate.ts`. Variants swap entire section arrays, including their `__resolveType` references. The resolver picks the winning variant first, then resolves the nested loaders. Both models support variant-driven data changes equally.
89
+
90
+ Where deco's section loaders add value our model lacks:
91
+
92
+ 1. **Server-side prop enrichment** -- A section receives CMS props and needs to transform them before render (e.g., take a `collectionId` and call an API). In our model, this requires registering a commerce loader in `setup.ts`.
93
+ 2. **Colocation** -- The data fetching logic lives with the component, making the section portable across sites.
94
+ 3. **Request context access** -- Deco's section loader receives `(props, req, ctx)`, so it can vary behavior based on cookies, headers, URL. Our commerce loaders only receive props.
95
+
96
+ #### Trade-offs
97
+
98
+ | | External wiring (current) | Section loaders (deco) | Hybrid |
99
+ |---|---|---|---|
100
+ | **React idiomatic** | Yes -- sections are pure components | No -- sections become more than components | Partial |
101
+ | **TanStack compatible** | Fully -- works with route loaders | Against the grain | Coexists awkwardly |
102
+ | **Portability** | Low -- needs setup.ts per site | High -- self-contained | Medium |
103
+ | **Request context** | Not passed to loaders today | Full access (req, cookies, headers) | Optional |
104
+ | **Testing** | Easy -- pure components, loaders separate | Harder -- component+loader coupled | Easy for pure sections |
105
+ | **CMS flexibility** | CMS controls which loader (`__resolveType`) | Section controls how data is fetched | Both |
106
+ | **Optimization** | Route-level can batch across sections | Per-section, harder to deduplicate | Route-level + optional per-section |
107
+ | **Complexity** | Simple mental model | Requires section-aware SSR pipeline | Two ways to do things |
108
+
109
+ #### Recommendation
110
+
111
+ **Keep external wiring** as the primary model -- it's idiomatic React/TanStack and already working. But address the two real gaps:
112
+
113
+ 1. **Pass request context to commerce loaders.** Change `CommerceLoader` signature from `(props) => Promise<any>` to `(props, ctx?: RequestContext) => Promise<any>`. This gives loaders access to cookies, headers, URL without changing the architecture.
114
+
115
+ 2. **Add a `sectionMiddleware` optional pattern.** Sections can export a `transformProps` function that runs server-side before render. This covers the enrichment use case without the full section loader lifecycle:
116
+
117
+ ```typescript
118
+ // Optional: section can export this
119
+ export function transformProps(props: Props): Props {
120
+ return { ...props, computedField: derive(props.someValue) };
121
+ }
122
+ // Required: the component
123
+ export default function MySection(props: Props) { /* render */ }
124
+ ```
125
+
126
+ The resolver would check for `transformProps` in the section registry and apply it during resolution. Light-touch, no new pipeline needed.
127
+
128
+ ---
129
+
130
+ ### B. Partial Section Re-rendering
131
+
132
+ **The question:** `useSection()` and `usePartialSection()` are stubs. How should we enable section-level updates without full page navigation?
133
+
134
+ #### What deco does
135
+
136
+ `useSection({ props, href })` builds a URL to `/deco/render?props=...&href=...&pathTemplate=...&renderSalt=...&__cb=...`. The `__cb` parameter is a Murmurhash cache-bust key derived from `[revisionId, vary, stableHref, deploymentId]`. It strips 20+ marketing UTM params from URLs for cache stability (fbclid, gclid, utm_*, etc.).
137
+
138
+ `usePartialSection()` wraps this and returns `hx-get`/`f-partial` attributes for Fresh/HTMX partial navigation. The response is server-rendered HTML that gets swapped into the DOM.
139
+
140
+ This powers: infinite scroll, load-more buttons, filter changes, tab content loading, and any interaction that refreshes a single section.
141
+
142
+ #### Options in our stack
143
+
144
+ **Option 1: Client-side re-fetching via TanStack Query + invoke**
145
+
146
+ ```
147
+ User interaction -> TanStack Query refetch -> POST /deco/invoke/:key -> JSON -> React re-render
148
+ ```
149
+
150
+ - **Pros:** Idiomatic React. Works today. TanStack Query handles SWR, dedup, background refetch. Integrates with `invokeQueryOptions()` we already built.
151
+ - **Cons:** Requires client-side JS for the section. Initial render is SSR, updates are client-side. Bundle size increases per interactive section. The section must be a client component (or at least have a client wrapper).
152
+ - **Complexity:** Low -- we already have the invoke proxy.
153
+ - **Best for:** Cart updates, wishlist toggles, autocomplete, any section with frequent client interaction.
154
+
155
+ **Option 2: Server-rendered HTML swap (HTMX-style)**
156
+
157
+ ```
158
+ User interaction -> fetch("/deco/render?section=X&props=...") -> HTML -> innerHTML swap
159
+ ```
160
+
161
+ - **Pros:** Zero client JS for the section content. True SSR. CDN-cacheable. Matches deco's model.
162
+ - **Cons:** DOM manipulation outside React's control. Breaks React's reconciliation. State management is awkward (the swapped-in HTML doesn't have React hydration). Need a custom `<PartialSection>` wrapper.
163
+ - **Complexity:** Medium -- need to build the URL construction, param stripping, and DOM swap logic.
164
+ - **Best for:** Infinite scroll, load-more, content that doesn't need client-side interactivity after swap.
165
+
166
+ **Option 3: Route-level navigation with TanStack Router SWR**
167
+
168
+ ```
169
+ User interaction -> router.navigate({ search: { page: 2 } }) -> full page re-render with SWR
170
+ ```
171
+
172
+ - **Pros:** Simplest. Fully server-rendered. URL reflects state (bookmarkable, shareable). TanStack Router's SWR makes it fast (stale page shows immediately, fresh data streams in).
173
+ - **Cons:** Full page re-render (even if fast). URL changes on every interaction. Not suitable for infinite scroll within a section.
174
+ - **Complexity:** Near-zero -- just use TanStack Router.
175
+ - **Best for:** Filter changes, pagination, sort order -- anything where URL state makes sense.
176
+
177
+ **Option 4: React Server Components (future)**
178
+
179
+ ```
180
+ User interaction -> Server re-renders component -> Streamed RSC payload -> React reconciles
181
+ ```
182
+
183
+ - **Pros:** True server-side re-rendering with proper React reconciliation. No client JS for the section. No DOM manipulation hacks.
184
+ - **Cons:** TanStack Start doesn't support RSC yet (experimental). Not available today.
185
+ - **Complexity:** N/A until available.
186
+ - **Best for:** Everything, once it works.
187
+
188
+ #### Recommendation
189
+
190
+ **Use Options 1 + 3 together, skip Option 2.** Here's why:
191
+
192
+ - **Option 3 (router navigation)** for filter changes, pagination, sort -- anywhere URL state makes sense. This is free with TanStack Router.
193
+ - **Option 1 (TanStack Query + invoke)** for cart, wishlist, autocomplete, load-more -- anywhere you need section-level updates without URL changes.
194
+
195
+ Option 2 (HTML swap) is a dead end in a React architecture. It fights React's reconciliation model and creates hydration problems. It only makes sense in an HTMX/Fresh world.
196
+
197
+ Deprecate the stub `usePartialSection()` and instead provide:
198
+ - `useSectionQuery(loaderKey, props, options?)` -- returns a TanStack Query hook for client-side section data
199
+ - Document the pattern of using TanStack Router search params for server-side section re-rendering
200
+
201
+ ---
202
+
203
+ ### C. Vary / Segment / CDN Cache Coherence
204
+
205
+ **The question:** Do we need deco's custom Vary/Segment system, or does Cloudflare Cache API + our `workerEntry.ts` cover it?
206
+
207
+ #### What deco does
208
+
209
+ Two-layer system:
210
+
211
+ 1. **Vary system:** Each loader pushes its cache-relevant dimensions into a `Vary` object during rendering. Example: a loader that reads a cookie pushes `"cookie:myKey"`. The aggregate of all pushed keys becomes the cache differentiation key. If a loader sets `shouldCache = false`, the entire page becomes uncacheable.
212
+
213
+ 2. **`segmentFor()`:** Computes a Murmurhash3 fingerprint from `sorted cookies + sorted flags + deploymentId + revision + URL`. Two users with identical fingerprints get the same CDN response. This is the cohort/segment hash.
214
+
215
+ Together, these ensure: anonymous users share cached pages, logged-in users get personalized (uncached) pages, and A/B test cohorts get their own cached variants.
216
+
217
+ #### What we have today
218
+
219
+ `workerEntry.ts` uses Cloudflare's Cache API with:
220
+ - Device-specific cache keys (`__cf_device=mobile|desktop`)
221
+ - Profile-based cache headers (`static`, `product`, `listing`, `search`, `cart`, `private`, `none`)
222
+ - `detectCacheProfile(url)` based on URL patterns
223
+ - Cache purge endpoint
224
+
225
+ This handles the simple case (anonymous vs. device), but doesn't differentiate by:
226
+ - Login state (logged-in users see personalized prices)
227
+ - A/B test cohort
228
+ - Sales channel / segment
229
+ - Cookies that affect content
230
+
231
+ #### Solution: Segment-aware cache keys
232
+
233
+ Instead of porting deco's full Vary system (which is tightly coupled to their resolver), build a lighter-weight segment key that integrates with Cloudflare's Cache API:
234
+
235
+ ```typescript
236
+ interface SegmentKey {
237
+ device: "mobile" | "desktop";
238
+ loggedIn: boolean;
239
+ salesChannel?: string;
240
+ flags: string[]; // sorted active flag names
241
+ }
242
+
243
+ function buildCacheKey(request: Request, segment: SegmentKey): Request {
244
+ const url = new URL(request.url);
245
+ url.searchParams.set("__seg", hashSegment(segment));
246
+ return new Request(url.toString(), { method: "GET" });
247
+ }
248
+ ```
249
+
250
+ **Where this runs:** In `workerEntry.ts`, before the Cache API lookup. The segment is derived from cookies (VTEX segment cookie, auth token presence) and evaluated flags.
251
+
252
+ **Trade-offs:**
253
+
254
+ | | Full Vary system (deco) | Segment cache key (proposed) |
255
+ |---|---|---|
256
+ | **Granularity** | Per-loader, any dimension | Per-request, predefined dimensions |
257
+ | **Complexity** | High -- every loader participates | Low -- computed once at the edge |
258
+ | **Cache hit rate** | Optimal -- minimal differentiation | Good -- slightly over-differentiated |
259
+ | **Coupling** | Tightly coupled to resolver | Decoupled -- runs at Worker level |
260
+ | **Implementation** | ~400 LOC across utils/vary.ts + segment.ts + loaders | ~100 LOC in workerEntry.ts |
261
+
262
+ **Recommendation:** Build the segment cache key approach. It covers the 90% case (login state, device, sales channel, A/B cohort) without the complexity of per-loader Vary. If we later need per-loader granularity, the segment system can be extended.
263
+
264
+ ---
265
+
266
+ ### D. RequestContext via AsyncLocalStorage
267
+
268
+ **The question:** Should we implement implicit per-request context like deco does?
269
+
270
+ #### What deco does
271
+
272
+ `RequestContext` uses `AsyncLocalStorage` (from `node:async_hooks`) to bind per-request state:
273
+
274
+ - **Automatic fetch cancellation:** Global `fetch` is monkey-patched to inject the request's `AbortSignal`. When a client disconnects, all in-flight fetches for that request abort automatically.
275
+ - **Implicit context access:** Any code in the call stack can call `RequestContext.signal`, `RequestContext.framework`, or `Context.active()` without prop drilling.
276
+ - **Context binding:** `RequestContext.bind(request, fn)` runs a function within a specific request's context.
277
+
278
+ #### Feasibility on Cloudflare Workers
279
+
280
+ `nodejs_compat` is already enabled in `wrangler.jsonc`. `AsyncLocalStorage` works on Workers with this flag. So this is technically feasible.
281
+
282
+ #### What it buys us
283
+
284
+ 1. **Automatic fetch abort** -- The biggest win. If a user navigates away mid-request, all VTEX/Shopify API calls for that request abort. Without this, abandoned requests keep running and consuming resources.
285
+ 2. **No prop drilling for request context** -- Loaders, utilities, and middleware can access the request without it being passed through every function signature.
286
+ 3. **Foundation for richer FnContext** -- Once we have implicit request context, we can build lazy `device`, `isBot`, `flags` accessors like deco does.
287
+
288
+ #### Trade-offs
289
+
290
+ | | AsyncLocalStorage | Explicit context passing |
291
+ |---|---|---|
292
+ | **Ergonomics** | Implicit -- any function can access | Explicit -- must thread through params |
293
+ | **Debuggability** | Harder -- invisible data flow | Easier -- data flow is visible |
294
+ | **Performance** | Slight overhead per request | Zero overhead |
295
+ | **Testing** | Must mock the global context | Just pass different params |
296
+ | **Global fetch patch** | Powerful but surprising (monkey-patch) | No surprises |
297
+ | **CF Workers compat** | Works with `nodejs_compat` | Always works |
298
+
299
+ #### Recommendation
300
+
301
+ **Implement it, but conservatively:**
302
+
303
+ 1. Create `RequestContext` with `AsyncLocalStorage` -- bind it in middleware, expose `.signal` and `.request` getters.
304
+ 2. **Do NOT monkey-patch global fetch.** Instead, expose `RequestContext.fetch` as an alternative that auto-injects the abort signal. Let code opt in.
305
+ 3. Use it in the commerce loader pipeline: `resolveDecoPage` binds the context, commerce loaders can access it.
306
+
307
+ This gives us automatic cancellation where it matters (commerce loaders making VTEX calls) without the surprise of a global monkey-patch.
308
+
309
+ ---
310
+
311
+ ## Part 2 -- Commerce Platform Gaps
312
+
313
+ These are concrete, actionable gaps in the commerce layer.
314
+
315
+ ---
316
+
317
+ ### E. VTEX Middleware -- DONE
318
+
319
+ **What's missing:** `vtex/middleware.ts` from `deco-cx/apps` does three critical things:
320
+
321
+ 1. **Segment extraction** -- Reads the VTEX segment cookie, decodes it, extracts sales channel, price tables, region ID, and trade policy. This determines which prices and products a user sees.
322
+ 2. **IS cookie propagation** -- Propagates Intelligent Search cookies (`vtex_is_*`) for search personalization and analytics.
323
+ 3. **Login-aware cache control** -- Detects if a user is logged in (via `VtexIdclientAutCookie`). Logged-in users get `Cache-Control: private` to prevent personalized content from being cached and served to others.
324
+
325
+ **Impact without it:**
326
+ - Anonymous and logged-in users may share cached pages (wrong prices, wrong wishlists)
327
+ - B2B customers on different price tables see retail prices
328
+ - IS search quality degrades without cookie propagation
329
+
330
+ **Proposed solution:**
331
+
332
+ Create `apps-start/vtex/middleware.ts`:
333
+
334
+ ```typescript
335
+ export interface VtexSegment {
336
+ salesChannel: string;
337
+ priceTables?: string[];
338
+ regionId?: string;
339
+ tradePolicy?: string;
340
+ isLoggedIn: boolean;
341
+ }
342
+
343
+ export function extractVtexSegment(request: Request): VtexSegment { /* ... */ }
344
+ export function vtexCacheControl(segment: VtexSegment): string { /* ... */ }
345
+ export function propagateISCookies(request: Request, response: Response): void { /* ... */ }
346
+ ```
347
+
348
+ This integrates with TanStack Start's `createMiddleware()` in the storefront. The segment feeds into the cache key system (Part 1, Section C).
349
+
350
+ **Location:** `apps-start/vtex/middleware.ts`
351
+ **Effort:** ~150 LOC
352
+ **Dependencies:** `vtexId.ts` for JWT parsing (Section E.1)
353
+
354
+ #### E.1 vtexId JWT parsing
355
+
356
+ We also need `vtexId.ts` to parse `VtexIdclientAutCookie` without calling VTEX APIs. This is a JWT decode (no verification needed since we're just checking presence and expiry, not authenticating).
357
+
358
+ ```typescript
359
+ export function parseVtexAuthCookie(cookie: string): { isLoggedIn: boolean; email?: string; exp?: number } { /* ... */ }
360
+ ```
361
+
362
+ **Location:** `apps-start/vtex/utils/vtexId.ts`
363
+ **Effort:** ~50 LOC
364
+
365
+ ---
366
+
367
+ ### F. VTEX Proxy Routes -- DONE
368
+
369
+ **What's missing:** VTEX checkout, My Account, and API calls require proxying to VTEX's servers. The storefront must route these paths to VTEX origin:
370
+
371
+ | Path | Purpose |
372
+ |------|---------|
373
+ | `/checkout/*` | VTEX checkout pages |
374
+ | `/account/*`, `/account` | My Account pages |
375
+ | `/api/*` | VTEX API (orderForm, session, etc.) |
376
+ | `/files/*`, `/arquivos/*` | VTEX static files (invoices, receipts) |
377
+ | `/checkout/changeToAnonymousUser/*` | Logout from checkout |
378
+
379
+ **Impact without it:** Checkout and My Account don't work. Cart operations fail because `/api/checkout/pub/orderForm/*` is unreachable.
380
+
381
+ **Proposed solution:**
382
+
383
+ Create `apps-start/vtex/utils/proxy.ts`:
384
+
385
+ ```typescript
386
+ export interface VtexProxyConfig {
387
+ account: string;
388
+ environment?: "vtexcommercestable" | "vtexcommercebeta";
389
+ extraPaths?: string[];
390
+ }
391
+
392
+ export function getVtexProxyPaths(config: VtexProxyConfig): string[] { /* ... */ }
393
+
394
+ export async function proxyToVtex(
395
+ request: Request,
396
+ config: VtexProxyConfig,
397
+ ): Promise<Response> { /* ... */ }
398
+ ```
399
+
400
+ The storefront registers these as TanStack Start API routes:
401
+
402
+ ```typescript
403
+ // src/routes/api/vtex-proxy.ts (storefront)
404
+ import { createAPIFileRoute } from "@tanstack/react-start/api";
405
+ import { proxyToVtex } from "@decocms/apps/vtex/utils/proxy";
406
+
407
+ export const APIRoute = createAPIFileRoute("/api/$")({
408
+ GET: async ({ request }) => proxyToVtex(request, vtexConfig),
409
+ POST: async ({ request }) => proxyToVtex(request, vtexConfig),
410
+ });
411
+ ```
412
+
413
+ **Cloudflare Workers consideration:** Workers have a 100MB response body limit and no streaming for subrequest bodies in some cases. For large file downloads (`/files/*`), we may need to use `Response.redirect()` to VTEX origin instead of proxying the body.
414
+
415
+ **Location:** `apps-start/vtex/utils/proxy.ts`
416
+ **Effort:** ~200 LOC
417
+ **Dependencies:** VTEX client config (already exists)
418
+
419
+ ---
420
+
421
+ ### G. Product Extension Pipeline
422
+
423
+ **What's missing:** Deco has `vtex/loaders/product/extensions/*` -- a composable pipeline to enrich products after the initial fetch:
424
+
425
+ 1. **Simulation extension** -- Calls VTEX's simulation API to get real-time prices (important for B2B, regional pricing, promotions)
426
+ 2. **Wishlist extension** -- Checks which products are in the user's wishlist and adds `isInWishlist` to each product
427
+ 3. **Details page extension** -- Enriches PDP products with additional data (related products, reviews, etc.)
428
+
429
+ Without this, our loaders return the raw VTEX response. Products may show stale prices (from the search index) rather than real-time prices.
430
+
431
+ **Proposed solution:**
432
+
433
+ Create a composable `enrichProducts()` pipeline in apps-start:
434
+
435
+ ```typescript
436
+ type ProductEnricher = (
437
+ products: Product[],
438
+ ctx: { request: Request; segment?: VtexSegment },
439
+ ) => Promise<Product[]>;
440
+
441
+ export function createProductPipeline(...enrichers: ProductEnricher[]): ProductEnricher {
442
+ return async (products, ctx) => {
443
+ let result = products;
444
+ for (const enricher of enrichers) {
445
+ result = await enricher(result, ctx);
446
+ }
447
+ return result;
448
+ };
449
+ }
450
+
451
+ // Built-in enrichers
452
+ export const withSimulation: ProductEnricher = async (products, ctx) => { /* ... */ };
453
+ export const withWishlist: ProductEnricher = async (products, ctx) => { /* ... */ };
454
+ ```
455
+
456
+ Storefronts compose their pipeline in `setup.ts`:
457
+
458
+ ```typescript
459
+ const enrichProducts = createProductPipeline(withSimulation, withWishlist);
460
+ // Wire into commerce loaders
461
+ ```
462
+
463
+ **Location:** `apps-start/vtex/utils/enrichment.ts` + `apps-start/vtex/enrichers/*.ts`
464
+ **Effort:** ~300 LOC
465
+ **Dependencies:** VTEX simulation API client, wishlist API (already in actions)
466
+
467
+ ---
468
+
469
+ ### H. Missing VTEX Loaders/Actions
470
+
471
+ Prioritized by commerce impact:
472
+
473
+ | Capability | Priority | Effort | Notes |
474
+ |-----------|----------|--------|-------|
475
+ | **Top searches** (`topsearches.ts`) | High | ~50 LOC | Autocomplete completeness. Simple IS API call. |
476
+ | **Search validator** (`productSearchValidator.ts`) | Medium | ~80 LOC | Validates search terms, redirects misspellings. |
477
+ | **Legacy brands** (`brands.ts`) | Medium | ~40 LOC | Brand listing pages. Simple catalog API call. |
478
+ | **Page type resolver** (`pageType.ts`) | Medium | ~60 LOC | URL to page type (product, category, brand, search). Already partially covered by our path matching. |
479
+ | **Order placed** (`orderplaced.ts`) | Medium | ~100 LOC | Thank-you page data (order summary, items, totals). |
480
+ | **Review submit** (`review/submit.ts`) | Low | ~60 LOC | Product review submission. |
481
+ | **Notify me** (`notifyme.ts`) | Low | ~50 LOC | Back-in-stock notification signup. |
482
+ | **MasterData search** (`searchDocuments.ts`) | Low | ~80 LOC | Query MasterData V2 documents. Already have create/update. |
483
+ | **Server-side analytics** (`sendEvent.ts`) | Low | ~100 LOC | Send analytics events from server. |
484
+ | **User sessions** (`getUserSessions.ts`) | Low | ~40 LOC | List active sessions. |
485
+ | **Payment token delete** | Low | ~30 LOC | Delete saved payment method. |
486
+
487
+ **Recommendation:** Implement top searches first (it's the most visible gap in the storefront). Then search validator + page type resolver (they improve SEO and search UX). The rest are feature-complete niceties.
488
+
489
+ ---
490
+
491
+ ### I. Missing Shopify Capabilities
492
+
493
+ | Capability | Priority | Effort | Notes |
494
+ |-----------|----------|--------|-------|
495
+ | **Draft order calculate** | Medium | ~100 LOC | B2B/wholesale pricing. Only needed for B2B stores. |
496
+ | **Password/digest cookies** | Medium | ~50 LOC | Storefront password protection (development stores). |
497
+ | **Config loader** | Low | ~30 LOC | Shop config (currencies, languages). |
498
+ | **Proxy routes** | Medium | ~80 LOC | `/apps/*`, `/tools/*` proxy to Shopify. Less critical than VTEX proxy. |
499
+ | **Sitemap handler** | Medium | ~80 LOC | Shopify-specific sitemap (see Section J). |
500
+
501
+ **Recommendation:** Draft order and proxy routes only if/when a Shopify storefront needs them. Password/digest cookies for dev stores.
502
+
503
+ ---
504
+
505
+ ## Part 3 -- SEO / Infrastructure
506
+
507
+ ---
508
+
509
+ ### J. Sitemap Generation -- DONE
510
+
511
+ **What's missing:** No sitemap.xml generation at all. Deco has three handlers:
512
+ - `website/handlers/sitemap.ts` -- Static routes from CMS pages
513
+ - `vtex/handlers/sitemap.ts` -- Pulls product/category URLs from VTEX's sitemap API
514
+ - `shopify/handlers/sitemap.ts` -- Pulls from Shopify's sitemap
515
+
516
+ **Impact:** Search engines can't discover all product/category pages. Major SEO penalty.
517
+
518
+ **Proposed solution:**
519
+
520
+ Two-part system:
521
+
522
+ 1. **Static sitemap from CMS pages** -- Scan `.deco/blocks/pages-*.json` at build time, generate sitemap entries for all page paths. Goes into `@decocms/start`.
523
+
524
+ 2. **Commerce sitemap from VTEX/Shopify** -- Runtime endpoint that fetches product/category URLs from the commerce platform API. Goes into `apps-start/vtex/utils/sitemap.ts` and `apps-start/shopify/utils/sitemap.ts`.
525
+
526
+ The storefront exposes a TanStack Start API route:
527
+
528
+ ```typescript
529
+ // src/routes/sitemap[.]xml.ts (storefront)
530
+ export const APIRoute = createAPIFileRoute("/sitemap.xml")({
531
+ GET: async () => {
532
+ const cmsPages = getCMSPagePaths();
533
+ const vtexPages = await getVtexSitemapUrls(vtexConfig);
534
+ return new Response(generateSitemapXml([...cmsPages, ...vtexPages]), {
535
+ headers: { "Content-Type": "application/xml" },
536
+ });
537
+ },
538
+ });
539
+ ```
540
+
541
+ **Caching:** Sitemap should be cached aggressively (`s-maxage=3600`). Product URLs don't change frequently.
542
+
543
+ **Location:** `deco-start/src/sdk/sitemap.ts` + `apps-start/vtex/utils/sitemap.ts`
544
+ **Effort:** ~200 LOC total
545
+ **Dependencies:** CMS page loader, VTEX catalog API
546
+
547
+ ---
548
+
549
+ ### K. Redirect System -- DONE
550
+
551
+ **What's missing:** Deco has a full redirect pipeline:
552
+ - `website/loaders/redirect.ts` -- Single redirect definition in CMS
553
+ - `website/loaders/redirects.ts` -- Aggregates all redirect routes
554
+ - `website/loaders/redirectsFromCsv.ts` -- Bulk import from CSV file/URL
555
+ - `website/handlers/redirect.ts` -- HTTP redirect handler
556
+
557
+ Our `site.json` already defines `website/loaders/redirects.ts` in routes, but it's in `SKIP_RESOLVE_TYPES` and ignored.
558
+
559
+ **Impact:** Old URLs from platform migrations (Deco v1, other platforms) return 404 instead of redirecting. SEO link equity is lost.
560
+
561
+ **Proposed solution:**
562
+
563
+ 1. **CMS-managed redirects** -- Read redirect blocks from `.deco/blocks/` at startup. Store in a `Map<string, { to: string; type: 301 | 302 }>`.
564
+
565
+ 2. **CSV import** -- Utility to parse CSV files with `from,to,type` columns. Can be loaded from URL (for Google Sheets export) or local file.
566
+
567
+ 3. **Middleware integration** -- Run redirect lookup in TanStack Start middleware, before route matching. Return `Response.redirect()` for matches.
568
+
569
+ ```typescript
570
+ // @decocms/start/sdk/redirects.ts
571
+ export function loadRedirects(blocks: Record<string, any>): Map<string, Redirect> { /* ... */ }
572
+ export function matchRedirect(path: string, redirects: Map<string, Redirect>): Redirect | null { /* ... */ }
573
+ ```
574
+
575
+ **Location:** `deco-start/src/sdk/redirects.ts`
576
+ **Effort:** ~150 LOC
577
+ **Dependencies:** CMS block loader (already exists)
578
+
579
+ ---
580
+
581
+ ### L. SEO Components / JSON-LD -- DONE
582
+
583
+ **What's missing:** Deco has:
584
+ - `commerce/sections/Seo/SeoPDPV2.tsx` -- JSON-LD structured data for product detail pages (Product schema, BreadcrumbList, offers, reviews)
585
+ - `commerce/sections/Seo/SeoPLPV2.tsx` -- JSON-LD for product listing pages (ItemList, CollectionPage)
586
+ - `website/components/Seo.tsx` -- General SEO meta tag injection
587
+ - `website/components/_seo/*` -- Social media preview cards (Open Graph, Twitter Cards, Discord, etc.)
588
+
589
+ These are in `SKIP_RESOLVE_TYPES` in our resolver, meaning the CMS has them but we don't render them.
590
+
591
+ **Impact:** Product pages lack structured data, reducing rich snippet visibility in search results (star ratings, price, availability).
592
+
593
+ **Proposed solution:**
594
+
595
+ Create SEO utility components in `deco-start` or `apps-start`:
596
+
597
+ ```typescript
598
+ // apps-start/commerce/components/JsonLd.tsx
599
+ export function ProductJsonLd({ product }: { product: Product }) {
600
+ const jsonld = productToJsonLd(product);
601
+ return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonld) }} />;
602
+ }
603
+
604
+ export function PLPJsonLd({ page }: { page: ProductListingPage }) { /* ... */ }
605
+ export function BreadcrumbJsonLd({ breadcrumb }: { breadcrumb: BreadcrumbList }) { /* ... */ }
606
+ ```
607
+
608
+ For meta tags, use TanStack Router's built-in `Meta` component via route `meta()` function:
609
+
610
+ ```typescript
611
+ export const Route = createFileRoute("/product/$slug")({
612
+ meta: ({ loaderData }) => [
613
+ { title: loaderData.product.name },
614
+ { name: "description", content: loaderData.product.description },
615
+ { property: "og:title", content: loaderData.product.name },
616
+ { property: "og:image", content: loaderData.product.image[0]?.url },
617
+ ],
618
+ });
619
+ ```
620
+
621
+ **Location:** `apps-start/commerce/components/` for JSON-LD, TanStack Router `meta()` for head tags
622
+ **Effort:** ~200 LOC
623
+ **Dependencies:** Commerce types (already exist)
624
+
625
+ ---
626
+
627
+ ### M. Deferred / Lazy Section Rendering
628
+
629
+ **What's missing:** Deco has two rendering optimization sections:
630
+
631
+ 1. **`Rendering/Deferred.tsx`** -- Streams the section HTML after the initial page shell loads. Uses React Suspense on the server to defer heavy sections.
632
+ 2. **`Rendering/Lazy.tsx`** -- Uses intersection observer (`hx-trigger="intersect once"`) to lazy-load sections when they scroll into view.
633
+
634
+ **What we have:** `DecoPageRenderer` already uses `React.Suspense` with `React.lazy()` for code splitting. But we don't have intersection-based lazy loading for below-the-fold content.
635
+
636
+ **Proposed solution:**
637
+
638
+ Create a `<LazySection>` wrapper that uses IntersectionObserver to defer rendering:
639
+
640
+ ```tsx
641
+ export function LazySection({ children, fallback, rootMargin = "200px" }: Props) {
642
+ const [isVisible, setVisible] = useState(false);
643
+ const ref = useRef<HTMLDivElement>(null);
644
+
645
+ useEffect(() => {
646
+ const observer = new IntersectionObserver(
647
+ ([entry]) => { if (entry.isIntersecting) { setVisible(true); observer.disconnect(); } },
648
+ { rootMargin },
649
+ );
650
+ if (ref.current) observer.observe(ref.current);
651
+ return () => observer.disconnect();
652
+ }, []);
653
+
654
+ return <div ref={ref}>{isVisible ? children : fallback}</div>;
655
+ }
656
+ ```
657
+
658
+ The `DecoPageRenderer` could automatically wrap sections below a configurable fold threshold (e.g., index > 3) with `LazySection`.
659
+
660
+ **Streaming (Deferred):** TanStack Start already supports streaming SSR via React Suspense. Sections loaded with `React.lazy()` naturally stream. No additional work needed for this.
661
+
662
+ **Location:** `deco-start/src/hooks/LazySection.tsx`
663
+ **Effort:** ~80 LOC
664
+ **Dependencies:** None
665
+
666
+ ---
667
+
668
+ ## Part 4 -- Schema / Admin
669
+
670
+ ---
671
+
672
+ ### N. Schema Generation Improvements -- DONE
673
+
674
+ All JSDoc tags, `$` prefix filtering, and widget type detection are implemented.
675
+
676
+ Remaining: Loader/action schema generation (deferred -- needs manifest format design).
677
+
678
+ **Location:** `deco-start/scripts/generate-schema.ts`
679
+
680
+ ---
681
+
682
+ ### N.1 Admin Schema Composition Architecture -- DONE
683
+
684
+ **Problem:** The admin's properties form showed "Unsupported field schema for field root: Unknown field type undefined" because:
685
+ 1. Framework-managed block types (pages) were hardcoded in the build-time schema generator, creating a coupling between the generator and the framework
686
+ 2. Base64 keys used unpadded encoding (`.replace(/=+$/, "")`) while the admin uses `btoa()` which produces padded output, causing definition lookup failures
687
+ 3. The admin cached stale `/_meta` responses, so schema changes didn't take effect
688
+
689
+ **Solution: Runtime schema composition via `composeMeta()`**
690
+
691
+ Framework-level schemas (pages, and future block types like loaders/matchers) are now defined in `deco-start/src/admin/schema.ts` and injected at runtime when `setMetaData()` is called, rather than being generated at build time.
692
+
693
+ Data flow:
694
+ ```
695
+ [generate-schema.ts] [setup.ts] [composeMeta()]
696
+ Scans src/sections/ ---> Imports meta.gen.json --> Injects page schema,
697
+ Produces section-only Calls setMetaData() merges definitions,
698
+ meta.gen.json populates pages root
699
+ | |
700
+ v v
701
+ Section schemas only Full schema with pages +
702
+ (pages: empty anyOf) sections + Resolvable
703
+ ```
704
+
705
+ Key changes:
706
+ - `scripts/generate-schema.ts` -- Only generates section schemas; `toBase64()` now produces standard padded Base64
707
+ - `src/admin/schema.ts` (NEW) -- Defines `MetaResponse` type, `composeMeta()`, `buildPageSchema()`; framework owns its block schemas
708
+ - `src/admin/meta.ts` -- `setMetaData()` calls `composeMeta()` before storing; ETag uses content-based DJB2 hash; etag included in JSON body for admin cache busting
709
+ - `src/admin/index.ts` -- Exports `composeMeta` and `MetaResponse`
710
+
711
+ **Location:** `deco-start/src/admin/schema.ts`, `deco-start/src/admin/meta.ts`, `deco-start/scripts/generate-schema.ts`
712
+
713
+ ---
714
+
715
+ ### O. Admin Live Preview / Hot Reload
716
+
717
+ **What's missing:**
718
+ - `/deco/preview` -- WebSocket-based live preview where the admin sends props and gets rendered HTML back in real time
719
+ - `/deco/reload` -- Accept a new decofile via POST without redeploying
720
+
721
+ **Impact:** CMS editors can't see changes in real-time. Every content change requires a full page refresh or redeploy.
722
+
723
+ **Proposed solution:**
724
+
725
+ **Preview:** Our `/deco/render` already renders a single section with given props. The admin currently refreshes the iframe on each change. A WebSocket upgrade would allow the admin to push props and receive HTML streams, reducing latency.
726
+
727
+ However, the admin client would need to implement the WebSocket protocol. This is a coordinated effort between `deco-start` and the admin frontend.
728
+
729
+ **Hot reload:** More impactful. A `/deco/reload` endpoint that:
730
+ 1. Accepts a new decofile JSON via POST
731
+ 2. Calls `setBlocks(newBlocks)` to update the in-memory state
732
+ 3. Subsequent page renders use the new blocks
733
+
734
+ This already partially works -- `setBlocks()` exists. The gap is the HTTP endpoint with authentication.
735
+
736
+ ```typescript
737
+ // New admin endpoint
738
+ export function handleReload(request: Request): Response {
739
+ const token = request.headers.get("Authorization");
740
+ if (!isValidReloadToken(token)) return new Response("Unauthorized", { status: 401 });
741
+ const newBlocks = await request.json();
742
+ setBlocks(newBlocks);
743
+ return new Response("OK");
744
+ }
745
+ ```
746
+
747
+ **Location:** `deco-start/src/admin/reload.ts`
748
+ **Effort:** ~50 LOC for reload, ~300 LOC for WebSocket preview
749
+ **Dependencies:** Admin frontend changes for WebSocket preview
750
+
751
+ ---
752
+
753
+ ## Part 5 -- Components / Utilities
754
+
755
+ ---
756
+
757
+ ### P. Optimized Media Components
758
+
759
+ **What's missing:** Deco provides `Image.tsx`, `Picture.tsx`, `Video.tsx` with:
760
+ - CDN-aware image transforms (resize, format conversion via URL params)
761
+ - Responsive `srcset` generation
762
+ - Lazy loading with blur placeholder
763
+ - Aspect ratio enforcement (prevents CLS)
764
+ - Width/height attributes for layout stability
765
+
766
+ **Proposed solution:**
767
+
768
+ Create these as React components in `apps-start/commerce/components/` (since they're used across commerce storefronts):
769
+
770
+ ```tsx
771
+ interface ImageProps {
772
+ src: string;
773
+ width: number;
774
+ height: number;
775
+ alt: string;
776
+ sizes?: string;
777
+ loading?: "lazy" | "eager";
778
+ fetchPriority?: "high" | "low" | "auto";
779
+ cdn?: "deco" | "vtex" | "shopify" | "cloudflare";
780
+ }
781
+
782
+ export function Image({ src, width, height, cdn = "deco", ...props }: ImageProps) {
783
+ const optimizedSrc = buildCDNUrl(src, { width, height }, cdn);
784
+ const srcSet = buildSrcSet(src, width, cdn);
785
+ return <img src={optimizedSrc} srcSet={srcSet} width={width} height={height} {...props} />;
786
+ }
787
+ ```
788
+
789
+ The CDN URL builder would support multiple image CDNs:
790
+ - `decocache.com` assets -> Deco's image transform API
791
+ - VTEX image URLs -> VTEX's built-in resize params
792
+ - Shopify image URLs -> Shopify's CDN transforms
793
+ - Generic -> Cloudflare Image Resizing (if available)
794
+
795
+ **Location:** `apps-start/commerce/components/Image.tsx`, `Picture.tsx`, `Video.tsx`
796
+ **Effort:** ~250 LOC total
797
+ **Dependencies:** None
798
+
799
+ ---
800
+
801
+ ### Q. Client-side State Hooks
802
+
803
+ **What's missing:** Deco provides `useCart`, `useUser`, `useWishlist`, `useAutocomplete` as client-side reactive state hooks. These manage loading states, optimistic updates, and cache invalidation for their respective domains.
804
+
805
+ **Proposed solution:**
806
+
807
+ Use TanStack Query as the state management layer (we already have it in the stack). Create hook factories in `apps-start`:
808
+
809
+ ```typescript
810
+ // apps-start/vtex/hooks/useCart.ts
811
+ export function useCart() {
812
+ const query = useQuery({
813
+ queryKey: ["cart"],
814
+ queryFn: () => invokeLoader("vtex/loaders/cart.ts"),
815
+ staleTime: 0, // always fresh
816
+ });
817
+
818
+ const addItem = useMutation({
819
+ mutationFn: (item) => invokeAction("vtex/actions/checkout/addItems", item),
820
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["cart"] }),
821
+ });
822
+
823
+ return { cart: query.data, isLoading: query.isLoading, addItem, /* ... */ };
824
+ }
825
+ ```
826
+
827
+ This pattern leverages TanStack Query's built-in SWR, optimistic updates, cache invalidation, and loading states. No need to reinvent state management.
828
+
829
+ **Location:** `apps-start/vtex/hooks/useCart.ts`, `useUser.ts`, `useWishlist.ts`
830
+ **Effort:** ~150 LOC per hook
831
+ **Dependencies:** TanStack Query, invoke proxy
832
+
833
+ ---
834
+
835
+ ### R. Missing Matchers -- PARTIALLY DONE
836
+
837
+ **Current:** 6 matchers (always, never, device, random, utm, posthog) + 6 new (cookie, cron/date, host, pathname, queryString)
838
+ **Deco has:** 16 matchers
839
+
840
+ Prioritized by real-world usage:
841
+
842
+ | Matcher | Priority | Effort | Use case |
843
+ |---------|----------|--------|----------|
844
+ | **cookie** | High | ~30 LOC | Match by specific cookie value (e.g., loyalty program tier) |
845
+ | **pathname** | High | ~30 LOC | Match by URL pattern (e.g., show banner only on `/sale/*`) |
846
+ | **queryString** | Medium | ~30 LOC | Match by query params (e.g., `?ref=partner1`) |
847
+ | **date** | Medium | ~40 LOC | Time-based content (Black Friday banner starts Nov 29) |
848
+ | **host** | Medium | ~20 LOC | Multi-domain stores (different content per domain) |
849
+ | **multi** | Medium | ~40 LOC | AND/OR combinator for composing matchers |
850
+ | **negate** | Medium | ~15 LOC | Invert any matcher |
851
+ | **cron** | Low | ~60 LOC | Scheduled content swaps (weekend vs weekday) |
852
+ | **location** | Low | ~50 LOC | Geo-targeting (requires IP geolocation, CF provides this) |
853
+ | **environment** | Low | ~20 LOC | Prod vs staging content |
854
+ | **site** | Low | ~15 LOC | Multi-site setup |
855
+ | **userAgent** | Low | ~30 LOC | Bot detection, specific browser targeting |
856
+
857
+ **Recommendation:** Implement cookie, pathname, queryString, date, multi, and negate. These cover 95% of real-world CMS personalization needs. The rest are niche.
858
+
859
+ **Location:** `deco-start/src/cms/resolve.ts` (add cases to `evaluateMatcher`)
860
+ **Effort:** ~200 LOC for the priority set
861
+
862
+ ---
863
+
864
+ ### S. Minor Utilities
865
+
866
+ These are small, self-contained gaps:
867
+
868
+ #### S.1 `useScript` minification
869
+
870
+ Deco's `useScript` LRU-caches terser-minified output. Ours concatenates raw strings.
871
+
872
+ **Recommendation:** Skip. Modern bundlers already minify the output. The inline scripts are small (event handlers, analytics snippets). The minification overhead at runtime isn't worth it for our deployment model (Cloudflare Workers have limited CPU time).
873
+
874
+ #### S.2 `readFromStream` (SSE)
875
+
876
+ Deco's client SDK can read streaming invoke responses via Server-Sent Events.
877
+
878
+ **Recommendation:** Defer. We don't have streaming loaders today. When we need real-time data (e.g., stock updates, price changes), implement SSE support in the invoke proxy.
879
+
880
+ #### S.3 `setCSPHeaders` for admin embedding -- DONE
881
+
882
+ Sets `Content-Security-Policy: frame-ancestors` allowing the deco admin origin to embed the storefront.
883
+
884
+ **Recommendation:** Implement. Small effort, needed for admin iframe preview to work without browser blocking it.
885
+
886
+ ```typescript
887
+ export function setCSPHeaders(response: Response, adminOrigin: string = "https://admin.deco.cx"): void {
888
+ response.headers.set(
889
+ "Content-Security-Policy",
890
+ `frame-ancestors 'self' ${adminOrigin} https://localhost:*`,
891
+ );
892
+ }
893
+ ```
894
+
895
+ **Effort:** ~10 LOC. Add to `deco-start/src/admin/cors.ts`.
896
+
897
+ #### S.4 Cache-Control merge -- DONE
898
+
899
+ `mergeCacheControl(h1, h2)` takes the minimum of numeric values (most restrictive wins). Useful when multiple middleware layers set cache headers.
900
+
901
+ **Recommendation:** Implement. Small, useful utility.
902
+
903
+ ```typescript
904
+ export function mergeCacheControl(a: CacheControl, b: CacheControl): CacheControl {
905
+ return {
906
+ "max-age": Math.min(a["max-age"] ?? Infinity, b["max-age"] ?? Infinity),
907
+ "s-maxage": Math.min(a["s-maxage"] ?? Infinity, b["s-maxage"] ?? Infinity),
908
+ // ... etc, taking most restrictive
909
+ };
910
+ }
911
+ ```
912
+
913
+ **Effort:** ~40 LOC. Add to `deco-start/src/sdk/cacheHeaders.ts`.
914
+
915
+ #### S.5 `wrapCaughtErrors` (deferred error proxy) -- DONE
916
+
917
+ Catches loader errors and wraps them in a Proxy that defers the throw to render time, so `ErrorFallback` handles it.
918
+
919
+ **Recommendation:** Implement. Improves resilience -- a failing loader doesn't crash the page, it just shows the section's error fallback.
920
+
921
+ ```typescript
922
+ export function wrapCaughtError(error: Error): Record<string, unknown> {
923
+ return new Proxy({}, {
924
+ get(_, prop) {
925
+ if (prop === "__isWrappedError") return true;
926
+ if (prop === "__error") return error;
927
+ throw error;
928
+ },
929
+ });
930
+ }
931
+ ```
932
+
933
+ **Effort:** ~30 LOC. Add to `deco-start/src/cms/resolve.ts`.
934
+
935
+ #### S.6 UTM param stripping for cache keys -- DONE
936
+
937
+ `useSection()` strips 20+ marketing query params for cache stability. Even without partial rendering, this is useful for page-level caching.
938
+
939
+ **Recommendation:** Implement in `workerEntry.ts` cache key builder.
940
+
941
+ Known params to strip: `fbclid`, `gclid`, `gclsrc`, `dclid`, `gbraid`, `wbraid`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `mc_cid`, `mc_eid`, `_hsenc`, `_hsmi`, `hsCtaTracking`, `__hsfp`, `__hssc`, `__hstc`, `msclkid`, `yclid`, `igshid`, `twclid`, `ttclid`.
942
+
943
+ **Effort:** ~20 LOC. Add to `deco-start/src/sdk/workerEntry.ts`.
944
+
945
+ ---
946
+
947
+ ## Part 6 -- Prioritized Roadmap
948
+
949
+ ### Tier 0 -- Blocking for production -- ALL DONE
950
+
951
+ | # | Gap | Section | Status | Location |
952
+ |---|-----|---------|--------|----------|
953
+ | 1 | VTEX proxy routes | F | **DONE** | `apps-start/vtex/utils/proxy.ts` |
954
+ | 2 | VTEX middleware (segment + login-aware cache + vtexId) | E | **DONE** | `apps-start/vtex/middleware.ts` + `apps-start/vtex/utils/vtexId.ts` |
955
+ | 3 | Redirect system | K | **DONE** | `deco-start/src/sdk/redirects.ts` |
956
+ | 4 | Sitemap generation | J | **DONE** | `deco-start/src/sdk/sitemap.ts` + `apps-start/vtex/utils/sitemap.ts` |
957
+ | 5 | SEO JSON-LD components | L | **DONE** | `apps-start/commerce/components/JsonLd.tsx` |
958
+
959
+ ### Tier 1 -- Important for quality -- ALL DONE
960
+
961
+ | # | Gap | Section | Status | Location |
962
+ |---|-----|---------|--------|----------|
963
+ | 6 | Segment-aware cache keys | C | **DONE** | `deco-start/src/sdk/workerEntry.ts` (SegmentKey + buildSegment option, UTM stripping, logged-in bypass) |
964
+ | 7 | Priority matchers (cookie, pathname, cron/date, host, queryString) | R | **DONE** | `deco-start/src/matchers/builtins.ts` |
965
+ | 8 | Top searches + search validator | H | **DONE** | `apps-start/vtex/loaders/search.ts` (already existed) |
966
+ | 9 | Product extension pipeline | G | **DONE** | `apps-start/vtex/utils/enrichment.ts` (createProductPipeline, withSimulation, withWishlist) |
967
+ | 10 | LazySection component | M | **DONE** | `deco-start/src/hooks/LazySection.tsx` |
968
+ | 11 | UTM param stripping + canonical URL utils | S.6 | **DONE** | `deco-start/src/sdk/urlUtils.ts` |
969
+ | 12 | `wrapCaughtErrors` for resilient rendering | S.5 | **DONE** | `deco-start/src/sdk/wrapCaughtErrors.ts` |
970
+ | 13 | CSP headers for admin embedding | S.3 | **DONE** | `deco-start/src/sdk/csp.ts` |
971
+ | 14 | Cache-Control merge utility | S.4 | **DONE** | `deco-start/src/sdk/mergeCacheControl.ts` |
972
+
973
+ ### Tier 2 -- DX and completeness -- ALL DONE
974
+
975
+ | # | Gap | Section | Status | Location |
976
+ |---|-----|---------|--------|----------|
977
+ | 15 | Schema gen: all JSDoc tags + `$` filtering + widget detection | N | **DONE** | `deco-start/scripts/generate-schema.ts` (20+ JSDoc tags, $ prefix filtering, widget type detection) |
978
+ | 16 | Client-side hooks (useCart, useUser, useWishlist) | Q | **DONE** | `apps-start/vtex/hooks/useCart.ts`, `useUser.ts`, `useWishlist.ts` |
979
+ | 17 | Optimized Image/Picture components | P | **DONE** | `apps-start/commerce/components/Image.tsx` (Image + Picture with multi-CDN support) |
980
+ | 18 | RequestContext via AsyncLocalStorage | D | **DONE** | `deco-start/src/sdk/requestContext.ts` |
981
+ | 19 | Commerce loader request context | A (rec. 1) | **DONE** | `deco-start/src/cms/resolve.ts` (CommerceLoaderContext + MatcherContext._request) |
982
+ | 20 | Remaining VTEX loaders (brands, pageType) | H | **DONE** | `apps-start/vtex/loaders/brands.ts`, `pageType.ts` (orderPlaced already in orders.ts) |
983
+ | 21 | Admin schema composition (composeMeta) | N.1 | **DONE** | `deco-start/src/admin/schema.ts` (runtime composition, padded Base64, content-hash ETag) |
984
+
985
+ ### Tier 3 -- Future / deferred
986
+
987
+ | # | Gap | Section | Dependencies |
988
+ |---|-----|---------|-------------|
989
+ | 22 | Loader/action schema generation | N | Manifest format design |
990
+ | 23 | Admin live preview (WebSocket) | O | Admin frontend changes |
991
+ | 24 | Hot reload endpoint | O | Auth token system |
992
+ | 25 | SSE streaming reader | S.2 | Streaming loaders |
993
+ | 26 | `sectionMiddleware` / transformProps | A (rec. 2) | Registry changes |
994
+ | 27 | Shopify draft order, proxy routes | I | Shopify store demand |
995
+ | 28 | Geo/location matchers | R | IP geolocation service |
996
+
997
+ ---
998
+
999
+ ## Intentional Divergences (not gaps)
1000
+
1001
+ These are architectural choices, not missing features:
1002
+
1003
+ | Deco | deco-start | Why it's intentional |
1004
+ |------|-----------|---------------------|
1005
+ | Section exports `loader()` + `action()` | Sections are pure React components | Idiomatic React/TanStack; loaders wired externally |
1006
+ | HTMX partials (`hx-get`, `f-partial`) | TanStack Query + Router navigation | React reconciliation model; no DOM swap hacks |
1007
+ | Fresh islands (selective hydration) | Full React SPA with SSR | TanStack Start's model; Suspense for code splitting |
1008
+ | Deno runtime | Node.js on Cloudflare Workers | Deployment target choice |
1009
+ | Import maps for app composition | npm packages | Standard Node.js package resolution |
1010
+ | Daemon/CRDT/tunnel dev server | Vite HMR + TanStack devtools | Modern frontend DX stack |
1011
+ | Global fetch monkey-patch | Explicit instrumented fetch | Predictable; no hidden behavior |
1012
+ | `RequestContext.framework = "htmx"` | Always React | Single rendering framework |
1013
+ | Per-loader Vary system | Segment-based cache keys | Simpler; Cloudflare Cache API handles differentiation |