@decocms/start 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
- package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
- package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
- package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
- package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
- package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
- package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
- package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
- package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
- package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
- package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
- package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
- package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
- package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
- package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
- package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
- package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
- package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
- package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
- package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
- package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
- package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
- package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
- package/.cursor/skills/deco-core-architecture/engine.md +220 -0
- package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
- package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
- package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
- package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
- package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
- package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
- package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
- package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
- package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
- package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
- package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
- package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
- package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
- package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
- package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
- package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
- package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
- package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
- package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
- package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
- package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
- package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
- package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
- package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
- package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
- package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
- package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
- package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
- package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
- package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
- package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
- package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
- package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
- package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
- package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
- package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
- package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
- package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
- package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
- package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
- package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
- package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
- package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
- package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
- package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
- package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
- package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
- package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
- package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
- package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
- package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
- package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
- package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
- package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
- package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
- package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
- package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
- package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
- package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
- package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
- package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
- package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
- package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
- package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
- package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
- package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
- package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
- package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
- package/.cursor/skills/find-skills/SKILL.md +133 -0
- package/.cursor/skills/incident-report/SKILL.md +179 -0
- package/.cursor/skills/incident-report/references/5-whys.md +75 -0
- package/.cursor/skills/incident-report/templates/client-report.md +187 -0
- package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
- package/.cursor/skills/template-skill/SKILL.md +38 -0
- package/.github/workflows/release.yml +32 -0
- package/.releaserc.json +25 -0
- package/CLAUDE.md +135 -0
- package/GAP_ANALYSIS.md +224 -0
- package/GAP_ANALYSIS_V2.md +1013 -0
- package/biome.json +39 -0
- package/knip.json +5 -0
- package/package.json +87 -0
- package/scripts/generate-blocks.ts +69 -0
- package/scripts/generate-invoke.ts +378 -0
- package/scripts/generate-schema.ts +657 -0
- package/src/admin/cors.ts +29 -0
- package/src/admin/decofile.ts +72 -0
- package/src/admin/index.ts +24 -0
- package/src/admin/invoke.ts +163 -0
- package/src/admin/liveControls.ts +29 -0
- package/src/admin/meta.ts +70 -0
- package/src/admin/render.ts +205 -0
- package/src/admin/schema.ts +686 -0
- package/src/admin/setup.ts +44 -0
- package/src/cms/index.ts +59 -0
- package/src/cms/loader.ts +180 -0
- package/src/cms/registry.ts +162 -0
- package/src/cms/resolve.ts +1005 -0
- package/src/cms/sectionLoaders.ts +294 -0
- package/src/hooks/DecoPageRenderer.tsx +444 -0
- package/src/hooks/LazySection.tsx +109 -0
- package/src/hooks/LiveControls.tsx +108 -0
- package/src/hooks/SectionErrorFallback.tsx +85 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/matchers/builtins.ts +184 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +131 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.ts +205 -0
- package/src/routes/adminRoutes.ts +83 -0
- package/src/routes/cmsRoute.ts +302 -0
- package/src/routes/components.tsx +34 -0
- package/src/routes/index.ts +15 -0
- package/src/sdk/analytics.ts +72 -0
- package/src/sdk/cacheHeaders.ts +268 -0
- package/src/sdk/cachedLoader.ts +206 -0
- package/src/sdk/clx.ts +3 -0
- package/src/sdk/cookie.ts +39 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/env.ts +27 -0
- package/src/sdk/index.ts +63 -0
- package/src/sdk/instrumentedFetch.ts +137 -0
- package/src/sdk/invoke.ts +133 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/redirects.ts +217 -0
- package/src/sdk/requestContext.ts +184 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +41 -0
- package/src/sdk/sitemap.ts +143 -0
- package/src/sdk/urlUtils.ts +117 -0
- package/src/sdk/useDevice.ts +82 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.ts +101 -0
- package/src/sdk/workerEntry.ts +703 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +13 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deco-tanstack-navigation
|
|
3
|
+
description: "Complete guide for migrating Fresh/Deno navigation to TanStack Router in Deco storefronts. Covers: replacing <a href> with <Link>, prefetch strategies for instant navigation, type-safe params, activeProps for menus, search state as URL source of truth, SSR-first SEO architecture, loaderDeps for reactive search params, form submissions via server actions, and programmatic preloading. Use when porting any Deco site from Fresh to TanStack Start."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deco TanStack Navigation Migration
|
|
7
|
+
|
|
8
|
+
Complete playbook for replacing Fresh/Deno navigation with TanStack Router in Deco storefronts. Goes beyond simple `<a>` to `<Link>` — covers the full power of the router to build sites that feel like native apps while keeping SSR-first SEO.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Migrating a Fresh/Deno storefront to TanStack Start
|
|
13
|
+
- Links cause full page reloads instead of SPA transitions
|
|
14
|
+
- Filters, sort, or search reload the entire page
|
|
15
|
+
- Forms submit via GET and append query params
|
|
16
|
+
- Navigation feels slow (no prefetching)
|
|
17
|
+
- Menus don't highlight the active page
|
|
18
|
+
- Need type-safe route params
|
|
19
|
+
- Want URL as the single source of truth for filters/pagination
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture: SSR-First, Hydrate Smart
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Request → Server
|
|
27
|
+
├─ TanStack Router matches route
|
|
28
|
+
├─ Route loader runs on server (createServerFn)
|
|
29
|
+
│ ├─ resolveDecoPage(path)
|
|
30
|
+
│ ├─ runSectionLoaders(sections, request)
|
|
31
|
+
│ └─ Return full page data
|
|
32
|
+
├─ React renders to HTML (SSR)
|
|
33
|
+
└─ Response: full HTML + serialized data
|
|
34
|
+
|
|
35
|
+
Client receives HTML
|
|
36
|
+
├─ Instantly visible (SEO, LCP, FCP)
|
|
37
|
+
├─ React hydrates (attaches event handlers)
|
|
38
|
+
├─ TanStack Router takes over navigation
|
|
39
|
+
└─ Subsequent navigations:
|
|
40
|
+
├─ Prefetch on hover/intent (data + component)
|
|
41
|
+
├─ Client-side render (no full page reload)
|
|
42
|
+
├─ Only the changed route re-renders
|
|
43
|
+
└─ Shared layout (header/footer) stays mounted
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This gives you:
|
|
47
|
+
- **SEO**: Full HTML on first request, crawlers see everything
|
|
48
|
+
- **Speed**: Prefetch makes subsequent pages feel instant
|
|
49
|
+
- **State**: Cart, menus, form inputs survive navigation
|
|
50
|
+
- **Bandwidth**: Only route data transfers, not the full HTML shell
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Pattern 1: `<a href>` to `<Link>` with Prefetch
|
|
55
|
+
|
|
56
|
+
### The Basic Migration
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// FRESH — full page reload on every click
|
|
60
|
+
<a href={url}>Click me</a>
|
|
61
|
+
|
|
62
|
+
// TANSTACK — SPA navigation, preserves state
|
|
63
|
+
import { Link } from "@tanstack/react-router";
|
|
64
|
+
<Link to={url}>Click me</Link>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Prefetch: Make Navigation Instant
|
|
68
|
+
|
|
69
|
+
The killer feature. The router can **preload the next page before the user clicks**.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Preload when user hovers or focuses the link
|
|
73
|
+
<Link to="/produtos" preload="intent">
|
|
74
|
+
Produtos
|
|
75
|
+
</Link>
|
|
76
|
+
|
|
77
|
+
// Preload immediately when the link renders (good for hero CTAs)
|
|
78
|
+
<Link to="/ofertas" preload="render">
|
|
79
|
+
Ver Ofertas
|
|
80
|
+
</Link>
|
|
81
|
+
|
|
82
|
+
// Disable prefetch (for low-priority links)
|
|
83
|
+
<Link to="/termos" preload={false}>
|
|
84
|
+
Termos de Uso
|
|
85
|
+
</Link>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**What gets preloaded:**
|
|
89
|
+
1. Route component code (the JS chunk)
|
|
90
|
+
2. Route loader data (the `createServerFn` call)
|
|
91
|
+
3. Any nested route data
|
|
92
|
+
|
|
93
|
+
When the user clicks, everything is already cached — **navigation is instant**.
|
|
94
|
+
|
|
95
|
+
### Prefetch Strategy by Component
|
|
96
|
+
|
|
97
|
+
| Component | Strategy | Why |
|
|
98
|
+
|-----------|----------|-----|
|
|
99
|
+
| Product card | `preload="intent"` | User will likely click after hover |
|
|
100
|
+
| NavItem (menu) | `preload="intent"` | High-intent interaction |
|
|
101
|
+
| Category link | `preload="intent"` | Top-of-funnel navigation |
|
|
102
|
+
| Hero CTA | `preload="render"` | Guaranteed next action |
|
|
103
|
+
| Breadcrumb | `preload="intent"` | Medium priority |
|
|
104
|
+
| Footer links | `preload={false}` | Rarely clicked |
|
|
105
|
+
| Filter options | N/A (use `useNavigate`) | Same page, different params |
|
|
106
|
+
|
|
107
|
+
### When NOT to Replace `<a>`
|
|
108
|
+
|
|
109
|
+
Keep native `<a href>` for:
|
|
110
|
+
- External links (`https://...` to other domains)
|
|
111
|
+
- Checkout redirects (VTEX checkout is on a different domain)
|
|
112
|
+
- Download links (href pointing to files)
|
|
113
|
+
- Anchor links (`#section-id`)
|
|
114
|
+
- `mailto:` / `tel:` links
|
|
115
|
+
|
|
116
|
+
### Discovery Command
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
rg '<a\s+href=' src/components/ src/sections/ --glob '*.tsx' -l
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Gotcha: VTEX URLs Are Absolute
|
|
123
|
+
|
|
124
|
+
VTEX APIs return absolute URLs. Always convert:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { relative } from "@decocms/apps/commerce/sdk/url";
|
|
128
|
+
|
|
129
|
+
<Link to={relative(product.url) ?? product.url} preload="intent">
|
|
130
|
+
{product.name}
|
|
131
|
+
</Link>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Pattern 2: Type-Safe Params
|
|
137
|
+
|
|
138
|
+
TanStack Router generates types from your route tree. Use them.
|
|
139
|
+
|
|
140
|
+
### Route Definition
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// src/routes/produto/$slug.tsx
|
|
144
|
+
export const Route = createFileRoute("/produto/$slug")({
|
|
145
|
+
loader: async ({ params }) => {
|
|
146
|
+
// params.slug is typed as string — guaranteed by the router
|
|
147
|
+
const product = await loadProduct({ data: params.slug });
|
|
148
|
+
if (!product) throw notFound();
|
|
149
|
+
return product;
|
|
150
|
+
},
|
|
151
|
+
component: ProductPage,
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Linking with Type Safety
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// TypeScript catches wrong params at compile time
|
|
159
|
+
<Link to="/produto/$slug" params={{ slug: product.slug }}>
|
|
160
|
+
{product.name}
|
|
161
|
+
</Link>
|
|
162
|
+
|
|
163
|
+
// ERROR: 'id' does not exist in type { slug: string }
|
|
164
|
+
<Link to="/produto/$slug" params={{ id: "123" }}>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### For Deco CMS Routes (Catch-All)
|
|
168
|
+
|
|
169
|
+
Deco sites use a catch-all route `/$` that resolves CMS pages. Links to CMS pages use plain paths:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
<Link to={`/${categorySlug}`} preload="intent">
|
|
173
|
+
{category.name}
|
|
174
|
+
</Link>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Pattern 3: `activeProps` for Menus
|
|
180
|
+
|
|
181
|
+
Automatically style the current page link.
|
|
182
|
+
|
|
183
|
+
### Basic Usage
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
<Link
|
|
187
|
+
to="/dashboard"
|
|
188
|
+
activeProps={{ className: "font-bold text-primary border-b-2 border-primary" }}
|
|
189
|
+
inactiveProps={{ className: "text-base-content/60" }}
|
|
190
|
+
>
|
|
191
|
+
Dashboard
|
|
192
|
+
</Link>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Navigation Menu (Real Example)
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
function NavItem({ href, label }: { href: string; label: string }) {
|
|
199
|
+
return (
|
|
200
|
+
<Link
|
|
201
|
+
to={href}
|
|
202
|
+
preload="intent"
|
|
203
|
+
activeProps={{ className: "text-primary font-bold" }}
|
|
204
|
+
activeOptions={{ exact: false }}
|
|
205
|
+
className="text-sm hover:text-primary transition-colors"
|
|
206
|
+
>
|
|
207
|
+
{label}
|
|
208
|
+
</Link>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function NavBar({ items }: { items: Array<{ href: string; label: string }> }) {
|
|
213
|
+
return (
|
|
214
|
+
<nav className="flex gap-4">
|
|
215
|
+
{items.map((item) => (
|
|
216
|
+
<NavItem key={item.href} {...item} />
|
|
217
|
+
))}
|
|
218
|
+
</nav>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `activeOptions`
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
activeOptions={{
|
|
227
|
+
exact: true, // Only active on exact path match (not children)
|
|
228
|
+
includeSearch: true, // Include search params in matching
|
|
229
|
+
}}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Pattern 4: Search State as URL Source of Truth
|
|
235
|
+
|
|
236
|
+
Instead of managing filter/sort/pagination state in React state or signals, use the **URL as the single source of truth**.
|
|
237
|
+
|
|
238
|
+
### The Problem with React State for Filters
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// BAD: State is lost on page refresh, not shareable, no back-button support
|
|
242
|
+
const [sort, setSort] = useState("price:asc");
|
|
243
|
+
const [filters, setFilters] = useState({});
|
|
244
|
+
const [page, setPage] = useState(1);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### The TanStack Way: URL = State
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Link that preserves existing search params and adds/changes one
|
|
251
|
+
<Link
|
|
252
|
+
to="."
|
|
253
|
+
search={(prev) => ({
|
|
254
|
+
...prev,
|
|
255
|
+
page: 2,
|
|
256
|
+
})}
|
|
257
|
+
>
|
|
258
|
+
Próxima página
|
|
259
|
+
</Link>
|
|
260
|
+
|
|
261
|
+
// Link that adds a filter
|
|
262
|
+
<Link
|
|
263
|
+
to="."
|
|
264
|
+
search={(prev) => ({
|
|
265
|
+
...prev,
|
|
266
|
+
"filter.brand": "espacosmart",
|
|
267
|
+
})}
|
|
268
|
+
preload="intent"
|
|
269
|
+
>
|
|
270
|
+
Espaço Smart
|
|
271
|
+
</Link>
|
|
272
|
+
|
|
273
|
+
// Link that changes sort while keeping filters
|
|
274
|
+
<Link
|
|
275
|
+
to="."
|
|
276
|
+
search={(prev) => ({
|
|
277
|
+
...prev,
|
|
278
|
+
sort: "price:asc",
|
|
279
|
+
})}
|
|
280
|
+
>
|
|
281
|
+
Menor Preço
|
|
282
|
+
</Link>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Benefits
|
|
286
|
+
|
|
287
|
+
1. **Shareable**: Copy URL → paste → same exact view
|
|
288
|
+
2. **Back button**: Browser history just works
|
|
289
|
+
3. **SEO**: Crawlers see the filter/sort URLs
|
|
290
|
+
4. **SSR**: Server renders the correct results on first load
|
|
291
|
+
5. **No state management needed**: No Zustand, no signals, no context
|
|
292
|
+
|
|
293
|
+
### Reading Search Params in Components
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
function SearchResult() {
|
|
297
|
+
const { sort, q, page } = Route.useSearch();
|
|
298
|
+
// sort, q, page are typed based on route validation
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Validating Search Params (Advanced)
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { z } from "zod";
|
|
306
|
+
|
|
307
|
+
const searchSchema = z.object({
|
|
308
|
+
q: z.string().optional(),
|
|
309
|
+
sort: z.enum(["price:asc", "price:desc", "name:asc", "relevance:desc"]).optional(),
|
|
310
|
+
page: z.number().int().positive().optional().default(1),
|
|
311
|
+
"filter.brand": z.string().optional(),
|
|
312
|
+
"filter.price": z.string().optional(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
export const Route = createFileRoute("/s")({
|
|
316
|
+
validateSearch: searchSchema,
|
|
317
|
+
loaderDeps: ({ search }) => search,
|
|
318
|
+
loader: async ({ deps }) => {
|
|
319
|
+
return loadSearchResults({ data: deps });
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Now search params are **type-safe** and **validated**.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Pattern 5: `window.location` Mutations to `useNavigate`
|
|
329
|
+
|
|
330
|
+
### Problem
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// FRESH — forces full page reload
|
|
334
|
+
window.location.search = params.toString();
|
|
335
|
+
window.location.href = newUrl;
|
|
336
|
+
globalThis.window.location.search = params.toString();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Solution
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
343
|
+
|
|
344
|
+
function Sort() {
|
|
345
|
+
const navigate = useNavigate();
|
|
346
|
+
|
|
347
|
+
const applySort = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
348
|
+
const params = new URLSearchParams(window.location.search);
|
|
349
|
+
params.set("sort", e.currentTarget.value);
|
|
350
|
+
navigate({ search: Object.fromEntries(params) });
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### With Debounce (Price Range Sliders)
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
const navigate = useNavigate();
|
|
359
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
|
360
|
+
|
|
361
|
+
const applyPrice = (min: number, max: number) => {
|
|
362
|
+
clearTimeout(debounceRef.current);
|
|
363
|
+
debounceRef.current = setTimeout(() => {
|
|
364
|
+
const params = new URLSearchParams(window.location.search);
|
|
365
|
+
params.set("filter.price", `${min}:${max}`);
|
|
366
|
+
navigate({ search: Object.fromEntries(params) });
|
|
367
|
+
}, 500);
|
|
368
|
+
};
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Discovery
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
rg 'window\.location\.(search|href)\s*=' src/ --glob '*.{tsx,ts}'
|
|
375
|
+
rg 'globalThis\.window\.location' src/ --glob '*.{tsx,ts}'
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Pattern 6: Form Submissions
|
|
381
|
+
|
|
382
|
+
### Search Forms (Navigate with Query Params)
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
386
|
+
|
|
387
|
+
function SearchForm({ action = "/s", name = "q" }) {
|
|
388
|
+
const navigate = useNavigate();
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<form
|
|
392
|
+
action={action}
|
|
393
|
+
onSubmit={(e) => {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
const q = new FormData(e.currentTarget).get(name)?.toString();
|
|
396
|
+
if (q) navigate({ to: action, search: { q } });
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
<input name={name} />
|
|
400
|
+
<button type="submit">Search</button>
|
|
401
|
+
</form>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Keep `action` as fallback for no-JS/crawlers.
|
|
407
|
+
|
|
408
|
+
### Action Forms (Server Mutations)
|
|
409
|
+
|
|
410
|
+
Forms that POST data (newsletter, contact, shipping calc) use `createServerFn`:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import { createDocument } from "~/lib/vtex-actions-server";
|
|
414
|
+
|
|
415
|
+
function Newsletter() {
|
|
416
|
+
const [loading, setLoading] = useState(false);
|
|
417
|
+
const [message, setMessage] = useState("");
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<form onSubmit={async (e) => {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
const email = new FormData(e.currentTarget).get("email")?.toString();
|
|
423
|
+
if (!email) return;
|
|
424
|
+
try {
|
|
425
|
+
setLoading(true);
|
|
426
|
+
await createDocument({ data: { entity: "NW", dataForm: { email } } });
|
|
427
|
+
setMessage("Cadastrado com sucesso!");
|
|
428
|
+
} catch (err: any) {
|
|
429
|
+
setMessage("Erro: " + err.message);
|
|
430
|
+
} finally {
|
|
431
|
+
setLoading(false);
|
|
432
|
+
setTimeout(() => setMessage(""), 3000);
|
|
433
|
+
}
|
|
434
|
+
}}>
|
|
435
|
+
<input name="email" type="email" required />
|
|
436
|
+
<button type="submit" disabled={loading}>
|
|
437
|
+
{loading ? "Enviando..." : "Inscrever"}
|
|
438
|
+
</button>
|
|
439
|
+
{message && <p>{message}</p>}
|
|
440
|
+
</form>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Pattern 7: Route `loaderDeps` for Reactive Search Params
|
|
448
|
+
|
|
449
|
+
### Problem
|
|
450
|
+
|
|
451
|
+
After converting to `useNavigate`, the URL changes but the page content doesn't update.
|
|
452
|
+
|
|
453
|
+
### Root Cause
|
|
454
|
+
|
|
455
|
+
TanStack Router only re-runs a loader when its **dependencies** change. By default: path params only, NOT search params.
|
|
456
|
+
|
|
457
|
+
### Solution
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
export const Route = createFileRoute("/$")({
|
|
461
|
+
loaderDeps: ({ search }) => ({ search }),
|
|
462
|
+
|
|
463
|
+
loader: async ({ params, deps }) => {
|
|
464
|
+
const basePath = "/" + (params._splat || "");
|
|
465
|
+
const searchStr = deps.search
|
|
466
|
+
? "?" + new URLSearchParams(deps.search as Record<string, string>).toString()
|
|
467
|
+
: "";
|
|
468
|
+
|
|
469
|
+
const page = await loadCmsPage({ data: basePath + searchStr });
|
|
470
|
+
if (!page) throw notFound();
|
|
471
|
+
return page;
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Pass Search Params to Section Loaders
|
|
477
|
+
|
|
478
|
+
The request passed to section loaders must include search params:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
const loadCmsPage = createServerFn({ method: "GET" }).handler(async (ctx) => {
|
|
482
|
+
const fullPath = ctx.data as string;
|
|
483
|
+
const [basePath] = fullPath.split("?");
|
|
484
|
+
const serverUrl = getRequestUrl();
|
|
485
|
+
const urlWithSearch = fullPath.includes("?")
|
|
486
|
+
? new URL(fullPath, serverUrl.origin).toString()
|
|
487
|
+
: serverUrl.toString();
|
|
488
|
+
|
|
489
|
+
const request = new Request(urlWithSearch, { headers: getRequest().headers });
|
|
490
|
+
const page = await resolveDecoPage(basePath, matcherCtx);
|
|
491
|
+
const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
|
|
492
|
+
return { ...page, resolvedSections: enrichedSections };
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Pattern 8: Programmatic Preloading
|
|
499
|
+
|
|
500
|
+
For advanced flows (barcode scanner, autocomplete selection, keyboard navigation):
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { useRouter } from "@tanstack/react-router";
|
|
504
|
+
|
|
505
|
+
function BarcodeScanner() {
|
|
506
|
+
const router = useRouter();
|
|
507
|
+
|
|
508
|
+
const onScan = async (code: string) => {
|
|
509
|
+
const slug = await resolveBarcode(code);
|
|
510
|
+
|
|
511
|
+
// Preload the product page while showing feedback
|
|
512
|
+
await router.preloadRoute({
|
|
513
|
+
to: "/produto/$slug",
|
|
514
|
+
params: { slug },
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Navigate — page is already loaded, opens instantly
|
|
518
|
+
router.navigate({
|
|
519
|
+
to: "/produto/$slug",
|
|
520
|
+
params: { slug },
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Preload on Autocomplete Hover
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
function SearchSuggestion({ product }) {
|
|
530
|
+
const router = useRouter();
|
|
531
|
+
const url = relative(product.url);
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<Link
|
|
535
|
+
to={url}
|
|
536
|
+
onMouseEnter={() => {
|
|
537
|
+
router.preloadRoute({ to: url });
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
{product.name}
|
|
541
|
+
</Link>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Pattern 9: `<select>` with `selected` to `defaultValue`
|
|
549
|
+
|
|
550
|
+
### Problem
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
// FRESH/Preact — works but React warns
|
|
554
|
+
<option value={value} selected={value === sort}>{label}</option>
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Solution
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
<select defaultValue={sort} onChange={applySort}>
|
|
561
|
+
{options.map(({ value, label }) => (
|
|
562
|
+
<option key={value} value={value}>{label}</option>
|
|
563
|
+
))}
|
|
564
|
+
</select>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## SSR + SEO Best Practices
|
|
570
|
+
|
|
571
|
+
### Every Page is SSR by Default
|
|
572
|
+
|
|
573
|
+
TanStack Start renders on the server first. No extra config needed. But optimize:
|
|
574
|
+
|
|
575
|
+
1. **Head metadata from loader data**:
|
|
576
|
+
```typescript
|
|
577
|
+
export const Route = createFileRoute("/$")({
|
|
578
|
+
head: ({ loaderData }) => ({
|
|
579
|
+
meta: [
|
|
580
|
+
{ title: loaderData?.seo?.title ?? "Espaço Smart" },
|
|
581
|
+
{ name: "description", content: loaderData?.seo?.description ?? "" },
|
|
582
|
+
],
|
|
583
|
+
links: loaderData?.seo?.canonical
|
|
584
|
+
? [{ rel: "canonical", href: loaderData.seo.canonical }]
|
|
585
|
+
: [],
|
|
586
|
+
}),
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
2. **Structured data in sections** (JSON-LD runs server-side, no hydration needed):
|
|
591
|
+
```typescript
|
|
592
|
+
function ProductSection({ product }) {
|
|
593
|
+
return (
|
|
594
|
+
<>
|
|
595
|
+
<script
|
|
596
|
+
type="application/ld+json"
|
|
597
|
+
dangerouslySetInnerHTML={{
|
|
598
|
+
__html: JSON.stringify({
|
|
599
|
+
"@context": "https://schema.org",
|
|
600
|
+
"@type": "Product",
|
|
601
|
+
name: product.name,
|
|
602
|
+
// ...
|
|
603
|
+
}),
|
|
604
|
+
}}
|
|
605
|
+
/>
|
|
606
|
+
<div>{/* product UI */}</div>
|
|
607
|
+
</>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
3. **Internal links as `<Link>`** — crawlers follow them AND users get SPA navigation:
|
|
613
|
+
```typescript
|
|
614
|
+
<Link to={relative(product.url)} preload="intent">
|
|
615
|
+
<img src={product.image} alt={product.name} />
|
|
616
|
+
<span>{product.name}</span>
|
|
617
|
+
</Link>
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Complete Migration Checklist
|
|
623
|
+
|
|
624
|
+
### Navigation Links
|
|
625
|
+
- [ ] Product card `<a href>` → `<Link to preload="intent">`
|
|
626
|
+
- [ ] Category/NavItem `<a href>` → `<Link to preload="intent">`
|
|
627
|
+
- [ ] Breadcrumb `<a href>` → `<Link to>`
|
|
628
|
+
- [ ] Filter options `<a href>` → `<Link to>` (same-page search param change)
|
|
629
|
+
- [ ] Search suggestions `<a href>` → `<Link to preload="intent">`
|
|
630
|
+
- [ ] Footer internal links → `<Link to>`
|
|
631
|
+
|
|
632
|
+
### Mutations
|
|
633
|
+
- [ ] Sort `window.location.search =` → `useNavigate`
|
|
634
|
+
- [ ] PriceRange `window.location.search =` → `useNavigate` with debounce
|
|
635
|
+
- [ ] SearchBar `<form action>` → `onSubmit` + `useNavigate`
|
|
636
|
+
- [ ] Newsletter form → `onSubmit` + `createServerFn`
|
|
637
|
+
|
|
638
|
+
### Route Configuration
|
|
639
|
+
- [ ] `$.tsx` has `loaderDeps: ({ search }) => ({ search })`
|
|
640
|
+
- [ ] `$.tsx` passes search params to section loaders via Request URL
|
|
641
|
+
- [ ] `<select>` uses `defaultValue` instead of `<option selected>`
|
|
642
|
+
|
|
643
|
+
### Verification
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
# Internal links that are still <a> (should be <Link>):
|
|
647
|
+
rg '<a\s+href="/' src/components/ src/sections/ --glob '*.tsx' -l
|
|
648
|
+
|
|
649
|
+
# window.location mutations (should be useNavigate):
|
|
650
|
+
rg 'window\.location\.(search|href)\s*=' src/ --glob '*.{tsx,ts}'
|
|
651
|
+
|
|
652
|
+
# Forms without onSubmit (should have handler):
|
|
653
|
+
rg '<form[^>]*action=' src/ --glob '*.tsx' | rg -v 'onSubmit'
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Quick Reference Card
|
|
659
|
+
|
|
660
|
+
| Fresh Pattern | TanStack Pattern | Benefit |
|
|
661
|
+
|--------------|-----------------|---------|
|
|
662
|
+
| `<a href={url}>` | `<Link to={url} preload="intent">` | Instant navigation |
|
|
663
|
+
| `window.location.search = x` | `navigate({ search })` | No reload, keeps state |
|
|
664
|
+
| `<form action="/s">` | `onSubmit + useNavigate` | SPA navigation |
|
|
665
|
+
| `<form action="/" method="POST">` | `onSubmit + createServerFn` | Server mutation |
|
|
666
|
+
| `<option selected>` | `<select defaultValue>` | React-compatible |
|
|
667
|
+
| CSS active class manually | `activeProps={{ className }}` | Automatic |
|
|
668
|
+
| No prefetch | `preload="intent"` | Data ready before click |
|
|
669
|
+
| `req.url` in loader | `loaderDeps + deps.search` | Reactive to URL changes |
|
|
670
|
+
| `router.push(url)` | `router.preloadRoute + navigate` | Preload then navigate |
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Related Skills
|
|
675
|
+
|
|
676
|
+
| Skill | Purpose |
|
|
677
|
+
|-------|---------|
|
|
678
|
+
| `deco-to-tanstack-migration` | Full migration playbook (imports, signals, framework) |
|
|
679
|
+
| `deco-islands-migration` | Eliminating the islands/ directory |
|
|
680
|
+
| `deco-tanstack-storefront-patterns` | Runtime patterns and fixes post-migration |
|
|
681
|
+
| `deco-storefront-test-checklist` | Context-aware QA checklist generation |
|