@decocms/start 0.23.0 → 0.23.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 +1 -1
- package/src/admin/cors.ts +23 -3
- package/src/admin/index.ts +1 -1
- package/src/admin/meta.ts +4 -8
- package/src/admin/render.ts +4 -25
- package/src/admin/setup.ts +3 -3
- package/src/cms/index.ts +2 -0
- package/src/cms/loader.ts +2 -6
- package/src/cms/registry.ts +4 -3
- package/src/cms/resolve.ts +94 -61
- package/src/cms/sectionLoaders.ts +2 -9
- package/src/hooks/DecoPageRenderer.tsx +2 -9
- package/src/matchers/countryNames.ts +15 -0
- package/src/sdk/cacheHeaders.ts +26 -0
- package/src/sdk/djb2.ts +20 -0
- package/src/sdk/htmlShell.ts +55 -0
- package/src/sdk/index.ts +10 -0
- package/src/sdk/redirects.ts +8 -0
- package/src/sdk/requestContext.ts +3 -2
- package/src/sdk/sitemap.ts +21 -4
- package/src/sdk/urlUtils.ts +17 -0
- package/src/sdk/useDevice.ts +11 -2
- package/src/sdk/workerEntry.ts +4 -31
package/package.json
CHANGED
package/src/admin/cors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const ADMIN_ORIGINS = [
|
|
1
|
+
const ADMIN_ORIGINS = new Set([
|
|
2
2
|
"https://admin.deco.cx",
|
|
3
3
|
"https://v0-admin.deco.cx",
|
|
4
4
|
"https://play.deco.cx",
|
|
@@ -6,7 +6,24 @@ const ADMIN_ORIGINS = [
|
|
|
6
6
|
"https://deco.chat",
|
|
7
7
|
"https://admin.decocms.com",
|
|
8
8
|
"https://decocms.com",
|
|
9
|
-
];
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register additional allowed admin origins.
|
|
13
|
+
* Useful for self-hosted admin UIs or custom dashboards.
|
|
14
|
+
*/
|
|
15
|
+
export function registerAdminOrigin(origin: string): void {
|
|
16
|
+
ADMIN_ORIGINS.add(origin);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register multiple additional allowed admin origins.
|
|
21
|
+
*/
|
|
22
|
+
export function registerAdminOrigins(origins: string[]): void {
|
|
23
|
+
for (const origin of origins) {
|
|
24
|
+
ADMIN_ORIGINS.add(origin);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
export function isAdminOrLocalhost(request: Request): boolean {
|
|
12
29
|
const origin = request.headers.get("origin") || request.headers.get("referer") || "";
|
|
@@ -15,7 +32,10 @@ export function isAdminOrLocalhost(request: Request): boolean {
|
|
|
15
32
|
return true;
|
|
16
33
|
}
|
|
17
34
|
|
|
18
|
-
|
|
35
|
+
for (const domain of ADMIN_ORIGINS) {
|
|
36
|
+
if (origin.startsWith(domain)) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
export function corsHeaders(request: Request): Record<string, string> {
|
package/src/admin/index.ts
CHANGED
package/src/admin/meta.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { djb2Hex } from "../sdk/djb2";
|
|
1
2
|
import { composeMeta, type MetaResponse } from "./schema";
|
|
2
3
|
|
|
3
4
|
let metaData: MetaResponse | null = null;
|
|
@@ -26,18 +27,13 @@ export function setMetaData(data: MetaResponse) {
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Content-based hash for ETag.
|
|
29
|
-
* Uses
|
|
30
|
-
*
|
|
31
|
-
* re-fetch rather than use stale cached meta.
|
|
30
|
+
* Uses DJB2 over the serialised JSON so any definition change
|
|
31
|
+
* results in a different ETag, forcing admin to re-fetch.
|
|
32
32
|
*/
|
|
33
33
|
function getEtag(): string {
|
|
34
34
|
if (!cachedEtag) {
|
|
35
35
|
const str = JSON.stringify(metaData || {});
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < str.length; i++) {
|
|
38
|
-
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
|
39
|
-
}
|
|
40
|
-
cachedEtag = `"meta-${hash.toString(36)}"`;
|
|
36
|
+
cachedEtag = `"meta-${djb2Hex(str)}"`;
|
|
41
37
|
}
|
|
42
38
|
return cachedEtag;
|
|
43
39
|
}
|
package/src/admin/render.ts
CHANGED
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
2
|
import { loadBlocks, withBlocksOverride } from "../cms/loader";
|
|
3
3
|
import { getSection } from "../cms/registry";
|
|
4
|
-
import { resolveValue } from "../cms/resolve";
|
|
4
|
+
import { resolveValue, WELL_KNOWN_TYPES } from "../cms/resolve";
|
|
5
|
+
import { buildHtmlShell } from "../sdk/htmlShell";
|
|
5
6
|
import { LIVE_CONTROLS_SCRIPT } from "./liveControls";
|
|
6
|
-
import { getRenderShellConfig } from "./setup";
|
|
7
7
|
|
|
8
8
|
export { setRenderShell } from "./setup";
|
|
9
9
|
|
|
10
10
|
function wrapInHtmlShell(sectionHtml: string): string {
|
|
11
|
-
|
|
12
|
-
const stylesheets = [
|
|
13
|
-
...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
|
|
14
|
-
cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
|
|
15
|
-
].join("\n ");
|
|
16
|
-
|
|
17
|
-
const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
|
|
18
|
-
const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
|
|
19
|
-
const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
|
|
20
|
-
|
|
21
|
-
return `<!DOCTYPE html>
|
|
22
|
-
<html${langAttr}${themeAttr}>
|
|
23
|
-
<head>
|
|
24
|
-
<meta charset="utf-8" />
|
|
25
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
-
${stylesheets}
|
|
27
|
-
<script>${LIVE_CONTROLS_SCRIPT}</script>
|
|
28
|
-
</head>
|
|
29
|
-
<body${bodyAttr}>
|
|
30
|
-
${sectionHtml}
|
|
31
|
-
</body>
|
|
32
|
-
</html>`;
|
|
11
|
+
return buildHtmlShell({ body: sectionHtml, script: LIVE_CONTROLS_SCRIPT });
|
|
33
12
|
}
|
|
34
13
|
|
|
35
14
|
/**
|
|
@@ -139,7 +118,7 @@ export async function handleRender(request: Request): Promise<Response> {
|
|
|
139
118
|
}
|
|
140
119
|
|
|
141
120
|
// Page compositor: resolve + render all child sections
|
|
142
|
-
if (component ===
|
|
121
|
+
if (component === WELL_KNOWN_TYPES.PAGE) {
|
|
143
122
|
const rawSections = props.sections;
|
|
144
123
|
const resolvedSections = await resolveValue(rawSections);
|
|
145
124
|
const sectionsList = Array.isArray(resolvedSections)
|
package/src/admin/setup.ts
CHANGED
|
@@ -21,9 +21,9 @@ export {
|
|
|
21
21
|
|
|
22
22
|
let cssHref: string | null = null;
|
|
23
23
|
let fontHrefs: string[] = [];
|
|
24
|
-
let themeName = "
|
|
25
|
-
let bodyClass = "
|
|
26
|
-
let htmlLang = "
|
|
24
|
+
let themeName = "";
|
|
25
|
+
let bodyClass = "";
|
|
26
|
+
let htmlLang = "en";
|
|
27
27
|
|
|
28
28
|
export function setRenderShell(opts: {
|
|
29
29
|
css?: string;
|
package/src/cms/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export {
|
|
|
38
38
|
evaluateMatcher,
|
|
39
39
|
getAsyncRenderingConfig,
|
|
40
40
|
onBeforeResolve,
|
|
41
|
+
registerBotPattern,
|
|
41
42
|
registerCommerceLoader,
|
|
42
43
|
registerCommerceLoaders,
|
|
43
44
|
registerMatcher,
|
|
@@ -47,6 +48,7 @@ export {
|
|
|
47
48
|
setAsyncRenderingConfig,
|
|
48
49
|
setDanglingReferenceHandler,
|
|
49
50
|
setResolveErrorHandler,
|
|
51
|
+
WELL_KNOWN_TYPES,
|
|
50
52
|
} from "./resolve";
|
|
51
53
|
export type { SectionLoaderFn } from "./sectionLoaders";
|
|
52
54
|
export {
|
package/src/cms/loader.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as asyncHooks from "node:async_hooks";
|
|
2
|
+
import { djb2Hex } from "../sdk/djb2";
|
|
2
3
|
|
|
3
4
|
export type Resolvable = {
|
|
4
5
|
__resolveType?: string;
|
|
@@ -54,12 +55,7 @@ export function onChange(listener: ChangeListener) {
|
|
|
54
55
|
// ---------------------------------------------------------------------------
|
|
55
56
|
|
|
56
57
|
function computeRevision(blocks: Record<string, unknown>): string {
|
|
57
|
-
|
|
58
|
-
let hash = 5381;
|
|
59
|
-
for (let i = 0; i < str.length; i++) {
|
|
60
|
-
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
|
61
|
-
}
|
|
62
|
-
return hash.toString(36);
|
|
58
|
+
return djb2Hex(JSON.stringify(blocks));
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
// ---------------------------------------------------------------------------
|
package/src/cms/registry.ts
CHANGED
|
@@ -83,7 +83,8 @@ export async function preloadSectionModule(
|
|
|
83
83
|
if (mod.ErrorFallback) opts.errorFallback = mod.ErrorFallback;
|
|
84
84
|
sectionOptions[resolveType] = opts;
|
|
85
85
|
return opts;
|
|
86
|
-
} catch {
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn(`[Registry] Failed to preload section module "${resolveType}":`, e);
|
|
87
88
|
return existing;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -125,8 +126,8 @@ export async function preloadSectionComponents(keys: string[]): Promise<void> {
|
|
|
125
126
|
if (mod.LoadingFallback) opts.loadingFallback = mod.LoadingFallback;
|
|
126
127
|
if (mod.ErrorFallback) opts.errorFallback = mod.ErrorFallback;
|
|
127
128
|
sectionOptions[key] = opts;
|
|
128
|
-
} catch {
|
|
129
|
-
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.warn(`[Registry] Failed to preload component "${key}":`, e);
|
|
130
131
|
}
|
|
131
132
|
}),
|
|
132
133
|
);
|
package/src/cms/resolve.ts
CHANGED
|
@@ -8,6 +8,24 @@ if (!G.__deco) G.__deco = {};
|
|
|
8
8
|
if (!G.__deco.commerceLoaders) G.__deco.commerceLoaders = {};
|
|
9
9
|
if (!G.__deco.customMatchers) G.__deco.customMatchers = {};
|
|
10
10
|
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Well-known resolve types — extracted as constants so they're searchable
|
|
13
|
+
// and overridable. Consumers migrating from deco-cx/deco may have blocks
|
|
14
|
+
// with these __resolveType strings in their CMS JSON.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** @internal */
|
|
18
|
+
export const WELL_KNOWN_TYPES = {
|
|
19
|
+
LAZY: "website/sections/Rendering/Lazy.tsx",
|
|
20
|
+
DEFERRED: "website/sections/Rendering/Deferred.tsx",
|
|
21
|
+
REQUEST_TO_PARAM: "website/functions/requestToParam.ts",
|
|
22
|
+
COMMERCE_EXT_DETAILS: "commerce/loaders/product/extensions/detailsPage.ts",
|
|
23
|
+
COMMERCE_EXT_LISTING: "commerce/loaders/product/extensions/listingPage.ts",
|
|
24
|
+
MULTIVARIATE: "website/flags/multivariate.ts",
|
|
25
|
+
MULTIVARIATE_SECTION: "website/flags/multivariate/section.ts",
|
|
26
|
+
PAGE: "website/pages/Page.tsx",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
11
29
|
export type ResolvedSection = {
|
|
12
30
|
component: string;
|
|
13
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -93,12 +111,21 @@ export function getAsyncRenderingConfig(): AsyncRenderingConfig | null {
|
|
|
93
111
|
// Bot detection — bots always receive fully eager pages for SEO
|
|
94
112
|
// ---------------------------------------------------------------------------
|
|
95
113
|
|
|
96
|
-
const
|
|
97
|
-
/bot|crawl|spider|slurp|facebookexternalhit|mediapartners|google|bing|yandex|baidu|duckduck|teoma|ia_archiver|semrush|ahrefs|lighthouse/i
|
|
114
|
+
const botPatterns: RegExp[] = [
|
|
115
|
+
/bot|crawl|spider|slurp|facebookexternalhit|mediapartners|google|bing|yandex|baidu|duckduck|teoma|ia_archiver|semrush|ahrefs|lighthouse/i,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Add a custom bot detection regex.
|
|
120
|
+
* Requests matching any bot pattern receive fully eager pages for SEO.
|
|
121
|
+
*/
|
|
122
|
+
export function registerBotPattern(pattern: RegExp): void {
|
|
123
|
+
botPatterns.push(pattern);
|
|
124
|
+
}
|
|
98
125
|
|
|
99
126
|
function isBot(userAgent?: string): boolean {
|
|
100
127
|
if (!userAgent) return false;
|
|
101
|
-
return
|
|
128
|
+
return botPatterns.some((re) => re.test(userAgent));
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
export type CommerceLoader = (props: any) => Promise<any>;
|
|
@@ -187,6 +214,44 @@ export function registerMatcher(
|
|
|
187
214
|
customMatchers[key] = fn;
|
|
188
215
|
}
|
|
189
216
|
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Built-in matchers — registered through the same API as custom matchers
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
if (!G.__deco._builtinMatchersRegistered) {
|
|
222
|
+
G.__deco._builtinMatchersRegistered = true;
|
|
223
|
+
|
|
224
|
+
const builtinMatchers: Record<string, (rule: Record<string, unknown>, ctx: MatcherContext) => boolean> = {
|
|
225
|
+
"website/matchers/always.ts": () => true,
|
|
226
|
+
"$live/matchers/MatchAlways.ts": () => true,
|
|
227
|
+
"website/matchers/never.ts": () => false,
|
|
228
|
+
"website/matchers/device.ts": (rule, ctx) => {
|
|
229
|
+
const ua = (ctx.userAgent || "").toLowerCase();
|
|
230
|
+
const isMobile = /mobile|android|iphone|ipad|ipod|webos|blackberry|opera mini|iemobile/i.test(ua);
|
|
231
|
+
if (rule.mobile) return isMobile;
|
|
232
|
+
if (rule.desktop) return !isMobile;
|
|
233
|
+
return true;
|
|
234
|
+
},
|
|
235
|
+
"website/matchers/random.ts": (rule) => {
|
|
236
|
+
const traffic = typeof rule.traffic === "number" ? rule.traffic : 0.5;
|
|
237
|
+
return Math.random() < traffic;
|
|
238
|
+
},
|
|
239
|
+
"website/matchers/date.ts": (rule) => {
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const start = typeof rule.start === "string" ? new Date(rule.start).getTime() : 0;
|
|
242
|
+
const end = typeof rule.end === "string" ? new Date(rule.end).getTime() : Infinity;
|
|
243
|
+
return now >= start && now <= end;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const [key, fn] of Object.entries(builtinMatchers)) {
|
|
248
|
+
// Only register if not already overridden by consumer
|
|
249
|
+
if (!customMatchers[key]) {
|
|
250
|
+
customMatchers[key] = fn;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
190
255
|
// ---------------------------------------------------------------------------
|
|
191
256
|
// Error handling
|
|
192
257
|
// ---------------------------------------------------------------------------
|
|
@@ -253,49 +318,17 @@ export function evaluateMatcher(rule: Record<string, unknown> | undefined, ctx:
|
|
|
253
318
|
);
|
|
254
319
|
}
|
|
255
320
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
case "website/matchers/never.ts":
|
|
262
|
-
return false;
|
|
263
|
-
|
|
264
|
-
case "website/matchers/device.ts": {
|
|
265
|
-
const ua = (ctx.userAgent || "").toLowerCase();
|
|
266
|
-
const isMobile = /mobile|android|iphone|ipad|ipod|webos|blackberry|opera mini|iemobile/i.test(
|
|
267
|
-
ua,
|
|
268
|
-
);
|
|
269
|
-
if (rule.mobile) return isMobile;
|
|
270
|
-
if (rule.desktop) return !isMobile;
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
case "website/matchers/random.ts": {
|
|
275
|
-
const traffic = typeof rule.traffic === "number" ? rule.traffic : 0.5;
|
|
276
|
-
return Math.random() < traffic;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
case "website/matchers/date.ts": {
|
|
280
|
-
const now = Date.now();
|
|
281
|
-
const start = typeof rule.start === "string" ? new Date(rule.start).getTime() : 0;
|
|
282
|
-
const end = typeof rule.end === "string" ? new Date(rule.end).getTime() : Infinity;
|
|
283
|
-
return now >= start && now <= end;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
default: {
|
|
287
|
-
const customMatcher = customMatchers[resolveType];
|
|
288
|
-
if (customMatcher) {
|
|
289
|
-
try {
|
|
290
|
-
return customMatcher(rule, ctx);
|
|
291
|
-
} catch {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
console.warn(`[CMS] Unknown matcher: ${resolveType}, defaulting to false`);
|
|
321
|
+
const matcher = customMatchers[resolveType];
|
|
322
|
+
if (matcher) {
|
|
323
|
+
try {
|
|
324
|
+
return matcher(rule, ctx);
|
|
325
|
+
} catch {
|
|
296
326
|
return false;
|
|
297
327
|
}
|
|
298
328
|
}
|
|
329
|
+
|
|
330
|
+
console.warn(`[CMS] Unknown matcher: ${resolveType}, defaulting to false`);
|
|
331
|
+
return false;
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
// ---------------------------------------------------------------------------
|
|
@@ -357,33 +390,33 @@ async function internalResolve(value: unknown, rctx: ResolveContext): Promise<un
|
|
|
357
390
|
if (resolveType === "resolved") return obj.data ?? null;
|
|
358
391
|
|
|
359
392
|
// Lazy section wrapper — unwrap single inner section
|
|
360
|
-
if (resolveType ===
|
|
393
|
+
if (resolveType === WELL_KNOWN_TYPES.LAZY) {
|
|
361
394
|
return obj.section ? internalResolve(obj.section, childCtx) : null;
|
|
362
395
|
}
|
|
363
396
|
|
|
364
397
|
// Deferred section wrapper (legacy Fresh/HTMX) — unwrap inner sections array
|
|
365
|
-
if (resolveType ===
|
|
398
|
+
if (resolveType === WELL_KNOWN_TYPES.DEFERRED) {
|
|
366
399
|
return obj.sections ? internalResolve(obj.sections, childCtx) : null;
|
|
367
400
|
}
|
|
368
401
|
|
|
369
402
|
// Request param extraction
|
|
370
|
-
if (resolveType ===
|
|
403
|
+
if (resolveType === WELL_KNOWN_TYPES.REQUEST_TO_PARAM) {
|
|
371
404
|
const paramName = (obj as any).param as string;
|
|
372
405
|
return rctx.routeParams?.[paramName] ?? null;
|
|
373
406
|
}
|
|
374
407
|
|
|
375
408
|
// Commerce extension wrappers — unwrap to inner data
|
|
376
409
|
if (
|
|
377
|
-
resolveType ===
|
|
378
|
-
resolveType ===
|
|
410
|
+
resolveType === WELL_KNOWN_TYPES.COMMERCE_EXT_DETAILS ||
|
|
411
|
+
resolveType === WELL_KNOWN_TYPES.COMMERCE_EXT_LISTING
|
|
379
412
|
) {
|
|
380
413
|
return obj.data ? internalResolve(obj.data, childCtx) : null;
|
|
381
414
|
}
|
|
382
415
|
|
|
383
416
|
// Multivariate flags
|
|
384
417
|
if (
|
|
385
|
-
resolveType ===
|
|
386
|
-
resolveType ===
|
|
418
|
+
resolveType === WELL_KNOWN_TYPES.MULTIVARIATE ||
|
|
419
|
+
resolveType === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
387
420
|
) {
|
|
388
421
|
const variants = obj.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
389
422
|
if (!variants || variants.length === 0) return null;
|
|
@@ -631,7 +664,7 @@ function resolveFinalSectionKey(
|
|
|
631
664
|
if (!rt) return null;
|
|
632
665
|
|
|
633
666
|
// Lazy wrapper — unwrap single inner section
|
|
634
|
-
if (rt ===
|
|
667
|
+
if (rt === WELL_KNOWN_TYPES.LAZY) {
|
|
635
668
|
const inner = current.section;
|
|
636
669
|
if (!inner || typeof inner !== "object") return null;
|
|
637
670
|
current = inner as Record<string, unknown>;
|
|
@@ -655,8 +688,8 @@ function resolveFinalSectionKey(
|
|
|
655
688
|
}
|
|
656
689
|
|
|
657
690
|
if (
|
|
658
|
-
rt ===
|
|
659
|
-
rt ===
|
|
691
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE ||
|
|
692
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
660
693
|
) {
|
|
661
694
|
const variants = current.variants as
|
|
662
695
|
| Array<{ value: unknown; rule?: unknown }>
|
|
@@ -708,16 +741,16 @@ function isCmsDeferralWrapped(section: unknown, matcherCtx?: MatcherContext): bo
|
|
|
708
741
|
if (!rt) return false;
|
|
709
742
|
|
|
710
743
|
if (
|
|
711
|
-
rt ===
|
|
712
|
-
rt ===
|
|
744
|
+
rt === WELL_KNOWN_TYPES.LAZY ||
|
|
745
|
+
rt === WELL_KNOWN_TYPES.DEFERRED
|
|
713
746
|
) {
|
|
714
747
|
return true;
|
|
715
748
|
}
|
|
716
749
|
|
|
717
750
|
// Walk through multivariate flags to check the matched variant
|
|
718
751
|
if (
|
|
719
|
-
rt ===
|
|
720
|
-
rt ===
|
|
752
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE ||
|
|
753
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
721
754
|
) {
|
|
722
755
|
const variants = current.variants as
|
|
723
756
|
| Array<{ value: unknown; rule?: unknown }>
|
|
@@ -806,7 +839,7 @@ function resolveSectionShallow(
|
|
|
806
839
|
if (SKIP_RESOLVE_TYPES.has(rt)) return null;
|
|
807
840
|
|
|
808
841
|
// Lazy wrapper — unwrap to the inner section
|
|
809
|
-
if (rt ===
|
|
842
|
+
if (rt === WELL_KNOWN_TYPES.LAZY) {
|
|
810
843
|
const inner = current.section;
|
|
811
844
|
if (!inner || typeof inner !== "object") return null;
|
|
812
845
|
current = inner as Record<string, unknown>;
|
|
@@ -832,8 +865,8 @@ function resolveSectionShallow(
|
|
|
832
865
|
|
|
833
866
|
// Multivariate flags — evaluate matchers and continue with matched variant
|
|
834
867
|
if (
|
|
835
|
-
rt ===
|
|
836
|
-
rt ===
|
|
868
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE ||
|
|
869
|
+
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
837
870
|
) {
|
|
838
871
|
const variants = current.variants as
|
|
839
872
|
| Array<{ value: unknown; rule?: unknown }>
|
|
@@ -897,7 +930,7 @@ async function resolveSectionsList(
|
|
|
897
930
|
if (!rt) return [];
|
|
898
931
|
|
|
899
932
|
// Multivariate flags — evaluate matchers and recurse into matched variant
|
|
900
|
-
if (rt ===
|
|
933
|
+
if (rt === WELL_KNOWN_TYPES.MULTIVARIATE || rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION) {
|
|
901
934
|
const variants = obj.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
902
935
|
if (!variants?.length) return [];
|
|
903
936
|
for (const variant of variants) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* This runs AFTER resolveDecoPage and BEFORE React rendering,
|
|
9
9
|
* inside the TanStack Start server function.
|
|
10
10
|
*/
|
|
11
|
+
import { djb2 } from "../sdk/djb2";
|
|
11
12
|
import type { ResolvedSection } from "./resolve";
|
|
12
13
|
|
|
13
14
|
export type SectionLoaderFn = (
|
|
@@ -51,16 +52,8 @@ function evictSectionCacheIfNeeded() {
|
|
|
51
52
|
for (const [key] of toDelete) sectionLoaderCache.delete(key);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function djb2Hash(str: string): number {
|
|
55
|
-
let hash = 5381;
|
|
56
|
-
for (let i = 0; i < str.length; i++) {
|
|
57
|
-
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
58
|
-
}
|
|
59
|
-
return hash >>> 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
55
|
function sectionCacheKey(component: string, props: Record<string, unknown>): string {
|
|
63
|
-
return `${component}::${
|
|
56
|
+
return `${component}::${djb2(JSON.stringify(props))}`;
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
/**
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
setResolvedComponent,
|
|
19
19
|
} from "../cms/registry";
|
|
20
20
|
import type { DeferredSection, ResolvedSection } from "../cms/resolve";
|
|
21
|
+
import { djb2Hex } from "../sdk/djb2";
|
|
21
22
|
import { SectionErrorBoundary } from "./SectionErrorFallback";
|
|
22
23
|
|
|
23
24
|
type LazyComponent = ReturnType<typeof lazy>;
|
|
@@ -106,14 +107,6 @@ function getCachedDeferredSection(stableKey: string): ResolvedSection | null {
|
|
|
106
107
|
return entry.section;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
/** Fast DJB2 hash for cache key differentiation. */
|
|
110
|
-
function djb2(str: string): string {
|
|
111
|
-
let hash = 5381;
|
|
112
|
-
for (let i = 0; i < str.length; i++) {
|
|
113
|
-
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
114
|
-
}
|
|
115
|
-
return (hash >>> 0).toString(36);
|
|
116
|
-
}
|
|
117
110
|
|
|
118
111
|
const DEFERRED_FADE_CSS = `@keyframes decoFadeIn{from{opacity:0}to{opacity:1}}`;
|
|
119
112
|
|
|
@@ -228,7 +221,7 @@ function DeferredSectionWrapper({
|
|
|
228
221
|
errorFallback,
|
|
229
222
|
loadFn,
|
|
230
223
|
}: DeferredSectionWrapperProps) {
|
|
231
|
-
const propsHash =
|
|
224
|
+
const propsHash = djb2Hex(JSON.stringify(deferred.rawProps));
|
|
232
225
|
const stableKey = `${pagePath}::${deferred.component}::${deferred.index}::${propsHash}`;
|
|
233
226
|
const [section, setSection] = useState<ResolvedSection | null>(() =>
|
|
234
227
|
typeof document === "undefined" ? null : getCachedDeferredSection(stableKey),
|
|
@@ -87,3 +87,18 @@ export function resolveCountryCode(name: string): string {
|
|
|
87
87
|
// Assume it's already an ISO code
|
|
88
88
|
return name.toUpperCase();
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register an additional country name → ISO code mapping at runtime.
|
|
93
|
+
* Useful for site-specific CMS values not covered by the built-in list.
|
|
94
|
+
*/
|
|
95
|
+
export function registerCountryMapping(name: string, code: string): void {
|
|
96
|
+
COUNTRY_NAME_TO_CODE[name] = code;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register multiple country name → ISO code mappings at once.
|
|
101
|
+
*/
|
|
102
|
+
export function registerCountryMappings(mappings: Record<string, string>): void {
|
|
103
|
+
Object.assign(COUNTRY_NAME_TO_CODE, mappings);
|
|
104
|
+
}
|
package/src/sdk/cacheHeaders.ts
CHANGED
|
@@ -128,6 +128,20 @@ export function getCacheProfileConfig(profile: CacheProfile): CacheHeadersConfig
|
|
|
128
128
|
return PROFILES[profile];
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Override the default TTLs for a cache profile.
|
|
133
|
+
* Useful when the built-in values don't match your site's
|
|
134
|
+
* freshness requirements (e.g., longer product TTL for low-traffic stores).
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* setCacheProfileConfig("product", { maxAge: 120, sMaxAge: 600, staleWhileRevalidate: 7200, isPublic: true });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function setCacheProfileConfig(profile: CacheProfile, config: CacheHeadersConfig): void {
|
|
142
|
+
PROFILES[profile] = config;
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
// ---------------------------------------------------------------------------
|
|
132
146
|
// Client-side route cache defaults (TanStack Router staleTime / gcTime)
|
|
133
147
|
// ---------------------------------------------------------------------------
|
|
@@ -173,6 +187,18 @@ export function routeCacheDefaults(profile: CacheProfile): RouteCacheDefaults {
|
|
|
173
187
|
return ROUTE_CACHE[profile];
|
|
174
188
|
}
|
|
175
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Override the client-side route cache defaults for a profile.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* setRouteCacheDefaults("product", { staleTime: 120_000, gcTime: 10 * 60_000 });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function setRouteCacheDefaults(profile: CacheProfile, defaults: RouteCacheDefaults): void {
|
|
199
|
+
ROUTE_CACHE[profile] = defaults;
|
|
200
|
+
}
|
|
201
|
+
|
|
176
202
|
// ---------------------------------------------------------------------------
|
|
177
203
|
// URL-based cache profile detection
|
|
178
204
|
// ---------------------------------------------------------------------------
|
package/src/sdk/djb2.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DJB2 hash — a fast, non-cryptographic hash function.
|
|
3
|
+
*
|
|
4
|
+
* Used for ETags, cache keys, and content fingerprinting throughout the framework.
|
|
5
|
+
* Produces consistent unsigned 32-bit integers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Compute a DJB2 hash and return the raw unsigned 32-bit integer. */
|
|
9
|
+
export function djb2(str: string): number {
|
|
10
|
+
let hash = 5381;
|
|
11
|
+
for (let i = 0; i < str.length; i++) {
|
|
12
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
13
|
+
}
|
|
14
|
+
return hash >>> 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Compute a DJB2 hash and return a base-36 string. */
|
|
18
|
+
export function djb2Hex(str: string): string {
|
|
19
|
+
return djb2(str).toString(36);
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML shell builder for admin preview iframes and render endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Both `workerEntry.ts` (preview shell) and `admin/render.ts` (section render)
|
|
5
|
+
* need to produce an HTML document with the site's CSS, fonts, and theme.
|
|
6
|
+
* This module provides a single implementation to keep them consistent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getRenderShellConfig } from "../admin/setup";
|
|
10
|
+
|
|
11
|
+
export interface HtmlShellOptions {
|
|
12
|
+
/** Content to inject into <body>. */
|
|
13
|
+
body?: string;
|
|
14
|
+
/** Inline <script> content to inject in <head>. */
|
|
15
|
+
script?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a complete HTML shell using the current render config
|
|
20
|
+
* (set via `setRenderShell()` in the site's setup.ts).
|
|
21
|
+
*/
|
|
22
|
+
export function buildHtmlShell(options: HtmlShellOptions = {}): string {
|
|
23
|
+
const { cssHref, fontHrefs, themeName, bodyClass, htmlLang } = getRenderShellConfig();
|
|
24
|
+
|
|
25
|
+
const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
|
|
26
|
+
const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
|
|
27
|
+
const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
|
|
28
|
+
|
|
29
|
+
const stylesheets = [
|
|
30
|
+
...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
|
|
31
|
+
cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
|
|
32
|
+
]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join("\n ");
|
|
35
|
+
|
|
36
|
+
const scriptTag = options.script ? `<script>${options.script}</script>` : "";
|
|
37
|
+
|
|
38
|
+
const bodyContent = options.body ?? `<div id="preview-root" style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui;color:#666;">
|
|
39
|
+
Loading preview...
|
|
40
|
+
</div>`;
|
|
41
|
+
|
|
42
|
+
return `<!DOCTYPE html>
|
|
43
|
+
<html${langAttr}${themeAttr}>
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
47
|
+
<title>Preview</title>
|
|
48
|
+
${stylesheets}
|
|
49
|
+
${scriptTag}
|
|
50
|
+
</head>
|
|
51
|
+
<body${bodyAttr}>
|
|
52
|
+
${bodyContent}
|
|
53
|
+
</body>
|
|
54
|
+
</html>`;
|
|
55
|
+
}
|
package/src/sdk/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export {
|
|
|
14
14
|
getCacheProfileConfig,
|
|
15
15
|
registerCachePattern,
|
|
16
16
|
routeCacheDefaults,
|
|
17
|
+
setCacheProfileConfig,
|
|
18
|
+
setRouteCacheDefaults,
|
|
17
19
|
} from "./cacheHeaders";
|
|
18
20
|
export { clx } from "./clx";
|
|
19
21
|
export { decodeCookie, deleteCookie, getCookie, getServerSideCookie, setCookie } from "./cookie";
|
|
@@ -39,6 +41,7 @@ export {
|
|
|
39
41
|
parseRedirectsCsv,
|
|
40
42
|
type Redirect,
|
|
41
43
|
type RedirectMap,
|
|
44
|
+
registerRedirectResolveType,
|
|
42
45
|
} from "./redirects";
|
|
43
46
|
export { RequestContext, type RequestContextData } from "./requestContext";
|
|
44
47
|
export { createServerTimings, type ServerTimings } from "./serverTimings";
|
|
@@ -47,14 +50,21 @@ export {
|
|
|
47
50
|
canonicalUrl,
|
|
48
51
|
cleanPathForCacheKey,
|
|
49
52
|
hasTrackingParams,
|
|
53
|
+
registerTrackingParam,
|
|
54
|
+
registerTrackingParams,
|
|
50
55
|
stripTrackingParams,
|
|
51
56
|
} from "./urlUtils";
|
|
57
|
+
export { djb2, djb2Hex } from "./djb2";
|
|
58
|
+
export { buildHtmlShell, type HtmlShellOptions } from "./htmlShell";
|
|
52
59
|
export {
|
|
53
60
|
checkDesktop,
|
|
54
61
|
checkMobile,
|
|
55
62
|
checkTablet,
|
|
56
63
|
type Device,
|
|
57
64
|
detectDevice,
|
|
65
|
+
isMobileUA,
|
|
66
|
+
MOBILE_RE,
|
|
67
|
+
TABLET_RE,
|
|
58
68
|
useDevice,
|
|
59
69
|
} from "./useDevice";
|
|
60
70
|
export { useId } from "./useId";
|
package/src/sdk/redirects.ts
CHANGED
|
@@ -66,6 +66,14 @@ const REDIRECT_RESOLVE_TYPES = new Set([
|
|
|
66
66
|
"deco-sites/std/loaders/x/redirects.ts",
|
|
67
67
|
]);
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Register additional __resolveType strings that should be treated as redirect blocks.
|
|
71
|
+
* Useful for custom redirect loaders.
|
|
72
|
+
*/
|
|
73
|
+
export function registerRedirectResolveType(resolveType: string): void {
|
|
74
|
+
REDIRECT_RESOLVE_TYPES.add(resolveType);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
/**
|
|
70
78
|
* Load all redirect definitions from CMS blocks.
|
|
71
79
|
*
|
|
@@ -52,7 +52,8 @@ export interface RequestContextData {
|
|
|
52
52
|
|
|
53
53
|
const storage = new AsyncLocalStorage<RequestContextData>();
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
import { isMobileUA } from "./useDevice";
|
|
56
|
+
|
|
56
57
|
const BOT_RE =
|
|
57
58
|
/bot|crawl|spider|slurp|bingpreview|facebookexternalhit|linkedinbot|twitterbot|whatsapp|telegram|googlebot|yandex|baidu|duckduck/i;
|
|
58
59
|
|
|
@@ -126,7 +127,7 @@ export const RequestContext = {
|
|
|
126
127
|
if (!ctx) return "desktop";
|
|
127
128
|
if (ctx._device) return ctx._device;
|
|
128
129
|
const ua = ctx.request.headers.get("user-agent") ?? "";
|
|
129
|
-
ctx._device =
|
|
130
|
+
ctx._device = isMobileUA(ua) ? "mobile" : "desktop";
|
|
130
131
|
return ctx._device;
|
|
131
132
|
},
|
|
132
133
|
|
package/src/sdk/sitemap.ts
CHANGED
|
@@ -47,6 +47,17 @@ export interface SitemapOptions {
|
|
|
47
47
|
maxEntries?: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface CMSSitemapOptions {
|
|
51
|
+
/** Default changefreq for non-home pages. @default "weekly" */
|
|
52
|
+
defaultChangefreq?: SitemapEntry["changefreq"];
|
|
53
|
+
/** Default priority for non-home pages (0.0–1.0). @default 0.7 */
|
|
54
|
+
defaultPriority?: number;
|
|
55
|
+
/** Changefreq for the home page. @default "daily" */
|
|
56
|
+
homeChangefreq?: SitemapEntry["changefreq"];
|
|
57
|
+
/** Priority for the home page (0.0–1.0). @default 1.0 */
|
|
58
|
+
homePriority?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
// -------------------------------------------------------------------------
|
|
51
62
|
// CMS page entries
|
|
52
63
|
// -------------------------------------------------------------------------
|
|
@@ -57,22 +68,28 @@ export interface SitemapOptions {
|
|
|
57
68
|
* Reads all pages from the block store and generates URLs from their
|
|
58
69
|
* path patterns (excluding wildcard-only patterns like `/*`).
|
|
59
70
|
*/
|
|
60
|
-
export function getCMSSitemapEntries(origin: string): SitemapEntry[] {
|
|
71
|
+
export function getCMSSitemapEntries(origin: string, options?: CMSSitemapOptions): SitemapEntry[] {
|
|
61
72
|
const pages = getAllPages();
|
|
62
73
|
const entries: SitemapEntry[] = [];
|
|
63
74
|
const today = new Date().toISOString().split("T")[0];
|
|
64
75
|
|
|
76
|
+
const defaultChangefreq = options?.defaultChangefreq ?? "weekly";
|
|
77
|
+
const defaultPriority = options?.defaultPriority ?? 0.7;
|
|
78
|
+
const homeChangefreq = options?.homeChangefreq ?? "daily";
|
|
79
|
+
const homePriority = options?.homePriority ?? 1.0;
|
|
80
|
+
|
|
65
81
|
for (const { page } of pages) {
|
|
66
82
|
if (!page.path) continue;
|
|
67
83
|
|
|
68
84
|
if (page.path.includes("*") || page.path.includes(":")) continue;
|
|
69
85
|
|
|
70
|
-
const
|
|
86
|
+
const isHome = page.path === "/";
|
|
87
|
+
const loc = `${origin}${isHome ? "" : page.path}`;
|
|
71
88
|
entries.push({
|
|
72
89
|
loc: loc || origin,
|
|
73
90
|
lastmod: today,
|
|
74
|
-
changefreq:
|
|
75
|
-
priority:
|
|
91
|
+
changefreq: isHome ? homeChangefreq : defaultChangefreq,
|
|
92
|
+
priority: isHome ? homePriority : defaultPriority,
|
|
76
93
|
});
|
|
77
94
|
}
|
|
78
95
|
|
package/src/sdk/urlUtils.ts
CHANGED
|
@@ -27,6 +27,23 @@ const UTM_PARAMS = new Set([
|
|
|
27
27
|
"srsltid",
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Register additional tracking parameters to strip from cache keys.
|
|
32
|
+
* Useful for proprietary attribution params (e.g., custom analytics tags).
|
|
33
|
+
*/
|
|
34
|
+
export function registerTrackingParam(param: string): void {
|
|
35
|
+
UTM_PARAMS.add(param.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register multiple additional tracking parameters at once.
|
|
40
|
+
*/
|
|
41
|
+
export function registerTrackingParams(params: string[]): void {
|
|
42
|
+
for (const param of params) {
|
|
43
|
+
UTM_PARAMS.add(param.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Strip UTM and tracking parameters from a URL.
|
|
32
49
|
*
|
package/src/sdk/useDevice.ts
CHANGED
|
@@ -28,8 +28,17 @@ export type Device = "mobile" | "tablet" | "desktop";
|
|
|
28
28
|
// Android phones include "Mobile" in their UA; Android tablets do not.
|
|
29
29
|
// Check TABLET_RE first so `android(?!.*mobile)` captures tablets before
|
|
30
30
|
// the MOBILE_RE `android.*mobile` branch matches phones.
|
|
31
|
-
const MOBILE_RE = /mobile|android.*mobile|iphone|ipod|webos|blackberry|opera mini|iemobile/i;
|
|
32
|
-
const TABLET_RE = /ipad|tablet|kindle|silk|playbook|android(?!.*mobile)/i;
|
|
31
|
+
export const MOBILE_RE = /mobile|android.*mobile|iphone|ipod|webos|blackberry|opera mini|iemobile/i;
|
|
32
|
+
export const TABLET_RE = /ipad|tablet|kindle|silk|playbook|android(?!.*mobile)/i;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Simple mobile-or-not check (mobile + tablet = true).
|
|
36
|
+
* Use this for cache key splitting or any context where you
|
|
37
|
+
* only need a mobile/desktop binary decision.
|
|
38
|
+
*/
|
|
39
|
+
export function isMobileUA(userAgent: string): boolean {
|
|
40
|
+
return MOBILE_RE.test(userAgent) || TABLET_RE.test(userAgent);
|
|
41
|
+
}
|
|
33
42
|
|
|
34
43
|
/**
|
|
35
44
|
* Detect device type from a User-Agent string.
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { getRenderShellConfig } from "../admin/setup";
|
|
29
28
|
import {
|
|
30
29
|
type CacheProfile,
|
|
31
30
|
cacheHeaders,
|
|
32
31
|
detectCacheProfile,
|
|
33
32
|
getCacheProfileConfig,
|
|
34
33
|
} from "./cacheHeaders";
|
|
34
|
+
import { buildHtmlShell } from "./htmlShell";
|
|
35
35
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
36
|
+
import { isMobileUA } from "./useDevice";
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
// Types
|
|
@@ -244,34 +245,7 @@ const PREVIEW_SHELL_SCRIPT = `(function() {
|
|
|
244
245
|
})();`;
|
|
245
246
|
|
|
246
247
|
function buildPreviewShell(): string {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
|
|
250
|
-
const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
|
|
251
|
-
const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
|
|
252
|
-
|
|
253
|
-
const stylesheets = [
|
|
254
|
-
...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
|
|
255
|
-
cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
|
|
256
|
-
]
|
|
257
|
-
.filter(Boolean)
|
|
258
|
-
.join("\n ");
|
|
259
|
-
|
|
260
|
-
return `<!DOCTYPE html>
|
|
261
|
-
<html${langAttr}${themeAttr}>
|
|
262
|
-
<head>
|
|
263
|
-
<meta charset="utf-8" />
|
|
264
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
265
|
-
<title>Preview</title>
|
|
266
|
-
${stylesheets}
|
|
267
|
-
<script>${PREVIEW_SHELL_SCRIPT}</script>
|
|
268
|
-
</head>
|
|
269
|
-
<body${bodyAttr}>
|
|
270
|
-
<div id="preview-root" style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui;color:#666;">
|
|
271
|
-
Loading preview...
|
|
272
|
-
</div>
|
|
273
|
-
</body>
|
|
274
|
-
</html>`;
|
|
248
|
+
return buildHtmlShell({ script: PREVIEW_SHELL_SCRIPT });
|
|
275
249
|
}
|
|
276
250
|
|
|
277
251
|
// ---------------------------------------------------------------------------
|
|
@@ -316,7 +290,6 @@ export function injectGeoCookies(request: Request): Request {
|
|
|
316
290
|
return new Request(request, { headers });
|
|
317
291
|
}
|
|
318
292
|
|
|
319
|
-
const MOBILE_RE = /mobile|android|iphone|ipad|ipod/i;
|
|
320
293
|
const ONE_YEAR = 31536000;
|
|
321
294
|
|
|
322
295
|
const DEFAULT_BYPASS_PATHS = ["/_server", "/_build", "/deco/", "/live/", "/.decofile"];
|
|
@@ -433,7 +406,7 @@ export function createDecoWorkerEntry(
|
|
|
433
406
|
}
|
|
434
407
|
|
|
435
408
|
if (deviceSpecificKeys) {
|
|
436
|
-
const device =
|
|
409
|
+
const device = isMobileUA(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
437
410
|
url.searchParams.set("__cf_device", device);
|
|
438
411
|
}
|
|
439
412
|
|