@akinon/pz-theme 2.0.63-rc.0 → 2.0.63
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/CHANGELOG.md +8 -31
- package/package.json +4 -4
- package/src/__tests__/route-slug.test.ts +17 -0
- package/src/chrome/error-composition-source.ts +43 -0
- package/src/chrome/error-composition.tsx +42 -0
- package/src/chrome/page-context.ts +18 -0
- package/src/chrome/placeholder-slugs.ts +2 -1
- package/src/chrome/route-slug.ts +13 -1
- package/src/index.ts +8 -0
- package/src/load-placeholder-sections.ts +335 -0
- package/src/theme-placeholder.tsx +21 -315
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.63
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- Updated dependencies [
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Updated dependencies [2fea4143]
|
|
12
|
-
- Updated dependencies [b55acb768]
|
|
13
|
-
- Updated dependencies [760258c1c]
|
|
14
|
-
- Updated dependencies [143be2b9d]
|
|
15
|
-
- Updated dependencies [7889b08fe]
|
|
16
|
-
- Updated dependencies [9f8cd3bc5]
|
|
17
|
-
- Updated dependencies [bfafa3f49]
|
|
18
|
-
- Updated dependencies [57d7eb305]
|
|
19
|
-
- Updated dependencies [d99a6a7d5]
|
|
20
|
-
- Updated dependencies [9db81a714]
|
|
21
|
-
- Updated dependencies [591e345e1]
|
|
22
|
-
- Updated dependencies [4de5303c5]
|
|
23
|
-
- Updated dependencies [95b139dc1]
|
|
24
|
-
- Updated dependencies [1d00f2d06]
|
|
25
|
-
- Updated dependencies [4ac7b2a1e]
|
|
26
|
-
- Updated dependencies [e9598c71]
|
|
27
|
-
- Updated dependencies [4998a9631]
|
|
28
|
-
- Updated dependencies [804d2bd6]
|
|
29
|
-
- Updated dependencies [3909d3224]
|
|
30
|
-
- Updated dependencies [6a3d8a63]
|
|
31
|
-
- Updated dependencies [e18836b20]
|
|
32
|
-
- @akinon/next@2.0.63-rc.0
|
|
33
|
-
- @akinon/theme-contract@2.0.63-rc.0
|
|
3
|
+
## 2.0.63
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 72a62c6: ZERO-4823: Implement 404 and 500 pages for theme editor
|
|
8
|
+
- Updated dependencies [72a62c6]
|
|
9
|
+
- @akinon/theme-contract@2.0.63
|
|
10
|
+
- @akinon/next@2.0.63
|
|
34
11
|
|
|
35
12
|
## 2.0.62
|
|
36
13
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.63
|
|
4
|
+
"version": "2.0.63",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.63
|
|
11
|
+
"@akinon/next": "2.0.63",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.63
|
|
16
|
+
"@akinon/theme-contract": "2.0.63",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.63
|
|
23
|
+
"@akinon/next": "2.0.63",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -145,6 +145,23 @@ describe('resolveBodySlug', () => {
|
|
|
145
145
|
expect(isPageMountedSlug('checkout-page-body')).toBe(true);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
it('resolves the 404 screen and keeps it page-mounted', () => {
|
|
149
|
+
expect(resolveBodySlug('/404')).toBe(PLACEHOLDER_SLUGS.notFound);
|
|
150
|
+
expect(resolveBodySlug('/not-found')).toBe(PLACEHOLDER_SLUGS.notFound);
|
|
151
|
+
// The boundary mounts its own slot; a root catch-all must not double it.
|
|
152
|
+
expect(isPageMountedSlug(PLACEHOLDER_SLUGS.notFound)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('resolves the 500 screen and keeps it page-mounted', () => {
|
|
156
|
+
expect(resolveBodySlug('/500')).toBe(PLACEHOLDER_SLUGS.serverError);
|
|
157
|
+
expect(resolveBodySlug('/server-error')).toBe(
|
|
158
|
+
PLACEHOLDER_SLUGS.serverError
|
|
159
|
+
);
|
|
160
|
+
// The error boundary reads its composition from the layout provider; a
|
|
161
|
+
// root catch-all must not mount this slug a second time.
|
|
162
|
+
expect(isPageMountedSlug(PLACEHOLDER_SLUGS.serverError)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
148
165
|
it('returns null for a path with no placeholder', () => {
|
|
149
166
|
expect(resolveBodySlug('/kampanya-bilgilendirme')).toBeNull();
|
|
150
167
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { loadPlaceholderSections } from '../load-placeholder-sections';
|
|
4
|
+
import { generateThemeCSS } from '../utils';
|
|
5
|
+
import { getSlugCompositionState } from './composition-source';
|
|
6
|
+
import { buildErrorPageContext } from './page-context';
|
|
7
|
+
import { PLACEHOLDER_SLUGS } from './placeholder-slugs';
|
|
8
|
+
import type { ErrorComposition } from './error-composition';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prefetches the server-error composition for the layout to hand to the
|
|
12
|
+
* error boundary (see error-composition.tsx for why it must travel early).
|
|
13
|
+
*
|
|
14
|
+
* The cheap request-memoized composition-state read gates the expensive
|
|
15
|
+
* reconstruction: until a designer composes the slug, every page pays one
|
|
16
|
+
* widget read that ChromeSlot/theme-config sharing already amortizes, and
|
|
17
|
+
* nothing more.
|
|
18
|
+
*/
|
|
19
|
+
export async function loadErrorComposition(): Promise<ErrorComposition | null> {
|
|
20
|
+
const slug = PLACEHOLDER_SLUGS.serverError;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const state = await getSlugCompositionState(slug);
|
|
24
|
+
if (state.kind !== 'composed') return null;
|
|
25
|
+
|
|
26
|
+
const loaded = await loadPlaceholderSections(slug);
|
|
27
|
+
if (!loaded || loaded.sections.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
slug,
|
|
31
|
+
placeholderId: loaded.placeholderId,
|
|
32
|
+
sections: loaded.sections,
|
|
33
|
+
themeCSS: generateThemeCSS(loaded.sections),
|
|
34
|
+
themeSettings: loaded.themeSettings,
|
|
35
|
+
pageContext: buildErrorPageContext()
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// A failed prefetch must never take the page down with it — the boundary
|
|
39
|
+
// falls back to the code-owned error UI instead.
|
|
40
|
+
console.warn('Server-error composition prefetch failed:', error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from 'react';
|
|
4
|
+
import type { Section } from '../theme-section';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The server-error composition, reconstructed BEFORE anything fails.
|
|
8
|
+
*
|
|
9
|
+
* Next.js forces `error.tsx` to be a client component and hands it nothing
|
|
10
|
+
* but `{ error, reset }` — it cannot fetch on the server and it cannot
|
|
11
|
+
* receive props from one. The only channel left is a client context mounted
|
|
12
|
+
* by the layout above the boundary: the layout survives a page failure, so
|
|
13
|
+
* whatever it put here is still readable when the boundary renders. It also
|
|
14
|
+
* means the composition arrives with the page, not during the outage — the
|
|
15
|
+
* one moment the widget API itself may be down.
|
|
16
|
+
*/
|
|
17
|
+
export interface ErrorComposition {
|
|
18
|
+
slug: string;
|
|
19
|
+
placeholderId: string;
|
|
20
|
+
sections: Section[];
|
|
21
|
+
/** Pre-generated on the server; the boundary only injects it. */
|
|
22
|
+
themeCSS: string;
|
|
23
|
+
themeSettings: Record<string, unknown> | null;
|
|
24
|
+
pageContext: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ErrorCompositionContext = createContext<ErrorComposition | null>(null);
|
|
28
|
+
|
|
29
|
+
export const ErrorCompositionProvider = ({
|
|
30
|
+
composition,
|
|
31
|
+
children
|
|
32
|
+
}: {
|
|
33
|
+
composition: ErrorComposition | null;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}) => (
|
|
36
|
+
<ErrorCompositionContext.Provider value={composition}>
|
|
37
|
+
{children}
|
|
38
|
+
</ErrorCompositionContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** `null` until a designer composes the server-error placeholder. */
|
|
42
|
+
export const useErrorComposition = () => useContext(ErrorCompositionContext);
|
|
@@ -534,6 +534,24 @@ export const buildCheckoutPageContext = ({
|
|
|
534
534
|
};
|
|
535
535
|
};
|
|
536
536
|
|
|
537
|
+
/** The 404 screen: no commerce entity, just the page identity to bind. */
|
|
538
|
+
export const buildNotFoundPageContext = (path?: string) => ({
|
|
539
|
+
page: {
|
|
540
|
+
type: 'not-found',
|
|
541
|
+
title: 'Page Not Found',
|
|
542
|
+
path: path || '/404'
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/** The 500 screen: same shape as the 404, published ahead of the failure. */
|
|
547
|
+
export const buildErrorPageContext = (path?: string) => ({
|
|
548
|
+
page: {
|
|
549
|
+
type: 'error',
|
|
550
|
+
title: 'Server Error',
|
|
551
|
+
path: path || '/500'
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
537
555
|
/** A CMS flat page: no commerce entity, but a title and a slug to bind. */
|
|
538
556
|
export const buildFlatPageContext = (flatPage?: UnknownRecord | null) => {
|
|
539
557
|
const source = toRecord(flatPage);
|
|
@@ -70,7 +70,8 @@ export const PLACEHOLDER_SLUGS = {
|
|
|
70
70
|
sitemap: 'sitemap-page-body',
|
|
71
71
|
brands: 'brands-page-body',
|
|
72
72
|
flatPage: 'flat-page-body',
|
|
73
|
-
notFound: 'not-found-body'
|
|
73
|
+
notFound: 'not-found-body',
|
|
74
|
+
serverError: 'server-error-body'
|
|
74
75
|
} as const;
|
|
75
76
|
|
|
76
77
|
export type PlaceholderSlug =
|
package/src/chrome/route-slug.ts
CHANGED
|
@@ -93,6 +93,12 @@ const DEFAULT_RULES: RouteSlugRule[] = [
|
|
|
93
93
|
[/^\/sitemap/, PLACEHOLDER_SLUGS.sitemap],
|
|
94
94
|
[/^\/(markalar|brands)/, PLACEHOLDER_SLUGS.brands],
|
|
95
95
|
[/^\/flat-page\//, PLACEHOLDER_SLUGS.flatPage],
|
|
96
|
+
// The not-found boundary renders on ANY unmatched path, so it mounts its
|
|
97
|
+
// own slot; this rule serves the routable /404 page the editor previews.
|
|
98
|
+
[/^\/(404|not-found)(\/|$)/, PLACEHOLDER_SLUGS.notFound],
|
|
99
|
+
// Like the 404 rule: the error boundary renders wherever a page fails, so
|
|
100
|
+
// this rule only serves the routable /500 page the editor previews.
|
|
101
|
+
[/^\/(500|server-error)(\/|$)/, PLACEHOLDER_SLUGS.serverError],
|
|
96
102
|
[/^\/?$/, PLACEHOLDER_SLUGS.homepage]
|
|
97
103
|
];
|
|
98
104
|
|
|
@@ -136,7 +142,13 @@ const PAGE_MOUNTED_SLUGS = new Set<string>([
|
|
|
136
142
|
PLACEHOLDER_SLUGS.register,
|
|
137
143
|
PLACEHOLDER_SLUGS.forgotPassword,
|
|
138
144
|
PLACEHOLDER_SLUGS.guestLogin,
|
|
139
|
-
PLACEHOLDER_SLUGS.flatPage
|
|
145
|
+
PLACEHOLDER_SLUGS.flatPage,
|
|
146
|
+
// Mounted by the not-found boundary itself, which renders on any unmatched
|
|
147
|
+
// path — a route-resolved catch-all would never fire there.
|
|
148
|
+
PLACEHOLDER_SLUGS.notFound,
|
|
149
|
+
// Same shape: the error boundary carries its own composition (prefetched
|
|
150
|
+
// through the layout provider), so no catch-all may mount this slug.
|
|
151
|
+
PLACEHOLDER_SLUGS.serverError
|
|
140
152
|
]);
|
|
141
153
|
|
|
142
154
|
/** True when a deeper page/layout already mounts this slug's slot. */
|
package/src/index.ts
CHANGED
|
@@ -55,9 +55,17 @@ export {
|
|
|
55
55
|
buildOrderPageContext,
|
|
56
56
|
buildCheckoutPageContext,
|
|
57
57
|
buildFlatPageContext,
|
|
58
|
+
buildNotFoundPageContext,
|
|
59
|
+
buildErrorPageContext,
|
|
58
60
|
configurePageContextRoutes
|
|
59
61
|
} from './chrome/page-context';
|
|
60
62
|
|
|
63
|
+
export {
|
|
64
|
+
ErrorCompositionProvider,
|
|
65
|
+
useErrorComposition
|
|
66
|
+
} from './chrome/error-composition';
|
|
67
|
+
export type { ErrorComposition } from './chrome/error-composition';
|
|
68
|
+
|
|
61
69
|
export {
|
|
62
70
|
PLACEHOLDER_SLUGS,
|
|
63
71
|
customPageSlug,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import * as serverData from '@akinon/next/data/server';
|
|
4
|
+
import appFetch from '@akinon/next/utils/app-fetch';
|
|
5
|
+
import { ServerVariables } from '@akinon/next/utils/server-variables';
|
|
6
|
+
|
|
7
|
+
import { Section } from './theme-section';
|
|
8
|
+
import { devWarn } from './utils/dev-warn';
|
|
9
|
+
import { validateThemeSectionsInDev } from './schemas/theme-schema';
|
|
10
|
+
import { getThemeConfig, ThemeConfig } from './chrome/theme-config-source';
|
|
11
|
+
|
|
12
|
+
const { getWidgetData } = serverData;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* v1 cores ship a smaller server data layer: only `getWidgetData` exists,
|
|
16
|
+
* `getWidgetSchemaData` and `getCollectionWidgetData` arrived with v2.
|
|
17
|
+
*
|
|
18
|
+
* The endpoints are identical on both backends — v1 just never wrapped them.
|
|
19
|
+
* Degrading to "no data" would render every published section *without its
|
|
20
|
+
* blocks* (a section's block tree lives in the widget schema, not in the
|
|
21
|
+
* widget), so on v1 we call the REST endpoints directly. This is what keeps
|
|
22
|
+
* composed pages rendering on the brands still running v1.
|
|
23
|
+
*/
|
|
24
|
+
type ServerDataFn = (<T>(args: never) => Promise<T | null>) | undefined;
|
|
25
|
+
|
|
26
|
+
const optionalServerFn = (name: string): ServerDataFn =>
|
|
27
|
+
(serverData as unknown as Record<string, ServerDataFn>)[name];
|
|
28
|
+
|
|
29
|
+
const directFetch = async <T,>(url: string): Promise<T | null> => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await appFetch<T>({
|
|
32
|
+
url,
|
|
33
|
+
locale: ServerVariables.locale,
|
|
34
|
+
currency: ServerVariables.currency
|
|
35
|
+
});
|
|
36
|
+
return result ?? null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getCollectionWidgetData = async <T,>(args: {
|
|
43
|
+
slug: string;
|
|
44
|
+
}): Promise<T | null> => {
|
|
45
|
+
const fn = optionalServerFn('getCollectionWidgetData');
|
|
46
|
+
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
47
|
+
if (!args?.slug) return null;
|
|
48
|
+
return directFetch<T>(`/product_collection_widgets/${args.slug}/`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getWidgetSchemaData = async <T,>(args: {
|
|
52
|
+
widgetSlug: string;
|
|
53
|
+
}): Promise<T | null> => {
|
|
54
|
+
const fn = optionalServerFn('getWidgetSchemaData');
|
|
55
|
+
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
56
|
+
if (!args?.widgetSlug) return null;
|
|
57
|
+
return directFetch<T>(`/widgets/${args.widgetSlug}/schema/`);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ThemePlaceholderData = {
|
|
61
|
+
theme_editor_placeholder: Array<{
|
|
62
|
+
section_slug: string;
|
|
63
|
+
order: number;
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface LoadedPlaceholder {
|
|
68
|
+
sections: Section[];
|
|
69
|
+
placeholderId: string;
|
|
70
|
+
dataSources: ThemeConfig['dataSources'];
|
|
71
|
+
themeSettings: ThemeConfig['settings'];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetches and reconstructs the composed section tree for a placeholder slug.
|
|
76
|
+
*
|
|
77
|
+
* Shared by `ThemePlaceholder` (render time) and the error-composition
|
|
78
|
+
* prefetch (layout time): the error boundary is a client component that can
|
|
79
|
+
* neither fetch nor receive server props, so its composition has to be
|
|
80
|
+
* reconstructed ahead of the failure — and by the same code path, or the two
|
|
81
|
+
* renders could disagree about what a saved section contains.
|
|
82
|
+
*
|
|
83
|
+
* Returns `null` only when the data layer fails outright; a slug nobody has
|
|
84
|
+
* composed resolves with an empty `sections` array.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadPlaceholderSections(
|
|
87
|
+
slug: string
|
|
88
|
+
): Promise<LoadedPlaceholder | null> {
|
|
89
|
+
try {
|
|
90
|
+
// theme-config is optional for placeholder rendering, and the reader is
|
|
91
|
+
// memoized per request — a page with several placeholders, or a ChromeSlot
|
|
92
|
+
// asking the same question to decide whether it is pinned, reads it once.
|
|
93
|
+
const { dataSources, settings: themeSettings } = await getThemeConfig();
|
|
94
|
+
|
|
95
|
+
let data: { slug?: string; attributes?: { theme_editor_placeholder?: unknown; [key: string]: unknown } } | null = null;
|
|
96
|
+
try {
|
|
97
|
+
data = await getWidgetData<ThemePlaceholderData>({
|
|
98
|
+
slug
|
|
99
|
+
}) as typeof data;
|
|
100
|
+
} catch (placeholderError) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`Theme placeholder widget "${slug}" was not found. Rendering empty placeholder.`,
|
|
103
|
+
placeholderError
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sections: Section[] = [];
|
|
108
|
+
|
|
109
|
+
if (data?.attributes?.theme_editor_placeholder) {
|
|
110
|
+
let placeholderData: any = data.attributes.theme_editor_placeholder;
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
typeof placeholderData === 'object' &&
|
|
114
|
+
placeholderData !== null &&
|
|
115
|
+
'value' in placeholderData
|
|
116
|
+
) {
|
|
117
|
+
placeholderData = (
|
|
118
|
+
placeholderData as {
|
|
119
|
+
value: { section_slug: string; order: number }[];
|
|
120
|
+
}
|
|
121
|
+
).value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof placeholderData === 'string') {
|
|
125
|
+
try {
|
|
126
|
+
placeholderData = JSON.parse(placeholderData);
|
|
127
|
+
} catch {
|
|
128
|
+
devWarn(
|
|
129
|
+
'ThemePlaceholder',
|
|
130
|
+
`Placeholder "${slug}" layout JSON is malformed; rendering no sections`
|
|
131
|
+
);
|
|
132
|
+
placeholderData = [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(placeholderData)) {
|
|
137
|
+
placeholderData = [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sectionPromises = placeholderData.map(async (item: any) => {
|
|
141
|
+
try {
|
|
142
|
+
const sectionSlug = item.value?.section_slug || item.section_slug;
|
|
143
|
+
const sectionOrder = item.value?.order ?? item.order ?? 0;
|
|
144
|
+
|
|
145
|
+
if (!sectionSlug) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [regularSectionData, sectionSchema] = await Promise.all([
|
|
150
|
+
getWidgetData<any>({
|
|
151
|
+
slug: sectionSlug
|
|
152
|
+
}),
|
|
153
|
+
getWidgetSchemaData<any>({
|
|
154
|
+
widgetSlug: sectionSlug
|
|
155
|
+
})
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
let sectionData = regularSectionData;
|
|
159
|
+
if (!sectionData) {
|
|
160
|
+
try {
|
|
161
|
+
sectionData = await getCollectionWidgetData<any>({
|
|
162
|
+
slug: sectionSlug
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// Not a collection widget either; sectionData stays null
|
|
166
|
+
devWarn(
|
|
167
|
+
'ThemePlaceholder',
|
|
168
|
+
`Section "${sectionSlug}" data could not be resolved as a regular or collection widget; rendering from schema only`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sectionSchemaEntry = sectionSchema?.schema?.[sectionSlug];
|
|
174
|
+
const sectionMetadata = sectionSchemaEntry?.metadata || {};
|
|
175
|
+
|
|
176
|
+
const getBlockValue = (blockId: string): unknown => {
|
|
177
|
+
const attrData = sectionData?.attributes?.[blockId];
|
|
178
|
+
if (attrData === undefined || attrData === null) return undefined;
|
|
179
|
+
|
|
180
|
+
if (typeof attrData === 'string') {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(attrData);
|
|
183
|
+
} catch {
|
|
184
|
+
const trimmed = attrData.trim();
|
|
185
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
186
|
+
devWarn(
|
|
187
|
+
'ThemePlaceholder',
|
|
188
|
+
`Block "${blockId}" value looks like JSON but failed to parse; using the raw string`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return attrData;
|
|
192
|
+
}
|
|
193
|
+
} else if (typeof attrData === 'object' && 'value' in attrData) {
|
|
194
|
+
return attrData.value;
|
|
195
|
+
}
|
|
196
|
+
return attrData;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const reconstructBlock = (blockSchema: any): any => {
|
|
200
|
+
return {
|
|
201
|
+
id: blockSchema.id,
|
|
202
|
+
type: blockSchema.type || 'text',
|
|
203
|
+
label: blockSchema.label || blockSchema.id,
|
|
204
|
+
order: blockSchema.order || 0,
|
|
205
|
+
isIterator: blockSchema.isIterator,
|
|
206
|
+
iteratorDataPath: blockSchema.iteratorDataPath,
|
|
207
|
+
value: getBlockValue(blockSchema.id),
|
|
208
|
+
styles: blockSchema.styles || {},
|
|
209
|
+
properties: blockSchema.properties || {},
|
|
210
|
+
hidden: blockSchema.hidden || false,
|
|
211
|
+
blocks: (blockSchema.blocks || []).map((b: any) =>
|
|
212
|
+
reconstructBlock(b)
|
|
213
|
+
)
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const blocks: any[] = [];
|
|
218
|
+
if (sectionMetadata.blocks && Array.isArray(sectionMetadata.blocks)) {
|
|
219
|
+
sectionMetadata.blocks.forEach((blockSchema: any) => {
|
|
220
|
+
blocks.push(reconstructBlock(blockSchema));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
blocks.sort((a, b) => a.order - b.order);
|
|
225
|
+
|
|
226
|
+
// dataSourceId may be stored in schema metadata OR in widget data attributes
|
|
227
|
+
// (theme editor saves it as a widget attribute when user selects a data source)
|
|
228
|
+
const dataSourceId =
|
|
229
|
+
sectionMetadata.dataSourceId ||
|
|
230
|
+
(getBlockValue('dataSourceId') as string | undefined) ||
|
|
231
|
+
(getBlockValue('selectedDataSourceId') as string | undefined);
|
|
232
|
+
|
|
233
|
+
const dataSource = dataSourceId
|
|
234
|
+
? dataSources.find((ds) => ds.id === dataSourceId)
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
if (dataSourceId && !dataSource) {
|
|
238
|
+
devWarn(
|
|
239
|
+
'ThemePlaceholder',
|
|
240
|
+
`Section "${sectionSlug}" references data source "${dataSourceId}" which was not found; rendering placeholder items`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let dataSourceWithData = dataSource;
|
|
245
|
+
if (dataSource && dataSource.details) {
|
|
246
|
+
// Only fetch collection data if we have a valid widget slug
|
|
247
|
+
// (not the section's own slug which contains config, not products)
|
|
248
|
+
const collectionSlug = dataSource.details.collection?.slug;
|
|
249
|
+
if (collectionSlug) {
|
|
250
|
+
try {
|
|
251
|
+
let collectionData = await getWidgetData<{
|
|
252
|
+
[key: string]: unknown;
|
|
253
|
+
}>({
|
|
254
|
+
slug: collectionSlug
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!collectionData) {
|
|
258
|
+
collectionData = await getCollectionWidgetData<{
|
|
259
|
+
[key: string]: unknown;
|
|
260
|
+
}>({
|
|
261
|
+
slug: collectionSlug
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (collectionData) {
|
|
266
|
+
dataSourceWithData = {
|
|
267
|
+
...dataSource,
|
|
268
|
+
details: {
|
|
269
|
+
...dataSource.details,
|
|
270
|
+
collection: {
|
|
271
|
+
...dataSource.details.collection,
|
|
272
|
+
data: collectionData
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(
|
|
279
|
+
`Error fetching widget data for section ${sectionSlug}:`,
|
|
280
|
+
error
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// If no collection slug, the saved products from editor
|
|
285
|
+
// (details.collection.products) will be used as fallback
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const section: Section = {
|
|
289
|
+
id: sectionSlug,
|
|
290
|
+
type: sectionMetadata.type || 'default',
|
|
291
|
+
name: sectionMetadata.name || sectionSlug,
|
|
292
|
+
label: sectionMetadata.label || sectionSlug,
|
|
293
|
+
properties: sectionMetadata.properties || {},
|
|
294
|
+
styles: sectionMetadata.styles || {},
|
|
295
|
+
blocks,
|
|
296
|
+
order: sectionOrder,
|
|
297
|
+
hidden: sectionMetadata.hidden || false,
|
|
298
|
+
dataSourceId,
|
|
299
|
+
dataSource: dataSourceWithData
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return section;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const errorSlug =
|
|
305
|
+
item.value?.section_slug || item.section_slug || 'unknown';
|
|
306
|
+
console.error(
|
|
307
|
+
`Error parsing section metadata for ${errorSlug}:`,
|
|
308
|
+
error
|
|
309
|
+
);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const resolvedSections = await Promise.all(sectionPromises);
|
|
315
|
+
sections.push(
|
|
316
|
+
...resolvedSections.filter((s): s is Section => s !== null)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Dev-only, warn-only shape validation of the reconstructed tree. Surfaces
|
|
321
|
+
// structural drift between the editor/backend data and the render engine
|
|
322
|
+
// without changing what renders (no-op in production).
|
|
323
|
+
validateThemeSectionsInDev(sections);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
sections,
|
|
327
|
+
placeholderId: data?.slug?.toString() || slug,
|
|
328
|
+
dataSources,
|
|
329
|
+
themeSettings
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`Error fetching theme placeholder data for ${slug}:`, error);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -1,69 +1,8 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
-
import * as serverData from '@akinon/next/data/server';
|
|
4
|
-
import appFetch from '@akinon/next/utils/app-fetch';
|
|
5
|
-
import { ServerVariables } from '@akinon/next/utils/server-variables';
|
|
6
|
-
|
|
7
|
-
const { getWidgetData } = serverData;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* v1 cores ship a smaller server data layer: only `getWidgetData` exists,
|
|
11
|
-
* `getWidgetSchemaData` and `getCollectionWidgetData` arrived with v2.
|
|
12
|
-
*
|
|
13
|
-
* The endpoints are identical on both backends — v1 just never wrapped them.
|
|
14
|
-
* Degrading to "no data" would render every published section *without its
|
|
15
|
-
* blocks* (a section's block tree lives in the widget schema, not in the
|
|
16
|
-
* widget), so on v1 we call the REST endpoints directly. This is what keeps
|
|
17
|
-
* composed pages rendering on the brands still running v1.
|
|
18
|
-
*/
|
|
19
|
-
type ServerDataFn = (<T>(args: never) => Promise<T | null>) | undefined;
|
|
20
|
-
|
|
21
|
-
const optionalServerFn = (name: string): ServerDataFn =>
|
|
22
|
-
(serverData as unknown as Record<string, ServerDataFn>)[name];
|
|
23
|
-
|
|
24
|
-
const directFetch = async <T,>(url: string): Promise<T | null> => {
|
|
25
|
-
try {
|
|
26
|
-
const result = await appFetch<T>({
|
|
27
|
-
url,
|
|
28
|
-
locale: ServerVariables.locale,
|
|
29
|
-
currency: ServerVariables.currency
|
|
30
|
-
});
|
|
31
|
-
return result ?? null;
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const getCollectionWidgetData = async <T,>(args: {
|
|
38
|
-
slug: string;
|
|
39
|
-
}): Promise<T | null> => {
|
|
40
|
-
const fn = optionalServerFn('getCollectionWidgetData');
|
|
41
|
-
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
42
|
-
if (!args?.slug) return null;
|
|
43
|
-
return directFetch<T>(`/product_collection_widgets/${args.slug}/`);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const getWidgetSchemaData = async <T,>(args: {
|
|
47
|
-
widgetSlug: string;
|
|
48
|
-
}): Promise<T | null> => {
|
|
49
|
-
const fn = optionalServerFn('getWidgetSchemaData');
|
|
50
|
-
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
51
|
-
if (!args?.widgetSlug) return null;
|
|
52
|
-
return directFetch<T>(`/widgets/${args.widgetSlug}/schema/`);
|
|
53
|
-
};
|
|
54
3
|
import ThemePlaceholderWrapper from './theme-placeholder-wrapper';
|
|
55
|
-
import {
|
|
4
|
+
import { loadPlaceholderSections } from './load-placeholder-sections';
|
|
56
5
|
import { generateThemeCSS } from './utils';
|
|
57
|
-
import { devWarn } from './utils/dev-warn';
|
|
58
|
-
import { validateThemeSectionsInDev } from './schemas/theme-schema';
|
|
59
|
-
import { getThemeConfig } from './chrome/theme-config-source';
|
|
60
|
-
|
|
61
|
-
type ThemePlaceholderData = {
|
|
62
|
-
theme_editor_placeholder: Array<{
|
|
63
|
-
section_slug: string;
|
|
64
|
-
order: number;
|
|
65
|
-
}>;
|
|
66
|
-
};
|
|
67
6
|
|
|
68
7
|
interface ThemePlaceholderProps {
|
|
69
8
|
slug: string;
|
|
@@ -74,260 +13,27 @@ export default async function ThemePlaceholder({
|
|
|
74
13
|
slug,
|
|
75
14
|
pageContext = null
|
|
76
15
|
}: ThemePlaceholderProps) {
|
|
77
|
-
|
|
78
|
-
// theme-config is optional for placeholder rendering, and the reader is
|
|
79
|
-
// memoized per request — a page with several placeholders, or a ChromeSlot
|
|
80
|
-
// asking the same question to decide whether it is pinned, reads it once.
|
|
81
|
-
const { dataSources, settings: themeSettings } = await getThemeConfig();
|
|
82
|
-
|
|
83
|
-
let data: { slug?: string; attributes?: { theme_editor_placeholder?: unknown; [key: string]: unknown } } | null = null;
|
|
84
|
-
try {
|
|
85
|
-
data = await getWidgetData<ThemePlaceholderData>({
|
|
86
|
-
slug
|
|
87
|
-
}) as typeof data;
|
|
88
|
-
} catch (placeholderError) {
|
|
89
|
-
console.warn(
|
|
90
|
-
`Theme placeholder widget "${slug}" was not found. Rendering empty placeholder.`,
|
|
91
|
-
placeholderError
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const sections: Section[] = [];
|
|
96
|
-
|
|
97
|
-
if (data?.attributes?.theme_editor_placeholder) {
|
|
98
|
-
let placeholderData: any = data.attributes.theme_editor_placeholder;
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
typeof placeholderData === 'object' &&
|
|
102
|
-
placeholderData !== null &&
|
|
103
|
-
'value' in placeholderData
|
|
104
|
-
) {
|
|
105
|
-
placeholderData = (
|
|
106
|
-
placeholderData as {
|
|
107
|
-
value: { section_slug: string; order: number }[];
|
|
108
|
-
}
|
|
109
|
-
).value;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof placeholderData === 'string') {
|
|
113
|
-
try {
|
|
114
|
-
placeholderData = JSON.parse(placeholderData);
|
|
115
|
-
} catch {
|
|
116
|
-
devWarn(
|
|
117
|
-
'ThemePlaceholder',
|
|
118
|
-
`Placeholder "${slug}" layout JSON is malformed; rendering no sections`
|
|
119
|
-
);
|
|
120
|
-
placeholderData = [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!Array.isArray(placeholderData)) {
|
|
125
|
-
placeholderData = [];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const sectionPromises = placeholderData.map(async (item: any) => {
|
|
129
|
-
try {
|
|
130
|
-
const sectionSlug = item.value?.section_slug || item.section_slug;
|
|
131
|
-
const sectionOrder = item.value?.order ?? item.order ?? 0;
|
|
132
|
-
|
|
133
|
-
if (!sectionSlug) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const [regularSectionData, sectionSchema] = await Promise.all([
|
|
138
|
-
getWidgetData<any>({
|
|
139
|
-
slug: sectionSlug
|
|
140
|
-
}),
|
|
141
|
-
getWidgetSchemaData<any>({
|
|
142
|
-
widgetSlug: sectionSlug
|
|
143
|
-
})
|
|
144
|
-
]);
|
|
16
|
+
const loaded = await loadPlaceholderSections(slug);
|
|
145
17
|
|
|
146
|
-
|
|
147
|
-
if (!sectionData) {
|
|
148
|
-
try {
|
|
149
|
-
sectionData = await getCollectionWidgetData<any>({
|
|
150
|
-
slug: sectionSlug
|
|
151
|
-
});
|
|
152
|
-
} catch {
|
|
153
|
-
// Not a collection widget either; sectionData stays null
|
|
154
|
-
devWarn(
|
|
155
|
-
'ThemePlaceholder',
|
|
156
|
-
`Section "${sectionSlug}" data could not be resolved as a regular or collection widget; rendering from schema only`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const sectionSchemaEntry = sectionSchema?.schema?.[sectionSlug];
|
|
162
|
-
const sectionMetadata = sectionSchemaEntry?.metadata || {};
|
|
163
|
-
|
|
164
|
-
const getBlockValue = (blockId: string): unknown => {
|
|
165
|
-
const attrData = sectionData?.attributes?.[blockId];
|
|
166
|
-
if (attrData === undefined || attrData === null) return undefined;
|
|
167
|
-
|
|
168
|
-
if (typeof attrData === 'string') {
|
|
169
|
-
try {
|
|
170
|
-
return JSON.parse(attrData);
|
|
171
|
-
} catch {
|
|
172
|
-
const trimmed = attrData.trim();
|
|
173
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
174
|
-
devWarn(
|
|
175
|
-
'ThemePlaceholder',
|
|
176
|
-
`Block "${blockId}" value looks like JSON but failed to parse; using the raw string`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return attrData;
|
|
180
|
-
}
|
|
181
|
-
} else if (typeof attrData === 'object' && 'value' in attrData) {
|
|
182
|
-
return attrData.value;
|
|
183
|
-
}
|
|
184
|
-
return attrData;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const reconstructBlock = (blockSchema: any): any => {
|
|
188
|
-
return {
|
|
189
|
-
id: blockSchema.id,
|
|
190
|
-
type: blockSchema.type || 'text',
|
|
191
|
-
label: blockSchema.label || blockSchema.id,
|
|
192
|
-
order: blockSchema.order || 0,
|
|
193
|
-
isIterator: blockSchema.isIterator,
|
|
194
|
-
iteratorDataPath: blockSchema.iteratorDataPath,
|
|
195
|
-
value: getBlockValue(blockSchema.id),
|
|
196
|
-
styles: blockSchema.styles || {},
|
|
197
|
-
properties: blockSchema.properties || {},
|
|
198
|
-
hidden: blockSchema.hidden || false,
|
|
199
|
-
blocks: (blockSchema.blocks || []).map((b: any) =>
|
|
200
|
-
reconstructBlock(b)
|
|
201
|
-
)
|
|
202
|
-
};
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const blocks: any[] = [];
|
|
206
|
-
if (sectionMetadata.blocks && Array.isArray(sectionMetadata.blocks)) {
|
|
207
|
-
sectionMetadata.blocks.forEach((blockSchema: any) => {
|
|
208
|
-
blocks.push(reconstructBlock(blockSchema));
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
blocks.sort((a, b) => a.order - b.order);
|
|
213
|
-
|
|
214
|
-
// dataSourceId may be stored in schema metadata OR in widget data attributes
|
|
215
|
-
// (theme editor saves it as a widget attribute when user selects a data source)
|
|
216
|
-
const dataSourceId =
|
|
217
|
-
sectionMetadata.dataSourceId ||
|
|
218
|
-
(getBlockValue('dataSourceId') as string | undefined) ||
|
|
219
|
-
(getBlockValue('selectedDataSourceId') as string | undefined);
|
|
220
|
-
|
|
221
|
-
const dataSource = dataSourceId
|
|
222
|
-
? dataSources.find((ds) => ds.id === dataSourceId)
|
|
223
|
-
: undefined;
|
|
224
|
-
|
|
225
|
-
if (dataSourceId && !dataSource) {
|
|
226
|
-
devWarn(
|
|
227
|
-
'ThemePlaceholder',
|
|
228
|
-
`Section "${sectionSlug}" references data source "${dataSourceId}" which was not found; rendering placeholder items`
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let dataSourceWithData = dataSource;
|
|
233
|
-
if (dataSource && dataSource.details) {
|
|
234
|
-
// Only fetch collection data if we have a valid widget slug
|
|
235
|
-
// (not the section's own slug which contains config, not products)
|
|
236
|
-
const collectionSlug = dataSource.details.collection?.slug;
|
|
237
|
-
if (collectionSlug) {
|
|
238
|
-
try {
|
|
239
|
-
let collectionData = await getWidgetData<{
|
|
240
|
-
[key: string]: unknown;
|
|
241
|
-
}>({
|
|
242
|
-
slug: collectionSlug
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (!collectionData) {
|
|
246
|
-
collectionData = await getCollectionWidgetData<{
|
|
247
|
-
[key: string]: unknown;
|
|
248
|
-
}>({
|
|
249
|
-
slug: collectionSlug
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (collectionData) {
|
|
254
|
-
dataSourceWithData = {
|
|
255
|
-
...dataSource,
|
|
256
|
-
details: {
|
|
257
|
-
...dataSource.details,
|
|
258
|
-
collection: {
|
|
259
|
-
...dataSource.details.collection,
|
|
260
|
-
data: collectionData
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
} catch (error) {
|
|
266
|
-
console.error(
|
|
267
|
-
`Error fetching widget data for section ${sectionSlug}:`,
|
|
268
|
-
error
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// If no collection slug, the saved products from editor
|
|
273
|
-
// (details.collection.products) will be used as fallback
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const section: Section = {
|
|
277
|
-
id: sectionSlug,
|
|
278
|
-
type: sectionMetadata.type || 'default',
|
|
279
|
-
name: sectionMetadata.name || sectionSlug,
|
|
280
|
-
label: sectionMetadata.label || sectionSlug,
|
|
281
|
-
properties: sectionMetadata.properties || {},
|
|
282
|
-
styles: sectionMetadata.styles || {},
|
|
283
|
-
blocks,
|
|
284
|
-
order: sectionOrder,
|
|
285
|
-
hidden: sectionMetadata.hidden || false,
|
|
286
|
-
dataSourceId,
|
|
287
|
-
dataSource: dataSourceWithData
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
return section;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
const errorSlug =
|
|
293
|
-
item.value?.section_slug || item.section_slug || 'unknown';
|
|
294
|
-
console.error(
|
|
295
|
-
`Error parsing section metadata for ${errorSlug}:`,
|
|
296
|
-
error
|
|
297
|
-
);
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const resolvedSections = await Promise.all(sectionPromises);
|
|
303
|
-
sections.push(
|
|
304
|
-
...resolvedSections.filter((s): s is Section => s !== null)
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Dev-only, warn-only shape validation of the reconstructed tree. Surfaces
|
|
309
|
-
// structural drift between the editor/backend data and the render engine
|
|
310
|
-
// without changing what renders (no-op in production).
|
|
311
|
-
validateThemeSectionsInDev(sections);
|
|
312
|
-
|
|
313
|
-
const themeCSS = generateThemeCSS(sections);
|
|
314
|
-
|
|
315
|
-
return (
|
|
316
|
-
<>
|
|
317
|
-
{themeCSS && <style dangerouslySetInnerHTML={{ __html: themeCSS }} />}
|
|
318
|
-
<ThemePlaceholderWrapper
|
|
319
|
-
slug={slug}
|
|
320
|
-
initialSections={sections}
|
|
321
|
-
initialPlaceholderId={data?.slug?.toString() || slug}
|
|
322
|
-
isDesignMode={false}
|
|
323
|
-
dataSources={dataSources}
|
|
324
|
-
initialThemeSettings={themeSettings}
|
|
325
|
-
initialPageContext={pageContext}
|
|
326
|
-
/>
|
|
327
|
-
</>
|
|
328
|
-
);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
console.error(`Error fetching theme placeholder data for ${slug}:`, error);
|
|
18
|
+
if (!loaded) {
|
|
331
19
|
return null;
|
|
332
20
|
}
|
|
21
|
+
|
|
22
|
+
const { sections, placeholderId, dataSources, themeSettings } = loaded;
|
|
23
|
+
const themeCSS = generateThemeCSS(sections);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{themeCSS && <style dangerouslySetInnerHTML={{ __html: themeCSS }} />}
|
|
28
|
+
<ThemePlaceholderWrapper
|
|
29
|
+
slug={slug}
|
|
30
|
+
initialSections={sections}
|
|
31
|
+
initialPlaceholderId={placeholderId}
|
|
32
|
+
isDesignMode={false}
|
|
33
|
+
dataSources={dataSources}
|
|
34
|
+
initialThemeSettings={themeSettings}
|
|
35
|
+
initialPageContext={pageContext}
|
|
36
|
+
/>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
333
39
|
}
|