@decocms/start 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Liveness and readiness probe handlers.
3
+ *
4
+ * - `/_liveness` — simple 200 OK for load balancers (K8s, Cloudflare)
5
+ * - `/deco/_health` — detailed JSON health metrics (delegated to healthMetrics)
6
+ */
7
+
8
+ import { handleHealthCheck } from "./healthMetrics";
9
+
10
+ export function handleLiveness(request: Request): Response | null {
11
+ const url = new URL(request.url);
12
+
13
+ if (url.pathname === "/_liveness" || url.pathname === "/deco/_liveness") {
14
+ return new Response("OK", {
15
+ status: 200,
16
+ headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
17
+ });
18
+ }
19
+
20
+ return handleHealthCheck(request);
21
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Observability utilities for deco middleware.
3
+ *
4
+ * Pluggable adapters for tracing (spans) and metrics (counters, gauges,
5
+ * histograms). Works with any backend: OpenTelemetry, Sentry, Datadog, etc.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { configureTracer, configureMeter } from "@decocms/start/middleware";
10
+ * import { trace, metrics } from "@opentelemetry/api";
11
+ *
12
+ * configureTracer({
13
+ * startSpan: (name, attrs) => {
14
+ * const span = trace.getTracer("deco").startSpan(name, { attributes: attrs });
15
+ * return {
16
+ * end: () => span.end(),
17
+ * setError: (e) => span.recordException(e),
18
+ * setAttribute: (k, v) => span.setAttribute(k, v),
19
+ * };
20
+ * },
21
+ * });
22
+ *
23
+ * configureMeter({
24
+ * counterInc: (name, value, labels) => metrics.getMeter("deco").createCounter(name).add(value, labels),
25
+ * histogramRecord: (name, value, labels) => metrics.getMeter("deco").createHistogram(name).record(value, labels),
26
+ * });
27
+ * ```
28
+ */
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Tracer
32
+ // ---------------------------------------------------------------------------
33
+
34
+ import * as asyncHooks from "node:async_hooks";
35
+
36
+ export interface Span {
37
+ end(): void;
38
+ setError?(error: unknown): void;
39
+ setAttribute?(key: string, value: string | number | boolean): void;
40
+ }
41
+
42
+ export interface TracerAdapter {
43
+ startSpan(name: string, attributes?: Record<string, string | number | boolean>): Span;
44
+ }
45
+
46
+ let tracer: TracerAdapter | null = null;
47
+
48
+ // Per-request active span stored in AsyncLocalStorage so concurrent requests
49
+ // cannot overwrite each other's span when `withTracing` awaits async work.
50
+ // The namespace import + runtime guard mirrors loader.ts to stay safe in client builds.
51
+ const ALS = (asyncHooks as any).AsyncLocalStorage as
52
+ | (new <T>() => { getStore(): T | undefined; run<R>(store: T, fn: () => R): R })
53
+ | undefined;
54
+ const spanStorage: {
55
+ getStore(): Span | null | undefined;
56
+ run<R>(store: Span | null, fn: () => R): R;
57
+ } = ALS ? new ALS<Span | null>() : { getStore: () => undefined, run: (_s: any, fn: any) => fn() };
58
+
59
+ export function configureTracer(t: TracerAdapter) {
60
+ tracer = t;
61
+ }
62
+
63
+ export function getTracer(): TracerAdapter | null {
64
+ return tracer;
65
+ }
66
+
67
+ /** Get the currently active span for the current async context, if any. */
68
+ export function getActiveSpan(): Span | null {
69
+ return spanStorage.getStore() ?? null;
70
+ }
71
+
72
+ /** Set an attribute on the active span, if one exists. */
73
+ export function setSpanAttribute(key: string, value: string | number | boolean) {
74
+ getActiveSpan()?.setAttribute?.(key, value);
75
+ }
76
+
77
+ export async function withTracing<T>(
78
+ name: string,
79
+ fn: () => Promise<T>,
80
+ attributes?: Record<string, string | number | boolean>,
81
+ ): Promise<T> {
82
+ if (!tracer) return fn();
83
+
84
+ const span = tracer.startSpan(name, attributes);
85
+
86
+ try {
87
+ const result = await spanStorage.run(span, fn);
88
+ span.end();
89
+ return result;
90
+ } catch (error) {
91
+ span.setError?.(error);
92
+ span.end();
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Meter
99
+ // ---------------------------------------------------------------------------
100
+
101
+ type Labels = Record<string, string | number | boolean>;
102
+
103
+ export interface MeterAdapter {
104
+ counterInc(name: string, value?: number, labels?: Labels): void;
105
+ gaugeSet?(name: string, value: number, labels?: Labels): void;
106
+ histogramRecord?(name: string, value: number, labels?: Labels): void;
107
+ }
108
+
109
+ let meter: MeterAdapter | null = null;
110
+
111
+ export function configureMeter(m: MeterAdapter) {
112
+ meter = m;
113
+ }
114
+
115
+ export function getMeter(): MeterAdapter | null {
116
+ return meter;
117
+ }
118
+
119
+ /** Pre-defined metric names for consistency. */
120
+ export const MetricNames = {
121
+ HTTP_REQUESTS_TOTAL: "http_requests_total",
122
+ HTTP_REQUEST_DURATION_MS: "http_request_duration_ms",
123
+ HTTP_REQUEST_ERRORS: "http_request_errors_total",
124
+ CACHE_HIT: "cache_hit_total",
125
+ CACHE_MISS: "cache_miss_total",
126
+ RESOLVE_DURATION_MS: "resolve_duration_ms",
127
+ FETCH_DURATION_MS: "fetch_duration_ms",
128
+ } as const;
129
+
130
+ /**
131
+ * Record an HTTP request metric.
132
+ * Call in middleware after the response is produced.
133
+ */
134
+ export function recordRequestMetric(
135
+ method: string,
136
+ path: string,
137
+ status: number,
138
+ durationMs: number,
139
+ ) {
140
+ if (!meter) return;
141
+ const labels: Labels = { method, path: normalizePath(path), status };
142
+ meter.counterInc(MetricNames.HTTP_REQUESTS_TOTAL, 1, labels);
143
+ meter.histogramRecord?.(MetricNames.HTTP_REQUEST_DURATION_MS, durationMs, labels);
144
+ if (status >= 500) {
145
+ meter.counterInc(MetricNames.HTTP_REQUEST_ERRORS, 1, labels);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Record a cache hit/miss metric.
151
+ */
152
+ export function recordCacheMetric(hit: boolean, profile?: string) {
153
+ if (!meter) return;
154
+ const labels: Labels = profile ? { profile } : {};
155
+ meter.counterInc(hit ? MetricNames.CACHE_HIT : MetricNames.CACHE_MISS, 1, labels);
156
+ }
157
+
158
+ function normalizePath(path: string): string {
159
+ // Collapse dynamic segments to reduce cardinality
160
+ return path
161
+ .replace(/\/[0-9a-f]{8,}/gi, "/:id")
162
+ .replace(/\/\d+/g, "/:id")
163
+ .replace(/\/[^/]+\/p$/, "/:slug/p");
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Request logging
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const isDev =
171
+ typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV === "development";
172
+
173
+ /**
174
+ * Structured request log entry.
175
+ * JSON in production, colorized in development.
176
+ * Includes traceId when available.
177
+ */
178
+ export function logRequest(
179
+ request: Request,
180
+ status: number,
181
+ durationMs: number,
182
+ extra?: Record<string, unknown>,
183
+ ) {
184
+ const url = new URL(request.url);
185
+
186
+ if (isDev) {
187
+ const color = status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m";
188
+ const extraStr = extra ? ` ${JSON.stringify(extra)}` : "";
189
+ console.log(
190
+ `${color}${request.method}\x1b[0m ${url.pathname} ${status} ${durationMs.toFixed(0)}ms${extraStr}`,
191
+ );
192
+ } else {
193
+ console.log(
194
+ JSON.stringify({
195
+ level: status >= 500 ? "error" : "info",
196
+ method: request.method,
197
+ path: url.pathname,
198
+ status,
199
+ durationMs: Math.round(durationMs),
200
+ timestamp: new Date().toISOString(),
201
+ ...extra,
202
+ }),
203
+ );
204
+ }
205
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Admin Route Helpers
3
+ *
4
+ * Pre-built server handler configs for the Deco admin protocol routes.
5
+ * Sites spread these into their `createFileRoute` definitions to avoid
6
+ * repeating the same CORS + handler boilerplate.
7
+ *
8
+ * @example Site's `src/routes/deco/meta.ts`:
9
+ * ```ts
10
+ * import { createFileRoute } from "@tanstack/react-router";
11
+ * import { decoMetaRoute } from "@decocms/start/routes";
12
+ *
13
+ * export const Route = createFileRoute("/deco/meta")(decoMetaRoute);
14
+ * ```
15
+ */
16
+ import { corsHeaders } from "../admin/cors";
17
+ import { handleInvoke } from "../admin/invoke";
18
+ import { handleMeta } from "../admin/meta";
19
+ import { handleRender } from "../admin/render";
20
+
21
+ type HandlerFn = (ctx: { request: Request }) => Promise<Response> | Response;
22
+
23
+ function withCors(handler: HandlerFn): HandlerFn {
24
+ return async (ctx) => {
25
+ const response = await handler(ctx);
26
+ const headers = new Headers(response.headers);
27
+ for (const [k, v] of Object.entries(corsHeaders(ctx.request))) {
28
+ headers.set(k, v);
29
+ }
30
+ return new Response(response.body, {
31
+ status: response.status,
32
+ headers,
33
+ });
34
+ };
35
+ }
36
+
37
+ function optionsHandler(ctx: { request: Request }): Response {
38
+ return new Response(null, {
39
+ status: 204,
40
+ headers: corsHeaders(ctx.request),
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Route config for `/deco/meta` — serves JSON Schema + manifest.
46
+ * Spread into `createFileRoute("/deco/meta")({...})`.
47
+ */
48
+ export const decoMetaRoute = {
49
+ server: {
50
+ handlers: {
51
+ GET: withCors(({ request }) => handleMeta(request)),
52
+ OPTIONS: optionsHandler,
53
+ },
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Route config for `/deco/render` — section/page preview in iframe.
59
+ * Spread into `createFileRoute("/deco/render")({...})`.
60
+ */
61
+ export const decoRenderRoute = {
62
+ server: {
63
+ handlers: {
64
+ GET: withCors(async ({ request }) => handleRender(request)),
65
+ POST: withCors(async ({ request }) => handleRender(request)),
66
+ OPTIONS: optionsHandler,
67
+ },
68
+ },
69
+ };
70
+
71
+ /**
72
+ * Route config for `/deco/invoke/$` — loader/action execution.
73
+ * Spread into `createFileRoute("/deco/invoke/$")({...})`.
74
+ */
75
+ export const decoInvokeRoute = {
76
+ server: {
77
+ handlers: {
78
+ GET: withCors(async ({ request }) => handleInvoke(request)),
79
+ POST: withCors(async ({ request }) => handleInvoke(request)),
80
+ OPTIONS: optionsHandler,
81
+ },
82
+ },
83
+ };
@@ -0,0 +1,302 @@
1
+ /**
2
+ * CMS Route Helpers
3
+ *
4
+ * Reusable building blocks for CMS catch-all and homepage routes.
5
+ * Since TanStack Router requires routes to be file-based in the site repo,
6
+ * we export helper functions and config that sites use in their route files.
7
+ *
8
+ * @example Site's `src/routes/$.tsx`:
9
+ * ```ts
10
+ * import { createFileRoute, notFound } from "@tanstack/react-router";
11
+ * import { loadCmsPage, cmsRouteConfig } from "@decocms/start/routes";
12
+ * import { DecoPageRenderer } from "@decocms/start/hooks";
13
+ *
14
+ * export const Route = createFileRoute("/$")({
15
+ * ...cmsRouteConfig({
16
+ * siteName: "My Store",
17
+ * defaultTitle: "My Store - Best products",
18
+ * ignoreSearchParams: ["skuId"],
19
+ * }),
20
+ * });
21
+ * ```
22
+ */
23
+
24
+ import { createServerFn } from "@tanstack/react-start";
25
+ import {
26
+ getCookies,
27
+ getRequest,
28
+ getRequestHeader,
29
+ getRequestUrl,
30
+ } from "@tanstack/react-start/server";
31
+ import { createElement } from "react";
32
+ import { preloadSectionComponents } from "../cms/registry";
33
+ import type { DeferredSection, MatcherContext, ResolvedSection } from "../cms/resolve";
34
+ import { resolveDecoPage, resolveDeferredSection } from "../cms/resolve";
35
+ import { runSectionLoaders, runSingleSectionLoader } from "../cms/sectionLoaders";
36
+ import {
37
+ type CacheProfile,
38
+ cacheHeaders,
39
+ detectCacheProfile,
40
+ routeCacheDefaults,
41
+ } from "../sdk/cacheHeaders";
42
+
43
+ const isServer = typeof document === "undefined";
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Server function — loads a CMS page, runs section loaders, detects cache
47
+ // ---------------------------------------------------------------------------
48
+
49
+ type PageResult = Awaited<ReturnType<typeof loadCmsPageInternal>>;
50
+ const pageInflight = new Map<string, Promise<PageResult>>();
51
+
52
+ async function loadCmsPageInternal(fullPath: string) {
53
+ const [basePath] = fullPath.split("?");
54
+ const serverUrl = getRequestUrl();
55
+ // Prefer the real server URL when available — it preserves duplicate query
56
+ // params (e.g. filter.category-1=a&filter.category-1=b) that the TanStack
57
+ // Router search object (plain Record<string,string>) would collapse.
58
+ const realUrlPath = serverUrl.pathname + serverUrl.search;
59
+ const urlWithSearch =
60
+ realUrlPath.startsWith(basePath) && serverUrl.search
61
+ ? serverUrl.toString()
62
+ : fullPath.includes("?")
63
+ ? new URL(fullPath, serverUrl.origin).toString()
64
+ : serverUrl.toString();
65
+
66
+ const matcherCtx: MatcherContext = {
67
+ userAgent: getRequestHeader("user-agent") ?? "",
68
+ url: urlWithSearch,
69
+ path: basePath,
70
+ cookies: getCookies(),
71
+ };
72
+ const page = await resolveDecoPage(basePath, matcherCtx);
73
+ if (!page) return null;
74
+
75
+ const request = new Request(urlWithSearch, {
76
+ headers: getRequest().headers,
77
+ });
78
+ const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
79
+
80
+ // Pre-import eager section modules so their default exports are cached
81
+ // in resolvedComponents. This ensures SSR renders with direct component
82
+ // refs, and the client hydration can skip React.lazy/Suspense.
83
+ const eagerKeys = enrichedSections.map((s) => s.component);
84
+ await preloadSectionComponents(eagerKeys);
85
+
86
+ const cacheProfile = detectCacheProfile(basePath);
87
+ return {
88
+ ...page,
89
+ resolvedSections: enrichedSections,
90
+ deferredSections: page.deferredSections,
91
+ cacheProfile,
92
+ };
93
+ }
94
+
95
+ export const loadCmsPage = createServerFn({ method: "GET" })
96
+ .inputValidator((data: unknown) => data as string)
97
+ .handler(async (ctx) => {
98
+ const fullPath = ctx.data;
99
+ const [basePath] = fullPath.split("?");
100
+
101
+ const existing = pageInflight.get(basePath);
102
+ if (existing) return existing;
103
+
104
+ const promise = loadCmsPageInternal(fullPath).finally(() => pageInflight.delete(basePath));
105
+ pageInflight.set(basePath, promise);
106
+ return promise;
107
+ });
108
+
109
+ /**
110
+ * Same as loadCmsPage but hardcoded to "/" path.
111
+ * Avoids passing data through the server function for the homepage.
112
+ */
113
+ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async () => {
114
+ const matcherCtx: MatcherContext = {
115
+ userAgent: getRequestHeader("user-agent") ?? "",
116
+ url: getRequestUrl().toString(),
117
+ path: "/",
118
+ cookies: getCookies(),
119
+ };
120
+ const page = await resolveDecoPage("/", matcherCtx);
121
+ if (!page) return null;
122
+
123
+ const request = getRequest();
124
+ const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
125
+
126
+ const eagerKeys = enrichedSections.map((s) => s.component);
127
+ await preloadSectionComponents(eagerKeys);
128
+
129
+ return {
130
+ ...page,
131
+ resolvedSections: enrichedSections,
132
+ deferredSections: page.deferredSections,
133
+ };
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Deferred section loader — resolves + enriches a single section on demand
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export const loadDeferredSection = createServerFn({ method: "POST" })
141
+ .inputValidator(
142
+ (data: unknown) =>
143
+ data as { component: string; rawProps: Record<string, any>; pagePath: string },
144
+ )
145
+ .handler(async (ctx) => {
146
+ const { component, rawProps, pagePath } = ctx.data;
147
+
148
+ const matcherCtx: MatcherContext = {
149
+ userAgent: getRequestHeader("user-agent") ?? "",
150
+ url: getRequestUrl().toString(),
151
+ path: pagePath,
152
+ cookies: getCookies(),
153
+ };
154
+
155
+ const section = await resolveDeferredSection(component, rawProps, pagePath, matcherCtx);
156
+ if (!section) return null;
157
+
158
+ const request = new Request(getRequestUrl().toString(), {
159
+ headers: getRequest().headers,
160
+ });
161
+ const enriched = await runSingleSectionLoader(section, request);
162
+ return enriched;
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Default pending component — shown during SPA navigation while loader runs
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export function CmsPagePendingFallback() {
170
+ return createElement(
171
+ "div",
172
+ { className: "w-full min-h-[60vh] flex flex-col gap-6 py-8" },
173
+ createElement("div", {
174
+ className: "skeleton animate-pulse w-full rounded",
175
+ style: { aspectRatio: "1440/400", minHeight: 200 },
176
+ }),
177
+ createElement(
178
+ "div",
179
+ { className: "px-4 lg:px-8 flex flex-col gap-4" },
180
+ createElement("div", { className: "skeleton animate-pulse w-48 h-8 rounded" }),
181
+ createElement(
182
+ "div",
183
+ { className: "grid grid-cols-2 lg:grid-cols-4 gap-4" },
184
+ ...Array.from({ length: 4 }, (_, i) =>
185
+ createElement("div", {
186
+ key: i,
187
+ className: "skeleton animate-pulse w-full h-48 lg:h-64 rounded",
188
+ }),
189
+ ),
190
+ ),
191
+ ),
192
+ );
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Route configuration factory
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export interface CmsRouteOptions {
200
+ /** Site name used in page titles (e.g. "Espaço Smart"). */
201
+ siteName: string;
202
+ /** Default page title when CMS page has no name. */
203
+ defaultTitle: string;
204
+ /**
205
+ * Search params to exclude from loader deps.
206
+ * These params won't trigger a server re-fetch when they change.
207
+ * Defaults to `["skuId"]` — variant selection is client-side only.
208
+ */
209
+ ignoreSearchParams?: string[];
210
+ /** Custom pending component shown during SPA navigation. */
211
+ pendingComponent?: () => any;
212
+ }
213
+
214
+ /**
215
+ * Returns a TanStack Router route config object for a CMS catch-all route.
216
+ * Spread the result into your `createFileRoute("/$")({...})` call.
217
+ *
218
+ * Includes: loaderDeps, loader, headers, head, staleTime/gcTime.
219
+ * Does NOT include: component, notFoundComponent (site provides these).
220
+ */
221
+ export function cmsRouteConfig(options: CmsRouteOptions) {
222
+ const { siteName, defaultTitle, ignoreSearchParams = ["skuId"], pendingComponent } = options;
223
+
224
+ const ignoreSet = new Set(ignoreSearchParams);
225
+
226
+ return {
227
+ loaderDeps: ({ search }: { search: Record<string, string> }) => {
228
+ const filtered = Object.fromEntries(
229
+ Object.entries(search ?? {}).filter(([k]) => !ignoreSet.has(k)),
230
+ );
231
+ return {
232
+ search: Object.keys(filtered).length ? filtered : undefined,
233
+ };
234
+ },
235
+
236
+ loader: async ({
237
+ params,
238
+ deps,
239
+ }: {
240
+ params: { _splat?: string };
241
+ deps: { search?: Record<string, string> };
242
+ }) => {
243
+ const basePath = "/" + (params._splat || "");
244
+ const searchStr = deps.search
245
+ ? "?" + new URLSearchParams(deps.search as Record<string, string>).toString()
246
+ : "";
247
+ const page = await loadCmsPage({ data: basePath + searchStr });
248
+
249
+ // On the client (SPA navigation or initial hydration), pre-import
250
+ // eager section modules BEFORE React renders. This ensures
251
+ // getResolvedComponent() returns a value and we skip React.lazy.
252
+ if (!isServer && page?.resolvedSections) {
253
+ const keys = page.resolvedSections.map((s: ResolvedSection) => s.component);
254
+ await preloadSectionComponents(keys);
255
+ }
256
+ return page;
257
+ },
258
+
259
+ ...(pendingComponent ? { pendingComponent } : {}),
260
+
261
+ ...routeCacheDefaults("product"),
262
+
263
+ headers: ({ loaderData }: { loaderData?: { cacheProfile?: CacheProfile } }) => {
264
+ const profile = loaderData?.cacheProfile ?? "listing";
265
+ return cacheHeaders(profile);
266
+ },
267
+
268
+ head: ({ loaderData }: { loaderData?: { name?: string } }) => ({
269
+ meta: [
270
+ {
271
+ title: loaderData?.name ? `${loaderData.name} | ${siteName}` : defaultTitle,
272
+ },
273
+ ],
274
+ }),
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Returns a TanStack Router route config for the CMS homepage route.
280
+ * Spread into `createFileRoute("/")({...})`.
281
+ */
282
+ export function cmsHomeRouteConfig(options: {
283
+ defaultTitle: string;
284
+ pendingComponent?: () => any;
285
+ }) {
286
+ return {
287
+ loader: async () => {
288
+ const page = await loadCmsHomePage();
289
+ if (!isServer && page?.resolvedSections) {
290
+ const keys = page.resolvedSections.map((s: ResolvedSection) => s.component);
291
+ await preloadSectionComponents(keys);
292
+ }
293
+ return page;
294
+ },
295
+ ...(options.pendingComponent ? { pendingComponent: options.pendingComponent } : {}),
296
+ ...routeCacheDefaults("static"),
297
+ headers: () => cacheHeaders("static"),
298
+ head: () => ({
299
+ meta: [{ title: options.defaultTitle }],
300
+ }),
301
+ };
302
+ }
@@ -0,0 +1,34 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import type { ResolvedSection } from "../cms/resolve";
3
+ import { DecoPageRenderer } from "../hooks/DecoPageRenderer";
4
+
5
+ /**
6
+ * Default CMS page component. Renders all resolved sections.
7
+ * Sites can use this directly or build their own.
8
+ */
9
+ export function CmsPage({ sections }: { sections: ResolvedSection[] }) {
10
+ return (
11
+ <div>
12
+ <DecoPageRenderer sections={sections} />
13
+ </div>
14
+ );
15
+ }
16
+
17
+ /**
18
+ * Default 404 page for CMS routes.
19
+ * Sites can override with their own branded version.
20
+ */
21
+ export function NotFoundPage() {
22
+ return (
23
+ <div className="min-h-screen flex items-center justify-center">
24
+ <div className="text-center">
25
+ <h1 className="text-6xl font-bold text-base-content/20 mb-4">404</h1>
26
+ <h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
27
+ <p className="text-base-content/60 mb-6">No CMS page block matches this URL.</p>
28
+ <Link to="/" className="btn btn-primary">
29
+ Go Home
30
+ </Link>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ decoInvokeRoute,
3
+ decoMetaRoute,
4
+ decoRenderRoute,
5
+ } from "./adminRoutes";
6
+ export {
7
+ CmsPagePendingFallback,
8
+ type CmsRouteOptions,
9
+ cmsHomeRouteConfig,
10
+ cmsRouteConfig,
11
+ loadCmsHomePage,
12
+ loadCmsPage,
13
+ loadDeferredSection,
14
+ } from "./cmsRoute";
15
+ export { CmsPage, NotFoundPage } from "./components";