@decocms/start 0.21.1 → 0.22.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.21.1",
3
+ "version": "0.22.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -31,6 +31,7 @@
31
31
  "./sdk/wrapCaughtErrors": "./src/sdk/wrapCaughtErrors.ts",
32
32
  "./sdk/csp": "./src/sdk/csp.ts",
33
33
  "./sdk/urlUtils": "./src/sdk/urlUtils.ts",
34
+ "./sdk/normalizeUrls": "./src/sdk/normalizeUrls.ts",
34
35
  "./sdk/mergeCacheControl": "./src/sdk/mergeCacheControl.ts",
35
36
  "./sdk/requestContext": "./src/sdk/requestContext.ts",
36
37
  "./sdk/createInvoke": "./src/sdk/createInvoke.ts",
@@ -209,18 +209,21 @@ export function SectionList({ sections }: { sections: Section[] | null | undefin
209
209
  interface DeferredSectionWrapperProps {
210
210
  deferred: DeferredSection;
211
211
  pagePath: string;
212
+ pageUrl?: string;
212
213
  loadingFallback?: ReactNode;
213
214
  errorFallback?: ReactNode;
214
215
  loadFn: (data: {
215
216
  component: string;
216
217
  rawProps: Record<string, unknown>;
217
218
  pagePath: string;
219
+ pageUrl?: string;
218
220
  }) => Promise<ResolvedSection | null>;
219
221
  }
220
222
 
221
223
  function DeferredSectionWrapper({
222
224
  deferred,
223
225
  pagePath,
226
+ pageUrl,
224
227
  loadingFallback,
225
228
  errorFallback,
226
229
  loadFn,
@@ -262,7 +265,7 @@ function DeferredSectionWrapper({
262
265
  const skeleton = !optionsReady
263
266
  ? null
264
267
  : hasCustomFallback
265
- ? createElement(loadedOptions!.loadingFallback!)
268
+ ? createElement(loadedOptions!.loadingFallback!, deferred.rawProps)
266
269
  : (loadingFallback ??
267
270
  (isDev ? (
268
271
  <DevMissingFallbackWarning component={deferred.component} />
@@ -283,6 +286,7 @@ function DeferredSectionWrapper({
283
286
  component: deferred.component,
284
287
  rawProps: deferred.rawProps,
285
288
  pagePath,
289
+ pageUrl,
286
290
  })
287
291
  .then((result) => {
288
292
  if (result) deferredSectionCache.set(key0, { section: result, ts: Date.now() });
@@ -302,6 +306,7 @@ function DeferredSectionWrapper({
302
306
  component: deferred.component,
303
307
  rawProps: deferred.rawProps,
304
308
  pagePath,
309
+ pageUrl,
305
310
  })
306
311
  .then((result) => {
307
312
  if (result) deferredSectionCache.set(key1, { section: result, ts: Date.now() });
@@ -315,7 +320,7 @@ function DeferredSectionWrapper({
315
320
 
316
321
  observer.observe(el);
317
322
  return () => observer.disconnect();
318
- }, [deferred.component, deferred.rawProps, pagePath, section, loadFn]);
323
+ }, [deferred.component, deferred.rawProps, pagePath, pageUrl, section, loadFn]);
319
324
 
320
325
  if (error) {
321
326
  const errFallback = loadedOptions?.errorFallback
@@ -403,12 +408,15 @@ interface Props {
403
408
  sections: ResolvedSection[];
404
409
  deferredSections?: DeferredSection[];
405
410
  pagePath?: string;
411
+ /** Original page URL (with query params) — forwarded to deferred section loaders. */
412
+ pageUrl?: string;
406
413
  loadingFallback?: ReactNode;
407
414
  errorFallback?: ReactNode;
408
415
  loadDeferredSectionFn?: (data: {
409
416
  component: string;
410
417
  rawProps: Record<string, unknown>;
411
418
  pagePath: string;
419
+ pageUrl?: string;
412
420
  }) => Promise<ResolvedSection | null>;
413
421
  }
414
422
 
@@ -416,6 +424,7 @@ export function DecoPageRenderer({
416
424
  sections,
417
425
  deferredSections,
418
426
  pagePath = "/",
427
+ pageUrl,
419
428
  loadingFallback,
420
429
  errorFallback,
421
430
  loadDeferredSectionFn,
@@ -436,6 +445,7 @@ export function DecoPageRenderer({
436
445
  key={`deferred-${pagePath}-${item.deferred.key}-${item.deferred.index}`}
437
446
  deferred={item.deferred}
438
447
  pagePath={pagePath}
448
+ pageUrl={pageUrl}
439
449
  loadingFallback={loadingFallback}
440
450
  errorFallback={errorFallback}
441
451
  loadFn={loadDeferredSectionFn}
@@ -39,6 +39,7 @@ import {
39
39
  detectCacheProfile,
40
40
  routeCacheDefaults,
41
41
  } from "../sdk/cacheHeaders";
42
+ import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
42
43
 
43
44
  const isServer = typeof document === "undefined";
44
45
 
@@ -86,9 +87,10 @@ async function loadCmsPageInternal(fullPath: string) {
86
87
  const cacheProfile = detectCacheProfile(basePath);
87
88
  return {
88
89
  ...page,
89
- resolvedSections: enrichedSections,
90
- deferredSections: page.deferredSections,
90
+ resolvedSections: normalizeUrlsInObject(enrichedSections),
91
+ deferredSections: normalizeUrlsInObject(page.deferredSections),
91
92
  cacheProfile,
93
+ pageUrl: urlWithSearch,
92
94
  };
93
95
  }
94
96
 
@@ -128,8 +130,8 @@ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async (
128
130
 
129
131
  return {
130
132
  ...page,
131
- resolvedSections: enrichedSections,
132
- deferredSections: page.deferredSections,
133
+ resolvedSections: normalizeUrlsInObject(enrichedSections),
134
+ deferredSections: normalizeUrlsInObject(page.deferredSections),
133
135
  };
134
136
  });
135
137
 
@@ -140,14 +142,15 @@ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async (
140
142
  export const loadDeferredSection = createServerFn({ method: "POST" })
141
143
  .inputValidator(
142
144
  (data: unknown) =>
143
- data as { component: string; rawProps: Record<string, any>; pagePath: string },
145
+ data as { component: string; rawProps: Record<string, any>; pagePath: string; pageUrl?: string },
144
146
  )
145
147
  .handler(async (ctx) => {
146
- const { component, rawProps, pagePath } = ctx.data;
148
+ const { component, rawProps, pagePath, pageUrl } = ctx.data;
147
149
 
150
+ const serverUrl = getRequestUrl().toString();
148
151
  const matcherCtx: MatcherContext = {
149
152
  userAgent: getRequestHeader("user-agent") ?? "",
150
- url: getRequestUrl().toString(),
153
+ url: pageUrl || serverUrl,
151
154
  path: pagePath,
152
155
  cookies: getCookies(),
153
156
  };
@@ -155,11 +158,11 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
155
158
  const section = await resolveDeferredSection(component, rawProps, pagePath, matcherCtx);
156
159
  if (!section) return null;
157
160
 
158
- const request = new Request(getRequestUrl().toString(), {
161
+ const request = new Request(pageUrl || serverUrl, {
159
162
  headers: getRequest().headers,
160
163
  });
161
164
  const enriched = await runSingleSectionLoader(section, request);
162
- return enriched;
165
+ return normalizeUrlsInObject(enriched);
163
166
  });
164
167
 
165
168
  // ---------------------------------------------------------------------------
package/src/sdk/index.ts CHANGED
@@ -27,6 +27,11 @@ export {
27
27
  } from "./instrumentedFetch";
28
28
  export { batchInvoke, createInvokeProxy, type InvokeProxy, invokeQueryOptions } from "./invoke";
29
29
  export { createCacheControlCollector, mergeCacheControl } from "./mergeCacheControl";
30
+ export {
31
+ getProductionOrigins,
32
+ normalizeUrlsInObject,
33
+ registerProductionOrigins,
34
+ } from "./normalizeUrls";
30
35
  export {
31
36
  addRedirects,
32
37
  loadRedirects,
@@ -0,0 +1,91 @@
1
+ /**
2
+ * URL normalization for CMS-resolved props.
3
+ *
4
+ * Strips registered production origins from absolute URLs, converting them to
5
+ * relative paths. This allows staging/preview deployments to work without
6
+ * every CMS-authored link sending users to the production domain.
7
+ *
8
+ * Only affects strings that START with a registered origin + "/" — image CDN
9
+ * URLs, API endpoints on different domains, and non-URL strings are untouched.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // In site's setup.ts:
14
+ * import { registerProductionOrigins } from "@decocms/start/sdk/normalizeUrls";
15
+ * registerProductionOrigins([
16
+ * "https://www.casaevideo.com.br",
17
+ * "https://casaevideo.com.br",
18
+ * ]);
19
+ * ```
20
+ */
21
+
22
+ let origins: string[] = [];
23
+
24
+ /**
25
+ * Register production origins that should be stripped from CMS-resolved URLs.
26
+ * Call once in your site's setup.ts before any page loads.
27
+ */
28
+ export function registerProductionOrigins(productionOrigins: string[]) {
29
+ origins = productionOrigins.map((o) => o.replace(/\/+$/, ""));
30
+ }
31
+
32
+ export function getProductionOrigins(): readonly string[] {
33
+ return origins;
34
+ }
35
+
36
+ function normalizeString(str: string): string {
37
+ for (const origin of origins) {
38
+ if (str.startsWith(origin + "/")) {
39
+ return str.slice(origin.length);
40
+ }
41
+ if (str === origin) {
42
+ return "/";
43
+ }
44
+ }
45
+ return str;
46
+ }
47
+
48
+ /**
49
+ * Deep-walk an object tree and rewrite any string value that starts with a
50
+ * registered production origin to a relative path. Returns the same reference
51
+ * if nothing was changed (structural sharing).
52
+ */
53
+ export function normalizeUrlsInObject<T>(obj: T): T {
54
+ if (!origins.length) return obj;
55
+ return deepNormalize(obj) as T;
56
+ }
57
+
58
+ function deepNormalize(val: unknown): unknown {
59
+ if (val == null) return val;
60
+
61
+ if (typeof val === "string") {
62
+ return normalizeString(val);
63
+ }
64
+
65
+ if (Array.isArray(val)) {
66
+ let changed = false;
67
+ const result = val.map((item) => {
68
+ const normalized = deepNormalize(item);
69
+ if (normalized !== item) changed = true;
70
+ return normalized;
71
+ });
72
+ return changed ? result : val;
73
+ }
74
+
75
+ if (typeof val === "object") {
76
+ // Skip React elements, Dates, RegExps, and other non-plain objects
77
+ const proto = Object.getPrototypeOf(val);
78
+ if (proto !== Object.prototype && proto !== null) return val;
79
+
80
+ let changed = false;
81
+ const result: Record<string, unknown> = {};
82
+ for (const [key, value] of Object.entries(val as Record<string, unknown>)) {
83
+ const normalized = deepNormalize(value);
84
+ result[key] = normalized;
85
+ if (normalized !== value) changed = true;
86
+ }
87
+ return changed ? result : val;
88
+ }
89
+
90
+ return val;
91
+ }