@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App system integration pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Consumes AppDefinition objects from @decocms/apps and automates:
|
|
5
|
+
* 1. Invoke handler registration (from manifest + explicit handlers)
|
|
6
|
+
* 2. Section registration (when manifest.sections is available)
|
|
7
|
+
* 3. App middleware registration (with state injection into RequestContext)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { setupApps } from "@decocms/start/sdk/setupApps";
|
|
12
|
+
* import * as vtexApp from "@decocms/apps/vtex/mod";
|
|
13
|
+
* import * as resendApp from "@decocms/apps/resend/mod";
|
|
14
|
+
*
|
|
15
|
+
* const vtex = await vtexApp.configure(blocks["deco-vtex"], resolveSecret);
|
|
16
|
+
* const resend = await resendApp.configure(blocks["deco-resend"], resolveSecret);
|
|
17
|
+
*
|
|
18
|
+
* await setupApps([vtex, resend].filter(Boolean));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { clearInvokeHandlers, registerInvokeHandlers } from "../admin/invoke";
|
|
23
|
+
import { registerSections } from "../cms/registry";
|
|
24
|
+
import { RequestContext } from "./requestContext";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types — mirrors @decocms/apps/commerce/app-types without importing it
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface AppManifest {
|
|
31
|
+
name: string;
|
|
32
|
+
loaders: Record<string, Record<string, unknown>>;
|
|
33
|
+
actions: Record<string, Record<string, unknown>>;
|
|
34
|
+
sections?: Record<string, () => Promise<any>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AppMiddleware {
|
|
38
|
+
(request: Request, next: () => Promise<Response>): Promise<Response>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AppDefinition<TState = unknown> {
|
|
42
|
+
name: string;
|
|
43
|
+
manifest: AppManifest;
|
|
44
|
+
state: TState;
|
|
45
|
+
middleware?: AppMiddleware;
|
|
46
|
+
dependencies?: AppDefinition[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extended definition with optional explicit handlers.
|
|
51
|
+
* autoconfigApps() attaches mod.handlers here before calling setupApps().
|
|
52
|
+
*/
|
|
53
|
+
export interface AppDefinitionWithHandlers<TState = unknown>
|
|
54
|
+
extends AppDefinition<TState> {
|
|
55
|
+
/** Pre-wrapped handlers from the app's mod.ts (e.g. unwrapped VTEX actions). */
|
|
56
|
+
handlers?: Record<string, (props: any, request: Request) => Promise<any>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// App middleware registry
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** Per-app state entries — injected into RequestContext.bag on every request. */
|
|
64
|
+
const appStates: Array<{ name: string; state: unknown }> = [];
|
|
65
|
+
|
|
66
|
+
const appMiddlewares: Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
middleware: AppMiddleware;
|
|
69
|
+
}> = [];
|
|
70
|
+
|
|
71
|
+
function registerAppState(name: string, state: unknown) {
|
|
72
|
+
appStates.push({ name, state });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function registerAppMiddleware(
|
|
76
|
+
name: string,
|
|
77
|
+
mw: AppMiddleware,
|
|
78
|
+
) {
|
|
79
|
+
appMiddlewares.push({ name, middleware: mw });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear all registrations. Called before re-running setupApps()
|
|
84
|
+
* on admin hot-reload to prevent duplicate middleware/state entries.
|
|
85
|
+
*/
|
|
86
|
+
function clearRegistrations() {
|
|
87
|
+
appStates.length = 0;
|
|
88
|
+
appMiddlewares.length = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns a chained middleware that runs all registered app middlewares.
|
|
93
|
+
* The site wires this into its own createMiddleware() chain.
|
|
94
|
+
*
|
|
95
|
+
* Before running app middlewares, all app states are injected into
|
|
96
|
+
* RequestContext.bag so loaders can access them via getAppState().
|
|
97
|
+
*
|
|
98
|
+
* Returns undefined if no app states or middlewares were registered.
|
|
99
|
+
*/
|
|
100
|
+
export function getAppMiddleware(): AppMiddleware | undefined {
|
|
101
|
+
if (appStates.length === 0 && appMiddlewares.length === 0) return undefined;
|
|
102
|
+
|
|
103
|
+
return async (request, next) => {
|
|
104
|
+
// Inject all app states into RequestContext bag
|
|
105
|
+
for (const { name, state } of appStates) {
|
|
106
|
+
RequestContext.setBag(`app:${name}:state`, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Chain app middlewares (first registered runs outermost)
|
|
110
|
+
if (appMiddlewares.length === 0) return next();
|
|
111
|
+
const run = async (i: number): Promise<Response> => {
|
|
112
|
+
if (i >= appMiddlewares.length) return next();
|
|
113
|
+
return appMiddlewares[i].middleware(request, () => run(i + 1));
|
|
114
|
+
};
|
|
115
|
+
return run(0);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Dependency flattening
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Topological sort: dependencies before parents.
|
|
125
|
+
* Combined with first-wins registration in registerInvokeHandlers,
|
|
126
|
+
* this means parent apps can override handlers from their dependencies
|
|
127
|
+
* by providing explicit `handlers` (registered before manifest flatten).
|
|
128
|
+
*/
|
|
129
|
+
function flattenDependencies(apps: AppDefinition[]): AppDefinition[] {
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
const result: AppDefinition[] = [];
|
|
132
|
+
|
|
133
|
+
function visit(app: AppDefinition) {
|
|
134
|
+
if (seen.has(app.name)) return;
|
|
135
|
+
seen.add(app.name);
|
|
136
|
+
if (app.dependencies) {
|
|
137
|
+
for (const dep of app.dependencies) visit(dep);
|
|
138
|
+
}
|
|
139
|
+
result.push(app);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const app of apps) visit(app);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Main pipeline
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initialize apps from their AppDefinitions.
|
|
152
|
+
*
|
|
153
|
+
* Call once in setup.ts after configuring apps via their mod.configure().
|
|
154
|
+
* Handles: invoke handler registration, section registration, middleware setup.
|
|
155
|
+
*/
|
|
156
|
+
export async function setupApps(
|
|
157
|
+
apps: Array<AppDefinitionWithHandlers | AppDefinition>,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
if (typeof document !== "undefined") return; // server-only
|
|
160
|
+
|
|
161
|
+
// Clear previous registrations (safe for hot-reload via onChange)
|
|
162
|
+
clearRegistrations();
|
|
163
|
+
clearInvokeHandlers();
|
|
164
|
+
|
|
165
|
+
for (const app of flattenDependencies(apps as AppDefinition[])) {
|
|
166
|
+
const appWithHandlers = app as AppDefinitionWithHandlers;
|
|
167
|
+
|
|
168
|
+
// 1. Register explicit handlers (pre-unwrapped by the app, e.g. resend)
|
|
169
|
+
if (appWithHandlers.handlers) {
|
|
170
|
+
registerInvokeHandlers(appWithHandlers.handlers);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Flatten manifest modules → individual invoke handlers
|
|
174
|
+
// manifest.actions["vtex/actions/checkout"] = { getOrCreateCart, addItemsToCart, ... }
|
|
175
|
+
// → register "vtex/actions/checkout/getOrCreateCart" as handler
|
|
176
|
+
for (const category of ["loaders", "actions"] as const) {
|
|
177
|
+
const modules = app.manifest[category];
|
|
178
|
+
if (!modules) continue;
|
|
179
|
+
|
|
180
|
+
for (const [moduleKey, moduleExports] of Object.entries(modules)) {
|
|
181
|
+
for (const [fnName, fn] of Object.entries(
|
|
182
|
+
moduleExports as Record<string, unknown>,
|
|
183
|
+
)) {
|
|
184
|
+
if (typeof fn !== "function") continue;
|
|
185
|
+
const key = `${moduleKey}/${fnName}`;
|
|
186
|
+
const handler = (props: any, req: Request) =>
|
|
187
|
+
(fn as Function)(props, req);
|
|
188
|
+
registerInvokeHandlers({
|
|
189
|
+
[key]: handler,
|
|
190
|
+
[`${key}.ts`]: handler,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 3. Register sections from manifest (future — when apps export sections)
|
|
197
|
+
if (app.manifest.sections) {
|
|
198
|
+
registerSections(
|
|
199
|
+
app.manifest.sections as Record<string, () => Promise<any>>,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 4. Always register app state (so getAppState() works for all apps)
|
|
204
|
+
registerAppState(app.name, app.state);
|
|
205
|
+
|
|
206
|
+
// 5. Register middleware (optional — not all apps have middleware)
|
|
207
|
+
if (app.middleware) {
|
|
208
|
+
registerAppMiddleware(app.name, app.middleware);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { buildHtmlShell } from "./htmlShell";
|
|
|
36
36
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
37
37
|
import { isMobileUA } from "./useDevice";
|
|
38
38
|
import { getRenderShellConfig } from "../admin/setup";
|
|
39
|
+
import { RequestContext } from "./requestContext";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Append Link preload headers for CSS and fonts so the browser starts
|
|
@@ -653,6 +654,10 @@ export function createDecoWorkerEntry(
|
|
|
653
654
|
env: Record<string, unknown>,
|
|
654
655
|
ctx: WorkerExecutionContext,
|
|
655
656
|
): Promise<Response> {
|
|
657
|
+
// Wrap the entire request in a RequestContext so that all code
|
|
658
|
+
// in the call stack (loaders, invoke handlers, vtexFetchWithCookies)
|
|
659
|
+
// can access the request and write response headers.
|
|
660
|
+
return RequestContext.run(request, async () => {
|
|
656
661
|
const url = new URL(request.url);
|
|
657
662
|
|
|
658
663
|
// Admin routes (/_meta, /.decofile, /live/previews) — always handled first
|
|
@@ -879,6 +884,7 @@ export function createDecoWorkerEntry(
|
|
|
879
884
|
// the stream in Workers runtime, causing Error 1101.
|
|
880
885
|
storeInCache(origin);
|
|
881
886
|
return dressResponse(origin, "MISS");
|
|
887
|
+
}); // end RequestContext.run()
|
|
882
888
|
},
|
|
883
889
|
};
|
|
884
890
|
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: deco-async-rendering-architecture
|
|
3
|
-
description: Architecture and internals of Async Section Rendering in @decocms/start. Documents the server-side eager/deferred split (resolve.ts) using CMS Lazy.tsx wrappers as source of truth, client-side IntersectionObserver loading (DecoPageRenderer.tsx), per-section SWR caching (sectionLoaders.ts), bot detection for SEO, the loadDeferredSection server function, and the full request flow from CMS page resolution to on-scroll hydration. Use when debugging async rendering, extending the framework, understanding how deferred sections are resolved, or troubleshooting why a section is/isn't being deferred.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Deco Async Section Rendering — Framework Architecture
|
|
7
|
-
|
|
8
|
-
Internal documentation for the async section rendering system in `@decocms/start`.
|
|
9
|
-
|
|
10
|
-
## When to Use This Skill
|
|
11
|
-
|
|
12
|
-
- Debugging why a section is or isn't being deferred
|
|
13
|
-
- Understanding the full request flow from CMS resolution to on-scroll loading
|
|
14
|
-
- Extending the async rendering system (new cache tiers, new deferral strategies)
|
|
15
|
-
- Fixing issues with deferred section data resolution
|
|
16
|
-
- Understanding how bot detection and SEO safety work
|
|
17
|
-
- Working on `@decocms/start` framework code
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Problem Solved
|
|
22
|
-
|
|
23
|
-
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.
|
|
24
|
-
|
|
25
|
-
## Architecture Overview
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
Request → resolveDecoPage()
|
|
29
|
-
├─ resolveSectionsList() → unwrap flags/blocks to get raw section array
|
|
30
|
-
├─ shouldDeferSection() → classify each section as eager or deferred
|
|
31
|
-
│ ├─ Eager: resolveRawSection() → full CMS + commerce resolution
|
|
32
|
-
│ └─ Deferred: resolveSectionShallow() → component key + raw CMS props only
|
|
33
|
-
├─ runSectionLoaders() → enrich eager sections (server loaders)
|
|
34
|
-
└─ Return { resolvedSections, deferredSections }
|
|
35
|
-
|
|
36
|
-
Client render → DecoPageRenderer
|
|
37
|
-
├─ mergeSections() → interleave eager + deferred by original index
|
|
38
|
-
├─ Eager: <Suspense><LazyComponent .../></Suspense>
|
|
39
|
-
└─ Deferred: <DeferredSectionWrapper>
|
|
40
|
-
├─ preloadSectionModule() → get LoadingFallback early
|
|
41
|
-
├─ Render skeleton (custom LoadingFallback or generic)
|
|
42
|
-
├─ IntersectionObserver(rootMargin: 300px)
|
|
43
|
-
└─ On intersect: loadDeferredSection serverFn
|
|
44
|
-
├─ resolveDeferredSection() → resolve __resolveType refs in rawProps
|
|
45
|
-
├─ runSingleSectionLoader() → enrich with server loader
|
|
46
|
-
└─ Return ResolvedSection → render real component with fade-in
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## Deferral Strategy: CMS Lazy.tsx as Source of Truth
|
|
52
|
-
|
|
53
|
-
### How it works now (respectCmsLazy)
|
|
54
|
-
|
|
55
|
-
The deferral decision is driven by **CMS editor choices**, not a global index threshold:
|
|
56
|
-
|
|
57
|
-
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
|
|
58
|
-
2. **`foldThreshold`** (default `Infinity`) — fallback for sections NOT wrapped in Lazy; with default `Infinity`, non-wrapped sections are always eager
|
|
59
|
-
3. **`alwaysEager`** — section keys that override all deferral (Header, Footer, Theme, etc.)
|
|
60
|
-
|
|
61
|
-
### Why this approach
|
|
62
|
-
|
|
63
|
-
The previous `foldThreshold` approach deferred sections by index position, ignoring editor intent. This caused:
|
|
64
|
-
- Sections that editors wanted eager getting deferred
|
|
65
|
-
- No control per-page (threshold was global)
|
|
66
|
-
- Homepage with 12 sections marked Lazy in CMS showing 0 deferred
|
|
67
|
-
|
|
68
|
-
Now editors control deferral by wrapping sections in `Lazy.tsx` in the CMS admin, and the framework respects that.
|
|
69
|
-
|
|
70
|
-
### `isCmsLazyWrapped(section)` in `resolve.ts`
|
|
71
|
-
|
|
72
|
-
Detects whether a section is wrapped in `website/sections/Rendering/Lazy.tsx`, either:
|
|
73
|
-
- Directly: `section.__resolveType === "website/sections/Rendering/Lazy.tsx"`
|
|
74
|
-
- Via named block: `section.__resolveType` references a block whose `__resolveType` is `"website/sections/Rendering/Lazy.tsx"`
|
|
75
|
-
|
|
76
|
-
### `shouldDeferSection(section, flatIndex, cfg, isBotReq)`
|
|
77
|
-
|
|
78
|
-
Updated decision logic:
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
1. Bot request? → EAGER (SEO safety)
|
|
82
|
-
2. No __resolveType? → EAGER (can't classify)
|
|
83
|
-
3. Is multivariate flag? → EAGER (requires runtime evaluation)
|
|
84
|
-
4. resolveFinalSectionKey() → walk block refs + Lazy wrappers to find final component
|
|
85
|
-
5. In alwaysEager set? → EAGER
|
|
86
|
-
6. isLayoutSection()? → EAGER
|
|
87
|
-
7. respectCmsLazy && isCmsLazyWrapped(section)? → DEFER
|
|
88
|
-
8. flatIndex >= foldThreshold? → DEFER (fallback, only if not wrapped)
|
|
89
|
-
9. Otherwise → EAGER
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Files and Their Roles
|
|
95
|
-
|
|
96
|
-
| File | Layer | Role |
|
|
97
|
-
|------|-------|------|
|
|
98
|
-
| `src/cms/resolve.ts` | Server | Types, config, eager/deferred split, CMS Lazy detection, shallow resolution, full deferred resolution |
|
|
99
|
-
| `src/cms/sectionLoaders.ts` | Server | Section loader registry, layout cache, SWR cacheable sections, `runSingleSectionLoader` |
|
|
100
|
-
| `src/cms/registry.ts` | Shared | Section component registry, `preloadSectionModule` for early LoadingFallback |
|
|
101
|
-
| `src/routes/cmsRoute.ts` | Server | `loadCmsPage`, `loadCmsHomePage`, `loadDeferredSection` server functions |
|
|
102
|
-
| `src/hooks/DecoPageRenderer.tsx` | Client | Merge, render eager/deferred, `DeferredSectionWrapper`, dev warnings |
|
|
103
|
-
| `src/cms/index.ts` | Barrel | Re-exports all public types and functions |
|
|
104
|
-
| `src/routes/index.ts` | Barrel | Re-exports route helpers including `loadDeferredSection` |
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## Server-Side: Eager/Deferred Split
|
|
109
|
-
|
|
110
|
-
### Entry point: `resolveDecoPage()` in `resolve.ts`
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
resolveDecoPage(targetPath, matcherCtx)
|
|
114
|
-
1. findPageByPath(targetPath) → { page, params }
|
|
115
|
-
2. Get raw sections array:
|
|
116
|
-
- If page.sections is Array → use directly
|
|
117
|
-
- If page.sections is wrapped (multivariate flag, block ref) → resolveSectionsList()
|
|
118
|
-
3. For each raw section:
|
|
119
|
-
- If shouldDeferSection() → resolveSectionShallow() → DeferredSection
|
|
120
|
-
- Else → resolveRawSection() (full resolution) → ResolvedSection[]
|
|
121
|
-
4. Return { resolvedSections, deferredSections }
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### `resolveSectionsList(value, rctx, depth)`
|
|
125
|
-
|
|
126
|
-
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.
|
|
127
|
-
|
|
128
|
-
### `resolveFinalSectionKey(section)`
|
|
129
|
-
|
|
130
|
-
Walks block reference chain and unwraps `Lazy` wrappers to find the final registered section component key:
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
"Header - 01" (named block)
|
|
134
|
-
→ { __resolveType: "website/sections/Rendering/Lazy.tsx", section: {...} }
|
|
135
|
-
→ { __resolveType: "site/sections/Header/Header.tsx", ...props }
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Returns `"site/sections/Header/Header.tsx"`, checked against `alwaysEager` and `isLayoutSection`.
|
|
139
|
-
|
|
140
|
-
### `resolveSectionShallow(section)`
|
|
141
|
-
|
|
142
|
-
Synchronously follows block refs and unwraps Lazy to extract `component` (final key) and `rawProps` (CMS props as-is). No API calls, no async.
|
|
143
|
-
|
|
144
|
-
### `resolveDeferredSection(component, rawProps, pagePath, matcherCtx)`
|
|
145
|
-
|
|
146
|
-
Called when client requests a deferred section. Runs full resolution:
|
|
147
|
-
1. `resolveProps(rawProps, rctx)` — resolves all nested `__resolveType` references
|
|
148
|
-
2. `normalizeNestedSections(resolvedProps)` — converts nested sections to `{ Component, props }`
|
|
149
|
-
3. Returns `ResolvedSection` ready for `runSingleSectionLoader`
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Server-Side: Section Caching
|
|
154
|
-
|
|
155
|
-
### Three cache tiers in `sectionLoaders.ts`
|
|
156
|
-
|
|
157
|
-
**Tier 1: Layout sections** (Header, Footer, Theme)
|
|
158
|
-
- 5-minute TTL, in-flight dedup, registered via `registerLayoutSections`
|
|
159
|
-
|
|
160
|
-
**Tier 2: Cacheable sections** (ProductShelf, FAQ)
|
|
161
|
-
- Configurable TTL via `registerCacheableSections`, SWR semantics, LRU eviction at 200 entries
|
|
162
|
-
- Cache key: `component::djb2Hash(JSON.stringify(props))`
|
|
163
|
-
|
|
164
|
-
**Tier 3: Regular sections** — No caching, always fresh.
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## Client-Side: DeferredSectionWrapper
|
|
169
|
-
|
|
170
|
-
### Lifecycle
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
1. Mount (stableKey = pagePath + component + index)
|
|
174
|
-
├─ preloadSectionModule(component) → extract LoadingFallback
|
|
175
|
-
└─ Render skeleton (custom or generic DefaultSectionFallback)
|
|
176
|
-
|
|
177
|
-
2. IntersectionObserver (rootMargin: "300px")
|
|
178
|
-
└─ On intersect (once):
|
|
179
|
-
├─ loadDeferredSection serverFn
|
|
180
|
-
├─ On success: render <LazyComponent .../> with fade-in
|
|
181
|
-
└─ On error: render ErrorFallback or null
|
|
182
|
-
|
|
183
|
-
3. SPA navigation: stableKey changes → reset state (triggered, section, error)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Key: stableKey for SPA navigation
|
|
187
|
-
|
|
188
|
-
`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.
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## Bot Detection (SEO Safety)
|
|
193
|
-
|
|
194
|
-
`isBot(userAgent)` regex detects search engine crawlers. When detected, ALL sections are resolved eagerly — `deferredSections` is empty.
|
|
195
|
-
|
|
196
|
-
---
|
|
197
|
-
|
|
198
|
-
## Types
|
|
199
|
-
|
|
200
|
-
### `AsyncRenderingConfig`
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
interface AsyncRenderingConfig {
|
|
204
|
-
respectCmsLazy: boolean; // Default true — use Lazy.tsx wrappers as deferral source
|
|
205
|
-
foldThreshold: number; // Default Infinity — fallback for non-wrapped sections
|
|
206
|
-
alwaysEager: Set<string>; // Section keys that must always be eager
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### `DeferredSection`
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
interface DeferredSection {
|
|
214
|
-
component: string;
|
|
215
|
-
key: string;
|
|
216
|
-
index: number;
|
|
217
|
-
rawProps: Record<string, unknown>;
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## Edge Cases and Gotchas
|
|
224
|
-
|
|
225
|
-
### 1. CMS Lazy.tsx is the source of truth
|
|
226
|
-
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`).
|
|
227
|
-
|
|
228
|
-
### 2. Block references to Lazy
|
|
229
|
-
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.
|
|
230
|
-
|
|
231
|
-
### 3. alwaysEager overrides Lazy wrapping
|
|
232
|
-
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.
|
|
233
|
-
|
|
234
|
-
### 4. Multivariate flags are always eager
|
|
235
|
-
Individual sections wrapped in `website/flags/multivariate.ts` require runtime matcher evaluation and can't be safely deferred.
|
|
236
|
-
|
|
237
|
-
### 5. InvalidCharacterError with section rendering
|
|
238
|
-
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.
|
|
239
|
-
|
|
240
|
-
### 6. Navigation flash prevention
|
|
241
|
-
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.
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Public API Summary
|
|
246
|
-
|
|
247
|
-
### From `@decocms/start/cms`
|
|
248
|
-
|
|
249
|
-
| Export | Type | Description |
|
|
250
|
-
|--------|------|-------------|
|
|
251
|
-
| `setAsyncRenderingConfig` | Function | Enable/configure async rendering |
|
|
252
|
-
| `getAsyncRenderingConfig` | Function | Read current config |
|
|
253
|
-
| `registerCacheableSections` | Function | Register sections for SWR loader caching |
|
|
254
|
-
| `runSingleSectionLoader` | Function | Run a single section's loader |
|
|
255
|
-
| `resolveDeferredSection` | Function | Fully resolve a deferred section's raw props |
|
|
256
|
-
| `preloadSectionModule` | Function | Eagerly import a section to extract LoadingFallback |
|
|
257
|
-
|
|
258
|
-
### From `@decocms/start/routes`
|
|
259
|
-
|
|
260
|
-
| Export | Type | Description |
|
|
261
|
-
|--------|------|-------------|
|
|
262
|
-
| `loadDeferredSection` | ServerFn | Server function to resolve + enrich deferred section on demand |
|
|
263
|
-
|
|
264
|
-
### From `@decocms/start/hooks`
|
|
265
|
-
|
|
266
|
-
| Export | Type | Description |
|
|
267
|
-
|--------|------|-------------|
|
|
268
|
-
| `DecoPageRenderer` | Component | Renders page with eager + deferred section support |
|
|
269
|
-
| `SectionRenderer` | Component | Renders a single section by registry key |
|
|
270
|
-
| `SectionList` | Component | Renders an array of sections |
|