@decocms/start 0.39.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 +1 -1
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +0 -270
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: deco-tanstack-storefront-patterns
|
|
3
|
-
description: Runtime patterns, fixes, and learnings for Deco storefronts running on TanStack Start/React/Cloudflare Workers. Covers nested sections, dev cache control, workerEntry guards, section loaders, Fresh-to-React JSX porting, VTEX API resilience, node:async_hooks client bundle leak, SliderJS DOM timing, cart button loading state, server functions for VTEX actions, DOM manipulation conflicts with React, TypeScript patterns, loader cache/cacheKey module exports via createCachedLoaderFromModule, and the full multi-layer VTEX cache architecture. Also covers: setup.ts import order for server functions (client-nav 404 fix), globalThis backing for module-level state (split-module RPC problem), async rendering/deferred sections with IntersectionObserver, loadDeferredSection POST instead of GET (431 fix), section ordering with index stamping (mergeSections sort-based), layout section caching (5min TTL for Header/Footer), analytics hydration mismatch fix (useScript + suppressHydrationWarning), and vite.config.ts requirements for published @decocms/start packages (esbuildOptions jsx automatic + virtual module fallback + resolve.dedupe). Use when debugging or enhancing a deco-start storefront after the initial migration.
|
|
4
|
-
---
|
|
5
1
|
|
|
6
2
|
# Deco TanStack Storefront Patterns
|
|
7
3
|
|
|
8
4
|
Patterns and fixes discovered while porting and running `espacosmart-storefront` on the `@decocms/start` + TanStack Start stack. These apply to **any** Deco site after the initial migration.
|
|
9
5
|
|
|
10
|
-
## When to Use This
|
|
6
|
+
## When to Use This Reference
|
|
11
7
|
|
|
12
8
|
- Debugging runtime errors in a deco-start storefront
|
|
13
9
|
- Porting sections that use nested sections (`{ Component, props }`)
|
|
@@ -994,135 +990,3 @@ export default defineConfig({
|
|
|
994
990
|
- **Fix #1 (`esbuildOptions.jsx: "automatic"`)**: `@decocms/start` ships raw `.tsx` source. Vite's esbuild pre-bundler uses the classic transform by default. `jsx: "automatic"` makes it emit `import { jsx } from "react/jsx-runtime"` instead of `React.createElement`.
|
|
995
991
|
- **Fix #2 (virtual module fallback)**: The import chain `cmsRoute.ts → @tanstack/react-start → @tanstack/start-server-core → router-manifest.js` reaches the client bundle. The `tanstack-start-injected-head-scripts:v` virtual is registered with `applyToEnvironment: server` only.
|
|
996
992
|
- **Fix #3 (dedupe)**: Prevents duplicate TanStack instances when hoisting from peer deps.
|
|
997
|
-
|
|
998
|
-
---
|
|
999
|
-
|
|
1000
|
-
## 25. SEO Architecture — Three Layers
|
|
1001
|
-
|
|
1002
|
-
### Problem
|
|
1003
|
-
|
|
1004
|
-
Sites migrated from Fresh/Deno often have broken SEO:
|
|
1005
|
-
- `Seo.tsx` component returns `null` (dead stub from migration)
|
|
1006
|
-
- Route `head()` functions only set `<title>`, missing description/canonical/OG
|
|
1007
|
-
- No JSON-LD structured data on PDPs
|
|
1008
|
-
- No Open Graph tags for social sharing
|
|
1009
|
-
|
|
1010
|
-
### Solution — Framework + Section + Component
|
|
1011
|
-
|
|
1012
|
-
**Layer 1: Framework head()** (`cmsRouteConfig` / `cmsHomeRouteConfig`)
|
|
1013
|
-
Automatically emits title, description, canonical, OG, Twitter Card, robots from `DecoPageResult.seo`. Requires `defaultDescription` option and `registerSeoSections()` in setup.
|
|
1014
|
-
|
|
1015
|
-
**Layer 2: Section SEO** (`registerSeoSections` + `registerSectionLoaders`)
|
|
1016
|
-
Sections like SEOPDP register as SEO contributors. Their section loader computes title/description/canonical from commerce data (product name, product description, breadcrumb canonical). The framework extracts these into `PageSeo`.
|
|
1017
|
-
|
|
1018
|
-
**Layer 3: Structured data** (section component renders JSON-LD)
|
|
1019
|
-
The `Seo.tsx` component renders `<script type="application/ld+json">` for Product, WebSite, BreadcrumbList schemas. Meta tags are NOT rendered here — the framework handles those via `head()`.
|
|
1020
|
-
|
|
1021
|
-
### Setup Checklist
|
|
1022
|
-
|
|
1023
|
-
```typescript
|
|
1024
|
-
// setup.ts
|
|
1025
|
-
import { registerSeoSections } from "@decocms/start/cms";
|
|
1026
|
-
|
|
1027
|
-
registerSeoSections(["site/sections/SEOPDP.tsx"]);
|
|
1028
|
-
|
|
1029
|
-
// Also register the SEOPDP section loader:
|
|
1030
|
-
registerSectionLoaders({
|
|
1031
|
-
"site/sections/SEOPDP.tsx": async (props, req) => {
|
|
1032
|
-
const mod = await import("./sections/SEOPDP");
|
|
1033
|
-
return mod.loader(props, req, { seo: {} } as any) ?? props;
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
```typescript
|
|
1039
|
-
// routes/$.tsx — spread full config, framework handles SEO
|
|
1040
|
-
const routeConfig = cmsRouteConfig({
|
|
1041
|
-
siteName: "My Store",
|
|
1042
|
-
defaultTitle: "My Store - Default Title",
|
|
1043
|
-
defaultDescription: "My Store — best products...",
|
|
1044
|
-
ignoreSearchParams: ["skuId"],
|
|
1045
|
-
});
|
|
1046
|
-
export const Route = createFileRoute("/$")({ ...routeConfig, component: CmsPage });
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
// __root.tsx — fallback description and OG globals
|
|
1051
|
-
head: () => ({
|
|
1052
|
-
meta: [
|
|
1053
|
-
{ charSet: "utf-8" },
|
|
1054
|
-
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
1055
|
-
{ title: "My Store - Default Title" },
|
|
1056
|
-
{ name: "description", content: "My Store — default description." },
|
|
1057
|
-
{ property: "og:site_name", content: "My Store" },
|
|
1058
|
-
{ property: "og:locale", content: "pt_BR" },
|
|
1059
|
-
],
|
|
1060
|
-
}),
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
### Anti-Patterns
|
|
1064
|
-
|
|
1065
|
-
- `Seo.tsx` returning `null` — must render JSON-LD at minimum
|
|
1066
|
-
- Custom `head()` in routes that only sets title — use `cmsRouteConfig` which handles full SEO
|
|
1067
|
-
- Hardcoded Device.Provider — use matchMedia for client, section loaders for server
|
|
1068
|
-
|
|
1069
|
-
---
|
|
1070
|
-
|
|
1071
|
-
## 26. Device Detection — No Hardcoded Provider
|
|
1072
|
-
|
|
1073
|
-
### Problem
|
|
1074
|
-
|
|
1075
|
-
Sites often have `<Device.Provider value={{ isMobile: false }}>` in `__root.tsx`, making ALL client-side components think they're on desktop regardless of actual viewport.
|
|
1076
|
-
|
|
1077
|
-
### Solution — Two-Layer Detection
|
|
1078
|
-
|
|
1079
|
-
**Server-side**: Section loaders via `registerSectionLoaders()` detect device from UA and inject `isMobile` / `device` as props. The page result also includes `device` for client use.
|
|
1080
|
-
|
|
1081
|
-
**Client-side**: Use `useSyncExternalStore` + `window.matchMedia` instead of React context:
|
|
1082
|
-
|
|
1083
|
-
```typescript
|
|
1084
|
-
// contexts/device.tsx
|
|
1085
|
-
import { useSyncExternalStore } from "react";
|
|
1086
|
-
|
|
1087
|
-
const MOBILE_QUERY = "(max-width: 767px)";
|
|
1088
|
-
|
|
1089
|
-
function subscribe(cb: () => void) {
|
|
1090
|
-
if (typeof window === "undefined") return () => {};
|
|
1091
|
-
const mql = window.matchMedia(MOBILE_QUERY);
|
|
1092
|
-
mql.addEventListener("change", cb);
|
|
1093
|
-
return () => mql.removeEventListener("change", cb);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
function getSnapshot() { return window.matchMedia(MOBILE_QUERY).matches; }
|
|
1097
|
-
function getServerSnapshot() { return false; }
|
|
1098
|
-
|
|
1099
|
-
export const useDevice = () => {
|
|
1100
|
-
const isMobile = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
1101
|
-
return { isMobile };
|
|
1102
|
-
};
|
|
1103
|
-
```
|
|
1104
|
-
|
|
1105
|
-
### Why matchMedia > UA context
|
|
1106
|
-
|
|
1107
|
-
- Reflects actual viewport, not a server guess
|
|
1108
|
-
- Reactive — updates if user resizes browser
|
|
1109
|
-
- No Provider needed — works anywhere in the component tree
|
|
1110
|
-
- SSR defaults to desktop (false), hydrates to real value
|
|
1111
|
-
|
|
1112
|
-
---
|
|
1113
|
-
|
|
1114
|
-
## Related Skills
|
|
1115
|
-
|
|
1116
|
-
| Skill | Purpose |
|
|
1117
|
-
|-------|---------|
|
|
1118
|
-
| `deco-to-tanstack-migration` | Initial migration playbook (imports, signals, architecture) |
|
|
1119
|
-
| `deco-cms-route-config` | Route config, SEO architecture, device detection |
|
|
1120
|
-
| `deco-tanstack-navigation` | SPA navigation patterns (`<Link>`, `useNavigate`, `loaderDeps`) |
|
|
1121
|
-
| `deco-islands-migration` | Eliminating the `src/islands/` directory |
|
|
1122
|
-
| `deco-edge-caching` | Edge caching with `createDecoWorkerEntry` |
|
|
1123
|
-
| `deco-vtex-fetch-cache` | SWR fetch cache for VTEX APIs (`fetchWithCache`, `vtexCachedFetch`) |
|
|
1124
|
-
| `deco-api-call-dedup` | API call dedup, batching, PLP filtering patterns |
|
|
1125
|
-
| `deco-cms-layout-caching` | Layout section caching in CMS resolve |
|
|
1126
|
-
| `deco-typescript-fixes` | TypeScript error patterns and fixes |
|
|
1127
|
-
| `deco-storefront-test-checklist` | Context-aware QA checklist generation |
|
|
1128
|
-
| `deco-startup-analysis` | Analyzing startup logs for issues |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Vite Configuration
|
|
2
|
+
|
|
3
|
+
## Final Config (Post-Migration)
|
|
4
|
+
|
|
5
|
+
After all imports are rewritten, the config should be minimal:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
9
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
10
|
+
import react from "@vitejs/plugin-react";
|
|
11
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
12
|
+
import { defineConfig } from "vite";
|
|
13
|
+
import path from "path";
|
|
14
|
+
|
|
15
|
+
const srcDir = path.resolve(__dirname, "src");
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
plugins: [
|
|
19
|
+
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
|
20
|
+
tanstackStart({ server: { entry: "server" } }),
|
|
21
|
+
react({
|
|
22
|
+
babel: {
|
|
23
|
+
plugins: [["babel-plugin-react-compiler", { target: "19" }]],
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
tailwindcss(),
|
|
27
|
+
],
|
|
28
|
+
resolve: {
|
|
29
|
+
alias: {
|
|
30
|
+
"~": srcDir,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**One alias only**: `~` -> `src/`. Nothing else.
|
|
37
|
+
|
|
38
|
+
## tsconfig.json
|
|
39
|
+
|
|
40
|
+
Must mirror the Vite alias:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"compilerOptions": {
|
|
45
|
+
"jsx": "react-jsx",
|
|
46
|
+
"moduleResolution": "bundler",
|
|
47
|
+
"module": "ESNext",
|
|
48
|
+
"target": "ES2022",
|
|
49
|
+
"skipLibCheck": true,
|
|
50
|
+
"strictNullChecks": true,
|
|
51
|
+
"baseUrl": ".",
|
|
52
|
+
"paths": {
|
|
53
|
+
"~/*": ["./src/*"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"include": ["src/**/*", "vite.config.ts"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No `$store/*`, `site/*`, `apps/*`, `preact`, `@preact/signals`, `@deco/deco` paths. Those are all dead.
|
|
61
|
+
|
|
62
|
+
## React Compiler
|
|
63
|
+
|
|
64
|
+
The `babel-plugin-react-compiler` with `target: "19"` enables automatic memoization. Requires `@vitejs/plugin-react` instead of the default SWC plugin.
|
|
65
|
+
|
|
66
|
+
Install: `npm install -D @vitejs/plugin-react babel-plugin-react-compiler`
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
For VTEX API keys, use Cloudflare Workers secrets or `.dev.vars`:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
VTEX_ACCOUNT=mystore
|
|
74
|
+
VTEX_APP_KEY=...
|
|
75
|
+
VTEX_APP_TOKEN=...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Accessed via `process.env.*` in `createServerFn` handlers.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# VTEX Commerce Gotchas
|
|
2
|
+
|
|
3
|
+
> Section loaders, cart CORS, price specs, facets, URL-blind loaders, cookie handling.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## 1. Section Loaders Don't Execute
|
|
7
|
+
|
|
8
|
+
Deco sections have `export const loader = async (props, req, ctx) => { ... }` that runs server-side before the component renders. In TanStack Start, these don't execute automatically. Components typed as `SectionProps<typeof loader>` expect the augmented props, but only receive the raw CMS block props.
|
|
9
|
+
|
|
10
|
+
**Symptom**: Components crash on `.find()`, `.length`, or property access of loader-provided props that are `undefined`.
|
|
11
|
+
|
|
12
|
+
**Fix**: Register them via `registerSectionLoaders()` in `setup.ts`.
|
|
13
|
+
|
|
14
|
+
**Safe-default pattern** (most pragmatic for initial migration):
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Before: component expects loader-augmented props
|
|
18
|
+
function ProductMain({ page, productAdditional, showTogether, priceSimulation, isMobile }: SectionProps<typeof loader>) {
|
|
19
|
+
|
|
20
|
+
// After: destructure with safe defaults for all loader-only props
|
|
21
|
+
function ProductMain(rawProps: any) {
|
|
22
|
+
const {
|
|
23
|
+
page,
|
|
24
|
+
productAdditional = [], // from section loader
|
|
25
|
+
showTogether = [], // from section loader
|
|
26
|
+
showTogetherSimulation = [], // from section loader
|
|
27
|
+
priceSimulation = 0, // from section loader
|
|
28
|
+
noInterestInstallmentValue = null,
|
|
29
|
+
skuProductsKit = [], // from section loader
|
|
30
|
+
isMobile = false, // from section loader (device detection)
|
|
31
|
+
} = rawProps;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This lets the core component render while gracefully degrading features that depend on loader data (cross-selling, price simulation, etc.).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## 7. VTEX API Auth on Cloudflare Workers
|
|
38
|
+
|
|
39
|
+
Env vars must be set via `wrangler secret put` or `.dev.vars`, not `.env`.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## 8. Cookie Handling
|
|
43
|
+
|
|
44
|
+
In TanStack Start, manage `checkout.vtex.com__orderFormId` cookies manually via `document.cookie`.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## 32. Section Loader Logic Must Not Be Stripped
|
|
48
|
+
|
|
49
|
+
**Severity**: HIGH — sections render empty/broken
|
|
50
|
+
|
|
51
|
+
During migration, section loaders (e.g., `sections/Header/Header.tsx`) may have their async data-fetching logic removed. For example, the `ctx.invoke.vtex.loaders.categories.tree()` call that populates navigation menus. Without it, the header renders with no category links.
|
|
52
|
+
|
|
53
|
+
**Fix**: Keep all section loader logic intact. The loader signature `(props, req, ctx) => {...}` and the `ctx.invoke` calls should be preserved as-is.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## 34. Commerce Loaders Are Blind to the URL
|
|
57
|
+
|
|
58
|
+
**Severity**: CRITICAL — search and category pages return wrong/no products
|
|
59
|
+
|
|
60
|
+
When `resolve.ts` processes CMS blocks, it passes only the static CMS block props to commerce loaders (PLP, PDP). The current URL, query string (`?q=`), path (`/drywall`), sort, pagination, and filter parameters are never forwarded.
|
|
61
|
+
|
|
62
|
+
**Symptom**: Search pages (`/s?q=parafuso`) return zero products. Category pages (`/drywall`) show random/no products. Sort and pagination controls do nothing.
|
|
63
|
+
|
|
64
|
+
**Root cause**: `resolveValue()` in `resolve.ts` calls commerce loaders with `resolvedProps` (CMS block config only). The `matcherCtx` (containing URL, path, user-agent) is used for matcher evaluation but never passed to commerce loaders.
|
|
65
|
+
|
|
66
|
+
**Fix**: Pass `matcherCtx` as a second argument to commerce loaders in `resolve.ts`. Then the PLP loader can extract `?q=` for search, path for categories, `?sort=` for sorting, `?page=` for pagination, and `?filter.X=Y` for facets.
|
|
67
|
+
|
|
68
|
+
This is a change in `@decocms/start` (resolve.ts). Until upstreamed, use patch-package or vendor the file.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## 35. VTEX Product Loaders Ship with Empty priceSpecification
|
|
72
|
+
|
|
73
|
+
**Severity**: HIGH — no discount badges, no strikethrough prices, no installments
|
|
74
|
+
|
|
75
|
+
All three VTEX product loaders (`vtexProductList`, `productListingPage`, `productDetailsPage`) build offers with `priceSpecification: []`. The `useOffer()` hook depends on this array to extract `ListPrice` (for discount math + strikethrough), `SalePrice`, and `Installment` entries.
|
|
76
|
+
|
|
77
|
+
**Symptom**: Product cards show only one price (no strikethrough). No "X% OFF" discount badge. No "Ou em Nx de R$ X sem juros" installment text.
|
|
78
|
+
|
|
79
|
+
**Fix**: Add a `buildPriceSpecification()` helper to each loader that transforms the VTEX `commertialOffer` data:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
function buildPriceSpecification(offer: any): any[] {
|
|
83
|
+
const specs: any[] = [];
|
|
84
|
+
if (offer.ListPrice != null) {
|
|
85
|
+
specs.push({ "@type": "UnitPriceSpecification", priceType: "https://schema.org/ListPrice", price: offer.ListPrice });
|
|
86
|
+
}
|
|
87
|
+
if (offer.Price != null) {
|
|
88
|
+
specs.push({ "@type": "UnitPriceSpecification", priceType: "https://schema.org/SalePrice", price: offer.Price });
|
|
89
|
+
}
|
|
90
|
+
// Find best no-interest installment
|
|
91
|
+
const noInterest = (offer.Installments ?? [])
|
|
92
|
+
.filter((i: any) => i.InterestRate === 0)
|
|
93
|
+
.sort((a: any, b: any) => b.NumberOfInstallments - a.NumberOfInstallments);
|
|
94
|
+
if (noInterest.length > 0) {
|
|
95
|
+
const best = noInterest[0];
|
|
96
|
+
specs.push({
|
|
97
|
+
"@type": "UnitPriceSpecification",
|
|
98
|
+
priceType: "https://schema.org/SalePrice",
|
|
99
|
+
priceComponentType: "https://schema.org/Installment",
|
|
100
|
+
billingDuration: best.NumberOfInstallments,
|
|
101
|
+
billingIncrement: best.Value,
|
|
102
|
+
price: best.TotalValuePlusInterestRate,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return specs;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This is a change in `@decocms/apps`. Until upstreamed, patch or vendor the loader files.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## 36. VTEX Facets API Response Structure Mismatch
|
|
113
|
+
|
|
114
|
+
The VTEX Intelligent Search facets endpoint returns `{ facets: ISFacetGroup[] }`, NOT a direct `ISFacetGroup[]` array. Accessing `response` directly as an array yields no filter data.
|
|
115
|
+
|
|
116
|
+
Additionally, `PRICERANGE` facets must be converted to `FilterToggle` format (with `value: "min:max"` strings) for the existing `Filters.tsx` component to render them. The component's `isToggle()` filter drops anything that isn't `FilterToggle`.
|
|
117
|
+
|
|
118
|
+
**Fix**: Unwrap with `const facetGroups = response.facets ?? [];` and convert price ranges:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
if (group.type === "PRICERANGE") {
|
|
122
|
+
return { "@type": "FilterToggle" as const, key: "price", label: group.name, quantity: 0,
|
|
123
|
+
values: group.values.map((v) => ({
|
|
124
|
+
value: `${v.range.from}:${v.range.to}`, label: `R$ ${v.range.from} - R$ ${v.range.to}`,
|
|
125
|
+
quantity: v.quantity, selected: false, url: `?filter.price=${v.range.from}:${v.range.to}`,
|
|
126
|
+
})),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## 39. Cart Requires Server-Side Proxy for VTEX API (CORS)
|
|
133
|
+
|
|
134
|
+
**Severity**: HIGH — add-to-cart, minicart, and checkout flow completely broken
|
|
135
|
+
|
|
136
|
+
The storefront domain (e.g., `espacosmart-tanstack.deco.site`) differs from the VTEX checkout domain (`lojaespacosmart.vtexcommercestable.com.br`). Direct browser `fetch()` calls to VTEX are blocked by CORS. Additionally, the `checkout.vtex.com__orderFormId` cookie is scoped to the VTEX domain and inaccessible from the storefront.
|
|
137
|
+
|
|
138
|
+
**Fix**: Use TanStack Start `createServerFn` to create server-side proxy functions:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// src/lib/vtex-cart-server.ts
|
|
142
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
143
|
+
|
|
144
|
+
export const getOrCreateCart = createServerFn({ method: "GET" })
|
|
145
|
+
.validator((orderFormId: string) => orderFormId)
|
|
146
|
+
.handler(async ({ data: orderFormId }) => {
|
|
147
|
+
const url = orderFormId
|
|
148
|
+
? `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${orderFormId}`
|
|
149
|
+
: `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm`;
|
|
150
|
+
const res = await fetch(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
|
|
153
|
+
body: JSON.stringify({ expectedOrderFormSections: ["items", "totalizers", "shippingData", "clientPreferencesData", "storePreferencesData", "marketingData"] }),
|
|
154
|
+
});
|
|
155
|
+
return res.json();
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The `useCart` hook manages the `orderFormId` in a client-side cookie and calls these server functions.
|
|
160
|
+
|
|
161
|
+
**Checkout URL**: The minicart's "Finalizar Compra" link must append the `orderFormId` as a query parameter since the VTEX checkout domain can't read the storefront's cookies:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const checkoutUrl = `https://secure.${STORE_DOMAIN}/checkout/?orderFormId=${orderFormId}`;
|
|
165
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Worker / Cloudflare / Build Gotchas
|
|
2
|
+
|
|
3
|
+
> TanStack worker entry stripping, setup ordering, AsyncLocalStorage, cache, npm.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## 9. Build Succeeds but Runtime Fails
|
|
7
|
+
|
|
8
|
+
After import rewrites, always test: build → dev → visit pages → test interactive features.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 10. npm link for Local Dev
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd apps-start && npm link
|
|
15
|
+
cd ../deco-start && npm link
|
|
16
|
+
cd ../my-store && npm link @decocms/apps @decocms/start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## 12. No Compat Layers
|
|
21
|
+
|
|
22
|
+
After migration: no `src/compat/`, only `~/*` alias, zero compat files in packages.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## 13. AsyncLocalStorage in Client Bundles
|
|
26
|
+
|
|
27
|
+
Use namespace import + runtime conditional (or the `deco-server-only-stubs` Vite plugin).
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## 14. TanStack Start Ignores Custom Worker Entry Code
|
|
31
|
+
|
|
32
|
+
**Severity**: CRITICAL -- cache logic, admin routes, and any custom request interception will silently not work in production.
|
|
33
|
+
|
|
34
|
+
TanStack Start's Cloudflare adapter **completely ignores** the `export default` in `server.ts`. It generates its own Worker entry that calls `createStartHandler(defaultStreamHandler)` directly. Custom logic inside `createServerEntry({ async fetch(request) { ... } })` is also stripped by Vite/Rollup in production builds.
|
|
35
|
+
|
|
36
|
+
**Symptom**: Admin routes like `/live/_meta` return HTML instead of JSON. Edge caching (Cache API, X-Cache headers) doesn't work despite being implemented. Every request hits the origin at full SSR cost. The `Cache-Control` headers from route-level `headers()` functions appear correctly (because TanStack applies them), but the custom `X-Cache` header and cache storage never execute.
|
|
37
|
+
|
|
38
|
+
**Diagnosis**: Search the built `dist/server/worker-entry-*.js` bundle for your custom code (e.g., `X-Cache`, `caches.open`, `_cache/purge`). If absent, TanStack stripped it.
|
|
39
|
+
|
|
40
|
+
**Fix**: Create a **separate** `src/worker-entry.ts` file that wraps TanStack Start's built handler. Point `wrangler.jsonc` to this file instead of `@tanstack/react-start/server-entry`.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/worker-entry.ts
|
|
44
|
+
import "./setup";
|
|
45
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
46
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
47
|
+
import { handleMeta, handleDecofileRead, handleDecofileReload, handleRender, corsHeaders } from "@decocms/start/admin";
|
|
48
|
+
|
|
49
|
+
const serverEntry = createServerEntry({
|
|
50
|
+
async fetch(request) {
|
|
51
|
+
return await handler.fetch(request);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default createDecoWorkerEntry(serverEntry, {
|
|
56
|
+
admin: { handleMeta, handleDecofileRead, handleDecofileReload, handleRender, corsHeaders },
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```jsonc
|
|
61
|
+
// wrangler.jsonc -- MUST point to custom entry, NOT the default
|
|
62
|
+
{
|
|
63
|
+
"main": "./src/worker-entry.ts",
|
|
64
|
+
// NOT: "main": "@tanstack/react-start/server-entry"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This ensures admin route interception AND edge caching survive the build because they're in the Worker's own fetch handler, outside of TanStack's build pipeline.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## 19. `import "./setup"` Ordering (CRITICAL)
|
|
72
|
+
|
|
73
|
+
`import "./setup"` MUST be the first import in both `server.ts` and `worker-entry.ts`. Without it, server functions in Vite split modules execute before `setBlocks()` has been called, causing `resolveDecoPage` to return null → 404 on client-side navigation.
|
|
74
|
+
|
|
75
|
+
**Symptom**: SSR works fine (F5), but clicking links shows "No CMS page block matches this URL".
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## 20. loadDeferredSection Must Use POST
|
|
79
|
+
|
|
80
|
+
Without this, the admin shows "Incorrect type. Expected 'array'" for fields that contain loader references in the `.decofile`.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## 24. new URL() with Relative Paths Fails in Workers
|
|
84
|
+
|
|
85
|
+
`new URL("/product/p")` works in browsers (uses `window.location` as base) but throws `Invalid URL` in Workers/Node because there's no implicit base.
|
|
86
|
+
|
|
87
|
+
**Fix**: Always provide a base URL:
|
|
88
|
+
```typescript
|
|
89
|
+
const parsed = new URL(url, "https://localhost");
|
|
90
|
+
return parsed.pathname + parsed.search;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## 25. Global Variables Throw ReferenceError
|
|
95
|
+
|
|
96
|
+
Code that references undeclared globals (e.g., `userAddressData` injected by VTEX scripts) will throw `ReferenceError: X is not defined` in Workers where those scripts don't run.
|
|
97
|
+
|
|
98
|
+
**Fix**: Access via `globalThis`:
|
|
99
|
+
```typescript
|
|
100
|
+
const data = (globalThis as any).userAddressData;
|
|
101
|
+
if (data && Array.isArray(data)) { /* use data */ }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## 26. Section-Type Props Use __resolveType Format
|
|
106
|
+
|
|
107
|
+
In the new `@decocms/start`, section-type props from the CMS arrive as `{ __resolveType: "site/sections/Foo.tsx", ...props }`, NOT the old `{ Component, props }` format. Components that render section props must handle this.
|
|
108
|
+
|
|
109
|
+
**Fix**: Create a `RenderSection` bridge component that:
|
|
110
|
+
1. Checks for `section.Component` (old format) and renders directly
|
|
111
|
+
2. Checks for `section.__resolveType` (new format), resolves via `getSection()` from `@decocms/start/cms`, and renders with `React.lazy` + `Suspense`
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## 27. jsdom Must Be Replaced in Workers
|
|
115
|
+
|
|
116
|
+
`jsdom` is a heavy Node.js dependency that cannot run in Cloudflare Workers. Components using it for HTML sanitization must use `dompurify` instead.
|
|
117
|
+
|
|
118
|
+
**Fix**: Replace `import { JSDOM } from "jsdom"` with:
|
|
119
|
+
```typescript
|
|
120
|
+
import DOMPurify from "dompurify";
|
|
121
|
+
const clean = typeof document !== "undefined" ? DOMPurify.sanitize(html) : html;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## 28. Deno npm: Prefix Must Be Removed
|
|
126
|
+
|
|
127
|
+
Imports like `import Color from "npm:colorjs.io"` use the Deno-specific `npm:` prefix. Vite/Node don't understand it.
|
|
128
|
+
|
|
129
|
+
**Fix**: Remove the `npm:` prefix and install the package: `npm install colorjs.io`.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## 30. Stale Edge Cache After Deploy Requires Explicit Purge
|
|
133
|
+
|
|
134
|
+
**Severity**: MEDIUM — causes "Failed to fetch dynamically imported module" errors
|
|
135
|
+
|
|
136
|
+
After deploying a new build to Cloudflare Workers, the edge cache may still serve old HTML that references previous JS bundle hashes. This causes module import failures.
|
|
137
|
+
|
|
138
|
+
**Fix**: After every deploy, purge the cache:
|
|
139
|
+
1. Set a `PURGE_TOKEN` secret: `npx wrangler secret put PURGE_TOKEN`
|
|
140
|
+
2. Call the purge endpoint: `POST /_cache/purge` with `Authorization: Bearer <token>` and body `{"paths":["/"]}`
|
|
141
|
+
3. Automate this in CI/CD (see the deploy.yml workflow)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## 44. Runtime Module Import Kills Lazy-Loaded Sections
|
|
145
|
+
|
|
146
|
+
**Severity**: HIGH — sections silently disappear, data appears in RSC streaming but component renders nothing
|
|
147
|
+
|
|
148
|
+
Vite tree-shakes unused imports in production builds, so a section file that imports a non-existent module may pass `npm run build` without errors. But at runtime, when the section is dynamically imported via `registerSections`'s lazy `() => import("./sections/X")`, ALL imports in the module execute. A missing file kills the entire section module.
|
|
149
|
+
|
|
150
|
+
**Symptom**: Product shelves or other sections disappear. HTML size drops significantly. Product data appears in React streaming data (`$R[...]` notation) but zero product cards render as actual HTML. No error in the build log.
|
|
151
|
+
|
|
152
|
+
**Example**:
|
|
153
|
+
```typescript
|
|
154
|
+
// sections/Product/ProductShelf.tsx
|
|
155
|
+
import LoadingCard from "~/components/product/loadingCard"; // file doesn't exist!
|
|
156
|
+
export { default, loader } from "~/components/product/ProductShelf";
|
|
157
|
+
|
|
158
|
+
export function LoadingFallback() {
|
|
159
|
+
return <LoadingCard />; // only used here — tree-shaken in build
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Build passes because `LoadingFallback` is a named export that nothing imports. But at runtime, the dynamic `import("./sections/Product/ProductShelf")` executes the module, hits the missing `loadingCard` import, and the entire section fails to load.
|
|
164
|
+
|
|
165
|
+
**Fix**: Create the missing file, even if it's a minimal stub:
|
|
166
|
+
```typescript
|
|
167
|
+
// components/product/loadingCard.tsx
|
|
168
|
+
export default function LoadingCard() {
|
|
169
|
+
return <div className="animate-pulse bg-base-200 h-[400px] w-[200px] rounded" />;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Prevention**: After copying files from the original repo, verify all imports resolve:
|
|
174
|
+
```bash
|
|
175
|
+
npx tsc --noEmit # catches missing modules that Vite's tree-shaking hides
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## 45. GitHub Packages npm Requires Auth Even for Public Packages
|
|
180
|
+
|
|
181
|
+
**Severity**: MEDIUM — blocks dependency installation for new contributors and CI
|
|
182
|
+
|
|
183
|
+
GitHub Packages' npm registry (`npm.pkg.github.com`) requires authentication even for public packages. This is a known limitation that GitHub has not resolved. Attempting to `npm install` a public `@decocms/*` package without a token returns `401 Unauthorized`.
|
|
184
|
+
|
|
185
|
+
**Workaround A (recommended for development)**: Use `github:` Git URL syntax instead of npm registry references. This bypasses the npm registry entirely and uses Git HTTPS (no auth needed for public repos):
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"@decocms/apps": "github:decocms/apps-start",
|
|
190
|
+
"@decocms/start": "github:decocms/deco-start#main"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Important**: The repo name in the `github:` URL must match the actual GitHub repo name, not the npm package name. `@decocms/start` is published from repo `decocms/deco-start`, NOT `decocms/start`.
|
|
195
|
+
|
|
196
|
+
**Workaround B (recommended for production)**: Publish to npmjs.com instead. Only npm's public registry supports truly zero-auth public package installation.
|
|
197
|
+
|
|
198
|
+
**Workaround C (if you must use GitHub Packages)**: Generate a GitHub PAT with `read:packages` scope and configure:
|
|
199
|
+
```bash
|
|
200
|
+
npm config set //npm.pkg.github.com/:_authToken <YOUR_TOKEN>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Or in project `.npmrc` with an env var (for CI):
|
|
204
|
+
```
|
|
205
|
+
@decocms:registry=https://npm.pkg.github.com
|
|
206
|
+
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Tradeoff with `github:` syntax**: No semver resolution — `npm update` is meaningless. Pin to a tag for stability: `github:decocms/deco-start#v0.14.2`. Without a tag, you get HEAD of the default branch.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# package.json Template
|
|
2
|
+
|
|
3
|
+
```json
|
|
4
|
+
{
|
|
5
|
+
"name": "my-tanstack-store",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "npm run generate && vite dev",
|
|
10
|
+
"build": "npm run generate && vite build",
|
|
11
|
+
"generate": "npm run generate:blocks && npm run generate:invoke && npm run generate:schema",
|
|
12
|
+
"generate:blocks": "tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
|
|
13
|
+
"generate:invoke": "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
|
|
14
|
+
"generate:schema": "tsx node_modules/@decocms/start/scripts/generate-schema.ts --site storefront",
|
|
15
|
+
"deploy": "wrangler deploy"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@decocms/apps": "^0.20.1",
|
|
19
|
+
"@decocms/start": "^0.16.4",
|
|
20
|
+
"@tanstack/react-query": "^5.90.21",
|
|
21
|
+
"@tanstack/react-router": "^1.166.2",
|
|
22
|
+
"@tanstack/react-router-devtools": "^1.166.2",
|
|
23
|
+
"@tanstack/react-start": "^1.166.2",
|
|
24
|
+
"@tanstack/react-store": "^0.9.1",
|
|
25
|
+
"react": "^19.2.4",
|
|
26
|
+
"react-dom": "^19.2.4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@cloudflare/vite-plugin": "^1.26.1",
|
|
30
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
31
|
+
"@tanstack/router-generator": "^1.166.0",
|
|
32
|
+
"@types/react": "^19.0.0",
|
|
33
|
+
"@types/react-dom": "^19.0.0",
|
|
34
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
35
|
+
"daisyui": "^5.5.19",
|
|
36
|
+
"tailwindcss": "^4.2.1",
|
|
37
|
+
"tsx": "^4.19.2",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vite": "^7.3.1",
|
|
40
|
+
"wrangler": "^4.14.1",
|
|
41
|
+
"@vitejs/plugin-react": "^4.5.2"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- `@decocms/start` and `@decocms/apps` come from GitHub Packages — needs `.npmrc`:
|
|
49
|
+
```
|
|
50
|
+
@decocms:registry=https://npm.pkg.github.com
|
|
51
|
+
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
|
|
52
|
+
```
|
|
53
|
+
- Set `NODE_AUTH_TOKEN` in `.env` (add `.env` to `.gitignore`)
|
|
54
|
+
- `generate` scripts run before dev and build to produce `blocks.gen.ts`, `invoke.gen.ts`, `meta.gen.json`
|
|
55
|
+
- `tsx` is needed for the generate scripts (TypeScript execution)
|