@decocms/start 0.38.0 → 0.40.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.
- package/.agents/skills/deco-migrate-script/SKILL.md +434 -0
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +382 -0
- package/.agents/skills/deco-to-tanstack-migration/references/admin-cms.md +154 -0
- package/{.cursor/skills/deco-async-rendering-site-guide/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/async-rendering.md} +296 -31
- package/.agents/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.agents/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.agents/skills/deco-to-tanstack-migration/references/css-styling.md +156 -0
- package/.agents/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.agents/skills/deco-to-tanstack-migration/references/gotchas.md +13 -0
- package/{.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/hydration-fixes.md} +139 -4
- package/.agents/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/{.cursor/skills/deco-islands-migration/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/islands.md} +0 -14
- package/.agents/skills/deco-to-tanstack-migration/references/jsx-migration.md +80 -0
- package/.agents/skills/deco-to-tanstack-migration/references/matchers.md +1064 -0
- package/{.cursor/skills/deco-tanstack-navigation/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/navigation.md} +1 -16
- package/.agents/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.agents/skills/deco-to-tanstack-migration/references/react-hooks-patterns.md +142 -0
- package/.agents/skills/deco-to-tanstack-migration/references/react-signals-state.md +72 -0
- package/{.cursor/skills/deco-tanstack-search/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/search.md} +1 -13
- package/.agents/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/{.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/storefront-patterns.md} +1 -137
- package/.agents/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +165 -0
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +209 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/README.md +45 -0
- package/package.json +2 -1
- package/src/admin/index.ts +2 -0
- package/src/admin/invoke.ts +53 -5
- package/src/admin/setup.ts +7 -1
- package/src/apps/autoconfig.ts +50 -72
- package/src/sdk/invoke.ts +123 -12
- package/src/sdk/requestContext.ts +42 -0
- package/src/sdk/setupApps.ts +211 -0
- package/src/sdk/workerEntry.ts +6 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +0 -270
|
@@ -1,13 +1,278 @@
|
|
|
1
|
+
# Async Rendering: Architecture & Site Implementation
|
|
2
|
+
|
|
3
|
+
## Part 1: Architecture
|
|
4
|
+
|
|
5
|
+
Internal documentation for the async section rendering system in `@decocms/start`.
|
|
6
|
+
|
|
7
|
+
### When to Use This Reference
|
|
8
|
+
|
|
9
|
+
- Debugging why a section is or isn't being deferred
|
|
10
|
+
- Understanding the full request flow from CMS resolution to on-scroll loading
|
|
11
|
+
- Extending the async rendering system (new cache tiers, new deferral strategies)
|
|
12
|
+
- Fixing issues with deferred section data resolution
|
|
13
|
+
- Understanding how bot detection and SEO safety work
|
|
14
|
+
- Working on `@decocms/start` framework code
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
### Problem Solved
|
|
19
|
+
|
|
20
|
+
TanStack Start serializes all `loaderData` as JSON in a `<script>` tag for client-side hydration. When a CMS page has 20+ sections with commerce data, the HTML payload becomes enormous (8+ MB on some pages). The root cause: `resolveDecoPage` fully resolves ALL sections, and TanStack Start embeds everything.
|
|
21
|
+
|
|
22
|
+
### Architecture Overview
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Request → resolveDecoPage()
|
|
26
|
+
├─ resolveSectionsList() → unwrap flags/blocks to get raw section array
|
|
27
|
+
├─ shouldDeferSection() → classify each section as eager or deferred
|
|
28
|
+
│ ├─ Eager: resolveRawSection() → full CMS + commerce resolution
|
|
29
|
+
│ └─ Deferred: resolveSectionShallow() → component key + raw CMS props only
|
|
30
|
+
├─ runSectionLoaders() → enrich eager sections (server loaders)
|
|
31
|
+
└─ Return { resolvedSections, deferredSections }
|
|
32
|
+
|
|
33
|
+
Client render → DecoPageRenderer
|
|
34
|
+
├─ mergeSections() → interleave eager + deferred by original index
|
|
35
|
+
├─ Eager: <Suspense><LazyComponent .../></Suspense>
|
|
36
|
+
└─ Deferred: <DeferredSectionWrapper>
|
|
37
|
+
├─ preloadSectionModule() → get LoadingFallback early
|
|
38
|
+
├─ Render skeleton (custom LoadingFallback or generic)
|
|
39
|
+
├─ IntersectionObserver(rootMargin: 300px)
|
|
40
|
+
└─ On intersect: loadDeferredSection serverFn
|
|
41
|
+
├─ resolveDeferredSection() → resolve __resolveType refs in rawProps
|
|
42
|
+
├─ runSingleSectionLoader() → enrich with server loader
|
|
43
|
+
└─ Return ResolvedSection → render real component with fade-in
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Deferral Strategy: CMS Lazy.tsx as Source of Truth
|
|
49
|
+
|
|
50
|
+
#### How it works now (respectCmsLazy)
|
|
51
|
+
|
|
52
|
+
The deferral decision is driven by **CMS editor choices**, not a global index threshold:
|
|
53
|
+
|
|
54
|
+
1. **`respectCmsLazy: true`** (default) — a section is deferred if and only if it's wrapped in `website/sections/Rendering/Lazy.tsx` in the CMS page JSON
|
|
55
|
+
2. **`foldThreshold`** (default `Infinity`) — fallback for sections NOT wrapped in Lazy; with default `Infinity`, non-wrapped sections are always eager
|
|
56
|
+
3. **`alwaysEager`** — section keys that override all deferral (Header, Footer, Theme, etc.)
|
|
57
|
+
|
|
58
|
+
#### Why this approach
|
|
59
|
+
|
|
60
|
+
The previous `foldThreshold` approach deferred sections by index position, ignoring editor intent. This caused:
|
|
61
|
+
- Sections that editors wanted eager getting deferred
|
|
62
|
+
- No control per-page (threshold was global)
|
|
63
|
+
- Homepage with 12 sections marked Lazy in CMS showing 0 deferred
|
|
64
|
+
|
|
65
|
+
Now editors control deferral by wrapping sections in `Lazy.tsx` in the CMS admin, and the framework respects that.
|
|
66
|
+
|
|
67
|
+
#### `isCmsLazyWrapped(section)` in `resolve.ts`
|
|
68
|
+
|
|
69
|
+
Detects whether a section is wrapped in `website/sections/Rendering/Lazy.tsx`, either:
|
|
70
|
+
- Directly: `section.__resolveType === "website/sections/Rendering/Lazy.tsx"`
|
|
71
|
+
- Via named block: `section.__resolveType` references a block whose `__resolveType` is `"website/sections/Rendering/Lazy.tsx"`
|
|
72
|
+
|
|
73
|
+
#### `shouldDeferSection(section, flatIndex, cfg, isBotReq)`
|
|
74
|
+
|
|
75
|
+
Updated decision logic:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
1. Bot request? → EAGER (SEO safety)
|
|
79
|
+
2. No __resolveType? → EAGER (can't classify)
|
|
80
|
+
3. Is multivariate flag? → EAGER (requires runtime evaluation)
|
|
81
|
+
4. resolveFinalSectionKey() → walk block refs + Lazy wrappers to find final component
|
|
82
|
+
5. In alwaysEager set? → EAGER
|
|
83
|
+
6. isLayoutSection()? → EAGER
|
|
84
|
+
7. respectCmsLazy && isCmsLazyWrapped(section)? → DEFER
|
|
85
|
+
8. flatIndex >= foldThreshold? → DEFER (fallback, only if not wrapped)
|
|
86
|
+
9. Otherwise → EAGER
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Files and Their Roles
|
|
92
|
+
|
|
93
|
+
| File | Layer | Role |
|
|
94
|
+
|------|-------|------|
|
|
95
|
+
| `src/cms/resolve.ts` | Server | Types, config, eager/deferred split, CMS Lazy detection, shallow resolution, full deferred resolution |
|
|
96
|
+
| `src/cms/sectionLoaders.ts` | Server | Section loader registry, layout cache, SWR cacheable sections, `runSingleSectionLoader` |
|
|
97
|
+
| `src/cms/registry.ts` | Shared | Section component registry, `preloadSectionModule` for early LoadingFallback |
|
|
98
|
+
| `src/routes/cmsRoute.ts` | Server | `loadCmsPage`, `loadCmsHomePage`, `loadDeferredSection` server functions |
|
|
99
|
+
| `src/hooks/DecoPageRenderer.tsx` | Client | Merge, render eager/deferred, `DeferredSectionWrapper`, dev warnings |
|
|
100
|
+
| `src/cms/index.ts` | Barrel | Re-exports all public types and functions |
|
|
101
|
+
| `src/routes/index.ts` | Barrel | Re-exports route helpers including `loadDeferredSection` |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Server-Side: Eager/Deferred Split
|
|
106
|
+
|
|
107
|
+
#### Entry point: `resolveDecoPage()` in `resolve.ts`
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
resolveDecoPage(targetPath, matcherCtx)
|
|
111
|
+
1. findPageByPath(targetPath) → { page, params }
|
|
112
|
+
2. Get raw sections array:
|
|
113
|
+
- If page.sections is Array → use directly
|
|
114
|
+
- If page.sections is wrapped (multivariate flag, block ref) → resolveSectionsList()
|
|
115
|
+
3. For each raw section:
|
|
116
|
+
- If shouldDeferSection() → resolveSectionShallow() → DeferredSection
|
|
117
|
+
- Else → resolveRawSection() (full resolution) → ResolvedSection[]
|
|
118
|
+
4. Return { resolvedSections, deferredSections }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `resolveSectionsList(value, rctx, depth)`
|
|
122
|
+
|
|
123
|
+
Resolves **only the outer wrapper** around the sections array. Handles multivariate flags, named block references, and `resolved` type wrappers. Extracts the raw section array WITHOUT resolving individual section commerce loaders.
|
|
124
|
+
|
|
125
|
+
#### `resolveFinalSectionKey(section)`
|
|
126
|
+
|
|
127
|
+
Walks block reference chain and unwraps `Lazy` wrappers to find the final registered section component key:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
"Header - 01" (named block)
|
|
131
|
+
→ { __resolveType: "website/sections/Rendering/Lazy.tsx", section: {...} }
|
|
132
|
+
→ { __resolveType: "site/sections/Header/Header.tsx", ...props }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Returns `"site/sections/Header/Header.tsx"`, checked against `alwaysEager` and `isLayoutSection`.
|
|
136
|
+
|
|
137
|
+
#### `resolveSectionShallow(section)`
|
|
138
|
+
|
|
139
|
+
Synchronously follows block refs and unwraps Lazy to extract `component` (final key) and `rawProps` (CMS props as-is). No API calls, no async.
|
|
140
|
+
|
|
141
|
+
#### `resolveDeferredSection(component, rawProps, pagePath, matcherCtx)`
|
|
142
|
+
|
|
143
|
+
Called when client requests a deferred section. Runs full resolution:
|
|
144
|
+
1. `resolveProps(rawProps, rctx)` — resolves all nested `__resolveType` references
|
|
145
|
+
2. `normalizeNestedSections(resolvedProps)` — converts nested sections to `{ Component, props }`
|
|
146
|
+
3. Returns `ResolvedSection` ready for `runSingleSectionLoader`
|
|
147
|
+
|
|
1
148
|
---
|
|
2
|
-
|
|
3
|
-
|
|
149
|
+
|
|
150
|
+
### Server-Side: Section Caching
|
|
151
|
+
|
|
152
|
+
#### Three cache tiers in `sectionLoaders.ts`
|
|
153
|
+
|
|
154
|
+
**Tier 1: Layout sections** (Header, Footer, Theme)
|
|
155
|
+
- 5-minute TTL, in-flight dedup, registered via `registerLayoutSections`
|
|
156
|
+
|
|
157
|
+
**Tier 2: Cacheable sections** (ProductShelf, FAQ)
|
|
158
|
+
- Configurable TTL via `registerCacheableSections`, SWR semantics, LRU eviction at 200 entries
|
|
159
|
+
- Cache key: `component::djb2Hash(JSON.stringify(props))`
|
|
160
|
+
|
|
161
|
+
**Tier 3: Regular sections** — No caching, always fresh.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Client-Side: DeferredSectionWrapper
|
|
166
|
+
|
|
167
|
+
#### Lifecycle
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
1. Mount (stableKey = pagePath + component + index)
|
|
171
|
+
├─ preloadSectionModule(component) → extract LoadingFallback
|
|
172
|
+
└─ Render skeleton (custom or generic DefaultSectionFallback)
|
|
173
|
+
|
|
174
|
+
2. IntersectionObserver (rootMargin: "300px")
|
|
175
|
+
└─ On intersect (once):
|
|
176
|
+
├─ loadDeferredSection serverFn
|
|
177
|
+
├─ On success: render <LazyComponent .../> with fade-in
|
|
178
|
+
└─ On error: render ErrorFallback or null
|
|
179
|
+
|
|
180
|
+
3. SPA navigation: stableKey changes → reset state (triggered, section, error)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Key: stableKey for SPA navigation
|
|
184
|
+
|
|
185
|
+
`DeferredSectionWrapper` uses `pagePath + component + index` as a stable key. When the route changes, this key changes, forcing React to remount the wrapper and reset all internal state. This prevents deferred sections from a previous page being "stuck" in a triggered state.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### Bot Detection (SEO Safety)
|
|
190
|
+
|
|
191
|
+
`isBot(userAgent)` regex detects search engine crawlers. When detected, ALL sections are resolved eagerly — `deferredSections` is empty.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### Types
|
|
196
|
+
|
|
197
|
+
#### `AsyncRenderingConfig`
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
interface AsyncRenderingConfig {
|
|
201
|
+
respectCmsLazy: boolean; // Default true — use Lazy.tsx wrappers as deferral source
|
|
202
|
+
foldThreshold: number; // Default Infinity — fallback for non-wrapped sections
|
|
203
|
+
alwaysEager: Set<string>; // Section keys that must always be eager
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### `DeferredSection`
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface DeferredSection {
|
|
211
|
+
component: string;
|
|
212
|
+
key: string;
|
|
213
|
+
index: number;
|
|
214
|
+
rawProps: Record<string, unknown>;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### Edge Cases and Gotchas
|
|
221
|
+
|
|
222
|
+
#### 1. CMS Lazy.tsx is the source of truth
|
|
223
|
+
Editors wrap sections in `website/sections/Rendering/Lazy.tsx` in the CMS admin. The framework detects this via `isCmsLazyWrapped()` and defers those sections. Sections NOT wrapped are eager (with `foldThreshold: Infinity`).
|
|
224
|
+
|
|
225
|
+
#### 2. Block references to Lazy
|
|
226
|
+
A section may reference a named block (e.g., `"Footer - 01"`) whose underlying definition is `Lazy.tsx`. `isCmsLazyWrapped` resolves one level of block reference to detect this.
|
|
227
|
+
|
|
228
|
+
#### 3. alwaysEager overrides Lazy wrapping
|
|
229
|
+
If `Footer.tsx` is in `alwaysEager` but wrapped in Lazy in the CMS, it stays eager. This is intentional — layout sections must always be in the initial HTML.
|
|
230
|
+
|
|
231
|
+
#### 4. Multivariate flags are always eager
|
|
232
|
+
Individual sections wrapped in `website/flags/multivariate.ts` require runtime matcher evaluation and can't be safely deferred.
|
|
233
|
+
|
|
234
|
+
#### 5. InvalidCharacterError with section rendering
|
|
235
|
+
In TanStack Start, resolved sections have `Component` as a string key (not a React component). Use `SectionRenderer` or `SectionList` from `@decocms/start/hooks` to render sections — never destructure `{ Component, props }` and use as JSX directly.
|
|
236
|
+
|
|
237
|
+
#### 6. Navigation flash prevention
|
|
238
|
+
Don't use `pendingComponent` on CMS routes — it replaces the entire page content (including Header/Footer) during transitions. Instead, use a root-level `NavigationProgress` bar that keeps previous page visible while loading.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Public API Summary
|
|
243
|
+
|
|
244
|
+
#### From `@decocms/start/cms`
|
|
245
|
+
|
|
246
|
+
| Export | Type | Description |
|
|
247
|
+
|--------|------|-------------|
|
|
248
|
+
| `setAsyncRenderingConfig` | Function | Enable/configure async rendering |
|
|
249
|
+
| `getAsyncRenderingConfig` | Function | Read current config |
|
|
250
|
+
| `registerCacheableSections` | Function | Register sections for SWR loader caching |
|
|
251
|
+
| `runSingleSectionLoader` | Function | Run a single section's loader |
|
|
252
|
+
| `resolveDeferredSection` | Function | Fully resolve a deferred section's raw props |
|
|
253
|
+
| `preloadSectionModule` | Function | Eagerly import a section to extract LoadingFallback |
|
|
254
|
+
|
|
255
|
+
#### From `@decocms/start/routes`
|
|
256
|
+
|
|
257
|
+
| Export | Type | Description |
|
|
258
|
+
|--------|------|-------------|
|
|
259
|
+
| `loadDeferredSection` | ServerFn | Server function to resolve + enrich deferred section on demand |
|
|
260
|
+
|
|
261
|
+
#### From `@decocms/start/hooks`
|
|
262
|
+
|
|
263
|
+
| Export | Type | Description |
|
|
264
|
+
|--------|------|-------------|
|
|
265
|
+
| `DecoPageRenderer` | Component | Renders page with eager + deferred section support |
|
|
266
|
+
| `SectionRenderer` | Component | Renders a single section by registry key |
|
|
267
|
+
| `SectionList` | Component | Renders an array of sections |
|
|
268
|
+
|
|
4
269
|
---
|
|
5
270
|
|
|
6
|
-
|
|
271
|
+
## Part 2: Site Implementation
|
|
7
272
|
|
|
8
273
|
How to configure and use Async Section Rendering in your Deco storefront.
|
|
9
274
|
|
|
10
|
-
|
|
275
|
+
### When to Use This Reference
|
|
11
276
|
|
|
12
277
|
- Setting up async section rendering on a new or existing Deco site
|
|
13
278
|
- Creating `LoadingFallback` components for sections
|
|
@@ -18,9 +283,9 @@ How to configure and use Async Section Rendering in your Deco storefront.
|
|
|
18
283
|
|
|
19
284
|
---
|
|
20
285
|
|
|
21
|
-
|
|
286
|
+
### Quick Start (3 steps)
|
|
22
287
|
|
|
23
|
-
|
|
288
|
+
#### 1. `src/setup.ts` — Enable async rendering
|
|
24
289
|
|
|
25
290
|
```ts
|
|
26
291
|
import {
|
|
@@ -49,7 +314,7 @@ registerCacheableSections({
|
|
|
49
314
|
});
|
|
50
315
|
```
|
|
51
316
|
|
|
52
|
-
|
|
317
|
+
#### 2. Wrap sections in Lazy in CMS JSONs
|
|
53
318
|
|
|
54
319
|
In `.deco/blocks/pages-*.json`, wrap below-the-fold sections:
|
|
55
320
|
|
|
@@ -75,15 +340,15 @@ In `.deco/blocks/pages-*.json`, wrap below-the-fold sections:
|
|
|
75
340
|
- SEO sections → **skip** (they're metadata, not visual)
|
|
76
341
|
- Everything else below the fold → **wrap in Lazy**
|
|
77
342
|
|
|
78
|
-
|
|
343
|
+
#### 3. Add LoadingFallback to every lazy section
|
|
79
344
|
|
|
80
345
|
Export `LoadingFallback` from the section file. See detailed patterns below.
|
|
81
346
|
|
|
82
347
|
---
|
|
83
348
|
|
|
84
|
-
|
|
349
|
+
### CMS Lazy Wrapper Strategy
|
|
85
350
|
|
|
86
|
-
|
|
351
|
+
#### Page audit checklist
|
|
87
352
|
|
|
88
353
|
For each CMS page (`pages-*.json`):
|
|
89
354
|
|
|
@@ -92,7 +357,7 @@ For each CMS page (`pages-*.json`):
|
|
|
92
357
|
3. Wrap everything else in `website/sections/Rendering/Lazy.tsx`.
|
|
93
358
|
4. Keep `alwaysEager` sections (Header, Footer, etc.) unwrapped even if they appear at the end.
|
|
94
359
|
|
|
95
|
-
|
|
360
|
+
#### Real-world example: Homepage
|
|
96
361
|
|
|
97
362
|
| Index | Section | Status |
|
|
98
363
|
|-------|---------|--------|
|
|
@@ -111,9 +376,9 @@ Result: 4 eager + 17 lazy → **52% payload reduction**.
|
|
|
111
376
|
|
|
112
377
|
---
|
|
113
378
|
|
|
114
|
-
|
|
379
|
+
### Creating LoadingFallback Components
|
|
115
380
|
|
|
116
|
-
|
|
381
|
+
#### Key rules
|
|
117
382
|
|
|
118
383
|
1. **Match dimensions**: Same container classes, padding, and aspect ratio as the real section
|
|
119
384
|
2. **CSS-only**: Use `skeleton animate-pulse` classes. No JS, no hooks, no data.
|
|
@@ -121,7 +386,7 @@ Result: 4 eager + 17 lazy → **52% payload reduction**.
|
|
|
121
386
|
4. **One per section file**: Export from `src/sections/Foo.tsx`, not from the component file
|
|
122
387
|
5. **Represent the content**: Skeletons should visually match the final layout
|
|
123
388
|
|
|
124
|
-
|
|
389
|
+
#### Product Card Skeleton (reusable pattern)
|
|
125
390
|
|
|
126
391
|
Most shelf/grid sections contain product cards. Define a shared skeleton:
|
|
127
392
|
|
|
@@ -148,7 +413,7 @@ function CardSkeleton() {
|
|
|
148
413
|
|
|
149
414
|
This matches the real `ProductCard` layout: image → flag → name (2 lines) → price block (from/to/installment) → buy button.
|
|
150
415
|
|
|
151
|
-
|
|
416
|
+
#### Pattern: Product Shelf
|
|
152
417
|
|
|
153
418
|
```tsx
|
|
154
419
|
export function LoadingFallback() {
|
|
@@ -168,7 +433,7 @@ export function LoadingFallback() {
|
|
|
168
433
|
}
|
|
169
434
|
```
|
|
170
435
|
|
|
171
|
-
|
|
436
|
+
#### Pattern: Tabbed Shelf
|
|
172
437
|
|
|
173
438
|
```tsx
|
|
174
439
|
export function LoadingFallback() {
|
|
@@ -194,7 +459,7 @@ export function LoadingFallback() {
|
|
|
194
459
|
}
|
|
195
460
|
```
|
|
196
461
|
|
|
197
|
-
|
|
462
|
+
#### Pattern: Search Result (PLP)
|
|
198
463
|
|
|
199
464
|
```tsx
|
|
200
465
|
export function LoadingFallback() {
|
|
@@ -232,7 +497,7 @@ export function LoadingFallback() {
|
|
|
232
497
|
}
|
|
233
498
|
```
|
|
234
499
|
|
|
235
|
-
|
|
500
|
+
#### Pattern: Full-width Banner/Carousel
|
|
236
501
|
|
|
237
502
|
```tsx
|
|
238
503
|
export function LoadingFallback() {
|
|
@@ -244,7 +509,7 @@ export function LoadingFallback() {
|
|
|
244
509
|
}
|
|
245
510
|
```
|
|
246
511
|
|
|
247
|
-
|
|
512
|
+
#### Pattern: FAQ Accordion
|
|
248
513
|
|
|
249
514
|
```tsx
|
|
250
515
|
export function LoadingFallback() {
|
|
@@ -260,7 +525,7 @@ export function LoadingFallback() {
|
|
|
260
525
|
}
|
|
261
526
|
```
|
|
262
527
|
|
|
263
|
-
|
|
528
|
+
#### Pattern: Testimonials/Cards Grid
|
|
264
529
|
|
|
265
530
|
```tsx
|
|
266
531
|
export function LoadingFallback() {
|
|
@@ -282,7 +547,7 @@ export function LoadingFallback() {
|
|
|
282
547
|
}
|
|
283
548
|
```
|
|
284
549
|
|
|
285
|
-
|
|
550
|
+
#### Pattern: Footer
|
|
286
551
|
|
|
287
552
|
```tsx
|
|
288
553
|
export function LoadingFallback() {
|
|
@@ -308,7 +573,7 @@ export function LoadingFallback() {
|
|
|
308
573
|
|
|
309
574
|
---
|
|
310
575
|
|
|
311
|
-
|
|
576
|
+
### SPA Navigation: NavigationProgress
|
|
312
577
|
|
|
313
578
|
**Do NOT use `pendingComponent`** on CMS routes — it replaces the entire page content (Header/Footer disappear, causing a "flash white").
|
|
314
579
|
|
|
@@ -338,9 +603,9 @@ Add `<NavigationProgress />` before your main layout in `RootLayout`.
|
|
|
338
603
|
|
|
339
604
|
---
|
|
340
605
|
|
|
341
|
-
|
|
606
|
+
### Configuration Reference
|
|
342
607
|
|
|
343
|
-
|
|
608
|
+
#### `setAsyncRenderingConfig(options)`
|
|
344
609
|
|
|
345
610
|
| Option | Type | Default | Description |
|
|
346
611
|
|--------|------|---------|-------------|
|
|
@@ -348,7 +613,7 @@ Add `<NavigationProgress />` before your main layout in `RootLayout`.
|
|
|
348
613
|
| `foldThreshold` | `number` | `Infinity` | Fallback for non-wrapped sections (Infinity = only Lazy-wrapped defer) |
|
|
349
614
|
| `alwaysEager` | `string[]` | `[]` | Section keys that are ALWAYS eager regardless |
|
|
350
615
|
|
|
351
|
-
|
|
616
|
+
#### `registerCacheableSections(configs)`
|
|
352
617
|
|
|
353
618
|
```ts
|
|
354
619
|
registerCacheableSections({
|
|
@@ -360,9 +625,9 @@ Good candidates: Product shelves (2-3 min), FAQ/content (15-30 min). NOT for PDP
|
|
|
360
625
|
|
|
361
626
|
---
|
|
362
627
|
|
|
363
|
-
|
|
628
|
+
### Debugging
|
|
364
629
|
|
|
365
|
-
|
|
630
|
+
#### Section not being deferred
|
|
366
631
|
|
|
367
632
|
1. Is `setAsyncRenderingConfig()` called in `setup.ts`?
|
|
368
633
|
2. Is the section wrapped in `website/sections/Rendering/Lazy.tsx` in the CMS JSON?
|
|
@@ -371,7 +636,7 @@ Good candidates: Product shelves (2-3 min), FAQ/content (15-30 min). NOT for PDP
|
|
|
371
636
|
5. Is it wrapped in a multivariate flag? (always eager)
|
|
372
637
|
6. Is the user-agent a bot? (bots always get full eager)
|
|
373
638
|
|
|
374
|
-
|
|
639
|
+
#### Verifying with curl
|
|
375
640
|
|
|
376
641
|
```bash
|
|
377
642
|
# Normal user — count deferred sections
|
|
@@ -385,13 +650,13 @@ curl -s -o /dev/null -w "Normal: %{size_download}\n" http://localhost:5173/
|
|
|
385
650
|
curl -s -o /dev/null -w "Bot: %{size_download}\n" -A "Googlebot/2.1" http://localhost:5173/
|
|
386
651
|
```
|
|
387
652
|
|
|
388
|
-
|
|
653
|
+
#### InvalidCharacterError with sections
|
|
389
654
|
|
|
390
655
|
If you see `Failed to execute 'createElement'` with a section path as tag name, the component is using `{ Component, props }` destructuring directly as JSX. Use `SectionRenderer` or `SectionList` from `@decocms/start/hooks` instead.
|
|
391
656
|
|
|
392
657
|
---
|
|
393
658
|
|
|
394
|
-
|
|
659
|
+
### Performance Impact
|
|
395
660
|
|
|
396
661
|
Measured on `espacosmart-storefront`:
|
|
397
662
|
|
|
@@ -403,7 +668,7 @@ Measured on `espacosmart-storefront`:
|
|
|
403
668
|
|
|
404
669
|
---
|
|
405
670
|
|
|
406
|
-
|
|
671
|
+
### Checklist for New Sites
|
|
407
672
|
|
|
408
673
|
- [ ] Call `setAsyncRenderingConfig()` in `setup.ts` with `alwaysEager` sections
|
|
409
674
|
- [ ] Audit all CMS page JSONs — wrap below-fold sections in `Lazy.tsx`
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Codemod Commands
|
|
2
|
+
|
|
3
|
+
All automation commands organized by phase. Run from project root.
|
|
4
|
+
|
|
5
|
+
## Phase 1 — Imports & JSX
|
|
6
|
+
|
|
7
|
+
### Preact → React (safe for bulk)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
11
|
+
-e 's|from "preact/hooks"|from "react"|g' \
|
|
12
|
+
-e 's|from "preact/compat"|from "react"|g' \
|
|
13
|
+
-e 's|from "preact"|from "react"|g'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### ComponentChildren → ReactNode
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
20
|
+
-e 's/ComponentChildren/ReactNode/g'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### SVG attributes (safe for bulk)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
find src/ -name '*.tsx' | xargs sed -i '' \
|
|
27
|
+
-e 's/stroke-width=/strokeWidth=/g' \
|
|
28
|
+
-e 's/stroke-linecap=/strokeLinecap=/g' \
|
|
29
|
+
-e 's/stroke-linejoin=/strokeLinejoin=/g' \
|
|
30
|
+
-e 's/fill-rule=/fillRule=/g' \
|
|
31
|
+
-e 's/clip-rule=/clipRule=/g' \
|
|
32
|
+
-e 's/clip-path=/clipPath=/g' \
|
|
33
|
+
-e 's/stroke-dasharray=/strokeDasharray=/g' \
|
|
34
|
+
-e 's/stroke-dashoffset=/strokeDashoffset=/g'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### HTML attributes
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
find src/ -name '*.tsx' | xargs sed -i '' \
|
|
41
|
+
-e 's/ for=/ htmlFor=/g' \
|
|
42
|
+
-e 's/ fetchpriority=/ fetchPriority=/g' \
|
|
43
|
+
-e 's/ autocomplete=/ autoComplete=/g'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Remove JSX pragmas
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
find src/ -name '*.tsx' -o -name '*.ts' | xargs sed -i '' \
|
|
50
|
+
-e '/\/\*\* @jsxRuntime automatic \*\//d' \
|
|
51
|
+
-e '/\/\*\* @jsx h \*\//d' \
|
|
52
|
+
-e '/\/\*\* @jsxFrag Fragment \*\//d'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Phase 2 — Signals
|
|
56
|
+
|
|
57
|
+
### Module-level signal imports (safe for bulk)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
61
|
+
's|from "@preact/signals"|from "@decocms/start/sdk/signal"|g'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Audit useSignal usage (manual conversion needed)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
grep -rn 'useSignal\|useComputed' src/ --include='*.tsx' --include='*.ts'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Phase 3 — Deco Framework
|
|
71
|
+
|
|
72
|
+
### Remove $fresh imports
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
76
|
+
-e 's|import { asset } from "\$fresh/runtime.ts";||g' \
|
|
77
|
+
-e 's|asset(\([^)]*\))|\1|g'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Replace site-local import aliases
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Replace with your actual site name:
|
|
84
|
+
SITE_NAME="osklenbr"
|
|
85
|
+
|
|
86
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
87
|
+
-e "s|from \"\\\$store/|from \"~/|g" \
|
|
88
|
+
-e "s|from \"deco-sites/${SITE_NAME}/|from \"~/|g" \
|
|
89
|
+
-e "s|from \"site/|from \"~/|g"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### IS_BROWSER replacement
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
96
|
+
-e 's|import { IS_BROWSER } from "\$fresh/runtime.ts";||g' \
|
|
97
|
+
-e 's|IS_BROWSER|typeof window !== "undefined"|g'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Phase 4 — Commerce
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
104
|
+
-e 's|from "apps/commerce/types.ts"|from "@decocms/apps/commerce/types"|g' \
|
|
105
|
+
-e 's|from "apps/admin/widgets.ts"|from "~/types/widgets"|g' \
|
|
106
|
+
-e 's|from "apps/website/components/Image.tsx"|from "~/components/ui/Image"|g' \
|
|
107
|
+
-e 's|from "apps/website/components/Picture.tsx"|from "~/components/ui/Picture"|g' \
|
|
108
|
+
-e 's|from "apps/website/components/Video.tsx"|from "~/components/ui/Video"|g'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### SDK utilities
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
115
|
+
-e 's|from "~/sdk/useOffer.ts"|from "@decocms/apps/commerce/sdk/useOffer"|g' \
|
|
116
|
+
-e 's|from "~/sdk/useOffer"|from "@decocms/apps/commerce/sdk/useOffer"|g' \
|
|
117
|
+
-e 's|from "~/sdk/format.ts"|from "@decocms/apps/commerce/sdk/formatPrice"|g'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Phase 6 — Islands
|
|
121
|
+
|
|
122
|
+
### Audit island types
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
echo "=== Wrapper islands (re-export from components) ==="
|
|
126
|
+
grep -rl 'export.*from.*components' src/islands/ --include='*.tsx' 2>/dev/null
|
|
127
|
+
|
|
128
|
+
echo ""
|
|
129
|
+
echo "=== Standalone islands (have real logic) ==="
|
|
130
|
+
find src/islands/ -name '*.tsx' ! -exec grep -l 'export.*from.*components' {} \; 2>/dev/null
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Repoint imports from islands/ to components/
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
|
|
137
|
+
's|from "~/islands/|from "~/components/|g'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Verification Commands
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Zero old imports (run after all phases):
|
|
144
|
+
echo "Preact: $(grep -r 'from "preact' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
145
|
+
echo "Signals: $(grep -r '@preact/signals' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
146
|
+
echo "@deco/deco: $(grep -r '@deco/deco' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
147
|
+
echo "\$fresh: $(grep -r '\$fresh' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
148
|
+
echo "apps/: $(grep -r 'from \"apps/' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
149
|
+
echo "islands/: $(grep -r 'from \"~/islands/' src/ --include='*.tsx' --include='*.ts' | wc -l)"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Pre-Flight Audit Script
|
|
153
|
+
|
|
154
|
+
Run against the source site before starting migration:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
echo "=== Source Site Audit ==="
|
|
158
|
+
echo "Components: $(find components/ sections/ -name '*.tsx' 2>/dev/null | wc -l)"
|
|
159
|
+
echo "Islands: $(find islands/ -name '*.tsx' 2>/dev/null | wc -l)"
|
|
160
|
+
echo "Sections: $(find sections/ -name '*.tsx' 2>/dev/null | wc -l)"
|
|
161
|
+
echo "Loaders: $(find loaders/ -name '*.ts' -o -name '*.tsx' 2>/dev/null | wc -l)"
|
|
162
|
+
echo ""
|
|
163
|
+
echo "=== Import Dependencies ==="
|
|
164
|
+
echo "Preact: $(grep -rl 'from "preact' . --include='*.tsx' --include='*.ts' 2>/dev/null | wc -l) files"
|
|
165
|
+
echo "Signals: $(grep -rl '@preact/signals' . --include='*.tsx' --include='*.ts' 2>/dev/null | wc -l) files"
|
|
166
|
+
echo "@deco/deco: $(grep -rl '@deco/deco' . --include='*.tsx' --include='*.ts' 2>/dev/null | wc -l) files"
|
|
167
|
+
echo "\$fresh: $(grep -rl '\$fresh/' . --include='*.tsx' --include='*.ts' 2>/dev/null | wc -l) files"
|
|
168
|
+
echo "apps/: $(grep -rl 'from \"apps/' . --include='*.tsx' --include='*.ts' 2>/dev/null | wc -l) files"
|
|
169
|
+
echo "useSignal: $(grep -r 'useSignal' . --include='*.tsx' --include='*.ts' -c 2>/dev/null | awk -F: '{sum+=$2} END{print sum}')"
|
|
170
|
+
echo ""
|
|
171
|
+
echo "=== CMS Blocks ==="
|
|
172
|
+
echo "Total: $(find .deco/blocks/ -name '*.json' 2>/dev/null | wc -l)"
|
|
173
|
+
echo "Pages: $(find .deco/blocks/ -name 'pages-*.json' 2>/dev/null | wc -l)"
|
|
174
|
+
```
|