personalize-connect-sdk 1.2.1 → 1.3.2
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/README.md +130 -27
- package/dist/index.d.mts +34 -5
- package/dist/index.d.ts +34 -5
- package/dist/index.js +232 -14
- package/dist/index.mjs +231 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Personalize Connect SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runtime SDK for [Personalize Connect](https://github.com/Sitecore-Hackathon/2026-Team-Solo) — a zero-code bridge between Sitecore XM Cloud components and Sitecore Personalize Full Stack Interactive Experiences.
|
|
4
|
+
|
|
5
|
+
The SDK reads configuration authored by the Marketplace app (stored in the content tree), calls Personalize for a decision via the Edge proxy, resolves the matching datasource from Experience Edge, and swaps component content — with zero per-component code. In Page Builder, components with personalization get a visual indicator.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -10,23 +12,21 @@ npm install personalize-connect-sdk
|
|
|
10
12
|
|
|
11
13
|
Peer dependency: `react` >= 18.
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## Quick Start (XM Cloud)
|
|
14
16
|
|
|
15
17
|
### 1. Wrap your app with `PersonalizeProvider`
|
|
16
18
|
|
|
19
|
+
In `_app.tsx` (Pages Router) or `layout.tsx` (App Router):
|
|
20
|
+
|
|
17
21
|
```tsx
|
|
18
22
|
import { PersonalizeProvider } from "personalize-connect-sdk";
|
|
19
23
|
|
|
20
|
-
export default function
|
|
24
|
+
export default function App({ children }) {
|
|
21
25
|
return (
|
|
22
26
|
<PersonalizeProvider
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Fetch datasource fields via Experience Edge GraphQL or Layout Service
|
|
27
|
-
const res = await fetch(`/api/datasource/${datasourceId}`);
|
|
28
|
-
return res.json();
|
|
29
|
-
}}
|
|
27
|
+
sitecoreEdgeContextId={process.env.SITECORE_EDGE_CONTEXT_ID}
|
|
28
|
+
siteName={process.env.SITECORE_SITE_NAME}
|
|
29
|
+
debug // remove in production
|
|
30
30
|
>
|
|
31
31
|
{children}
|
|
32
32
|
</PersonalizeProvider>
|
|
@@ -34,49 +34,152 @@ export default function RootLayout({ children }) {
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
That's it for provider setup. One prop (`sitecoreEdgeContextId`) drives everything:
|
|
38
|
+
|
|
39
|
+
- **Browser ID** — fetched from `edge-platform.sitecorecloud.io/v1/init`
|
|
40
|
+
- **Personalize calls** — routed through the Edge proxy (`/v1/personalize`)
|
|
41
|
+
- **Datasource resolution** — built-in via Edge proxy GraphQL
|
|
42
|
+
- **Config loading** — auto-discovered from the content tree via Edge
|
|
43
|
+
- **Editing detection** — auto-detected from JSS Sitecore context
|
|
44
|
+
|
|
37
45
|
### 2. Wrap components with `withPersonalizeConnect`
|
|
38
46
|
|
|
39
47
|
```tsx
|
|
40
48
|
import { withPersonalizeConnect } from "personalize-connect-sdk";
|
|
41
|
-
import MyComponent from "./MyComponent";
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
const PromoCard = ({ fields }) => (
|
|
51
|
+
<div>
|
|
52
|
+
<h2>{fields?.title?.value}</h2>
|
|
53
|
+
<p>{fields?.body?.value}</p>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export default withPersonalizeConnect(PromoCard);
|
|
45
58
|
```
|
|
46
59
|
|
|
47
|
-
The HOC
|
|
60
|
+
The HOC:
|
|
61
|
+
1. Looks up config from the content tree (loaded by the provider on mount)
|
|
62
|
+
2. Renders with the default datasource immediately
|
|
63
|
+
3. Calls Personalize asynchronously for a content decision
|
|
64
|
+
4. Resolves the matching datasource from Experience Edge
|
|
65
|
+
5. Re-renders with personalized `fields`
|
|
66
|
+
6. In Page Builder, shows a visual indicator (purple border + badge)
|
|
48
67
|
|
|
49
68
|
### 3. Or use the `usePersonalizeExperience` hook
|
|
50
69
|
|
|
51
70
|
```tsx
|
|
52
71
|
import { usePersonalizeExperience } from "personalize-connect-sdk";
|
|
53
72
|
|
|
54
|
-
function MyComponent({
|
|
55
|
-
const
|
|
73
|
+
function MyComponent({ rendering }) {
|
|
74
|
+
const config = /* get config from context or props */;
|
|
75
|
+
const { contentKey, resolvedFields, isLoading, error } = usePersonalizeExperience(config);
|
|
56
76
|
|
|
57
77
|
if (isLoading) return <Skeleton />;
|
|
58
|
-
return <div>{resolvedFields?.heading}</div>;
|
|
78
|
+
return <div>{resolvedFields?.heading?.value}</div>;
|
|
59
79
|
}
|
|
60
80
|
```
|
|
61
81
|
|
|
62
|
-
##
|
|
82
|
+
## Provider Props
|
|
83
|
+
|
|
84
|
+
### XM Cloud (recommended)
|
|
85
|
+
|
|
86
|
+
| Prop | Required | Description |
|
|
87
|
+
|------|----------|-------------|
|
|
88
|
+
| `sitecoreEdgeContextId` | Yes | Edge Context ID — drives all Edge proxy calls |
|
|
89
|
+
| `siteName` | Yes | XM Cloud site name |
|
|
90
|
+
| `sitecoreEdgeUrl` | No | Edge platform URL (defaults to `https://edge-platform.sitecorecloud.io`) |
|
|
91
|
+
|
|
92
|
+
### Legacy (direct credentials)
|
|
93
|
+
|
|
94
|
+
| Prop | Required | Description |
|
|
95
|
+
|------|----------|-------------|
|
|
96
|
+
| `clientKey` | Yes | Personalize API client key |
|
|
97
|
+
| `pointOfSale` | Yes | Point of sale identifier |
|
|
98
|
+
| `edgeUrl` | No | Experience Edge GraphQL endpoint |
|
|
99
|
+
| `apiKey` | No | Sitecore API key for Edge |
|
|
100
|
+
|
|
101
|
+
### Common
|
|
102
|
+
|
|
103
|
+
| Prop | Default | Description |
|
|
104
|
+
|------|---------|-------------|
|
|
105
|
+
| `channel` | `"WEB"` | Channel for Personalize calls |
|
|
106
|
+
| `language` | `"EN"` | Language code |
|
|
107
|
+
| `currencyCode` | `"USD"` | Currency code |
|
|
108
|
+
| `timeout` | `600` | Personalize call timeout (ms) |
|
|
109
|
+
| `debug` | `false` | Enable `[PersonalizeConnect]` console logging |
|
|
110
|
+
| `isEditing` | auto | Override Page Builder editing detection |
|
|
111
|
+
| `sitePath` | auto | Override site root path auto-discovery |
|
|
112
|
+
| `resolveDatasource` | built-in | Custom datasource resolver (overrides built-in Edge resolution) |
|
|
113
|
+
|
|
114
|
+
## How Config Loading Works
|
|
115
|
+
|
|
116
|
+
The Marketplace app stores configs in the content tree at:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
{sitePath}/Data/PersonalizeConnect/{pageItemId}/config-{renderingId}
|
|
120
|
+
```
|
|
63
121
|
|
|
64
|
-
|
|
122
|
+
On mount, the SDK:
|
|
123
|
+
1. Reads the page item ID from `__NEXT_DATA__` (JSS layout data)
|
|
124
|
+
2. Queries Edge for the page item's content tree path
|
|
125
|
+
3. Derives the site root path (first 4 path segments)
|
|
126
|
+
4. Fetches all config children for that page in one GraphQL query
|
|
127
|
+
5. Caches them in context, keyed by rendering instance ID
|
|
128
|
+
|
|
129
|
+
Each HOC looks up its config via `props.rendering.uid`. No config on the rendering means no personalization — the component renders normally.
|
|
130
|
+
|
|
131
|
+
## Config Shape
|
|
132
|
+
|
|
133
|
+
Authored by the Marketplace app, stored as JSON in the content tree:
|
|
65
134
|
|
|
66
135
|
```ts
|
|
67
136
|
interface PersonalizeConnectConfig {
|
|
68
|
-
friendlyId: string;
|
|
137
|
+
friendlyId: string; // Personalize Interactive Experience ID
|
|
69
138
|
contentMap: Record<string, string>; // contentKey -> datasource GUID
|
|
70
|
-
defaultKey: string;
|
|
139
|
+
defaultKey: string; // Fallback key
|
|
71
140
|
}
|
|
72
141
|
```
|
|
73
142
|
|
|
143
|
+
## Debug Logging
|
|
144
|
+
|
|
145
|
+
Pass `debug` to the provider to trace the full flow in the browser console:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
[PersonalizeConnect] Provider mounting { mode: 'Context ID', ... }
|
|
149
|
+
[PersonalizeConnect] BrowserId (edge): from cookie abc123...
|
|
150
|
+
[PersonalizeConnect] Config loader: Auto-discovered site path: /sitecore/content/company/company
|
|
151
|
+
[PersonalizeConnect] Config loader: loaded config for rendering xyz → experience homepage_promo
|
|
152
|
+
[PersonalizeConnect] [PromoCard] Config active: { friendlyId: 'homepage_promo', ... }
|
|
153
|
+
[PersonalizeConnect] callPersonalize [homepage_promo] → contentKey: returning-visitor
|
|
154
|
+
[PersonalizeConnect] [PromoCard] Fields resolved — swapping props.fields
|
|
155
|
+
```
|
|
156
|
+
|
|
74
157
|
## Exports
|
|
75
158
|
|
|
76
|
-
|
|
77
|
-
- `
|
|
159
|
+
**Provider & Context**
|
|
160
|
+
- `PersonalizeProvider` — Wrap your app
|
|
161
|
+
- `usePersonalizeContext` — Access context directly
|
|
162
|
+
|
|
163
|
+
**HOC & Hook**
|
|
164
|
+
- `withPersonalizeConnect` — Zero-code personalization HOC
|
|
78
165
|
- `usePersonalizeExperience` — Hook for manual control
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `
|
|
82
|
-
|
|
166
|
+
|
|
167
|
+
**Config**
|
|
168
|
+
- `loadPageConfigs` — Load configs from Edge (used internally, exported for advanced use)
|
|
169
|
+
|
|
170
|
+
**Edge Resolution**
|
|
171
|
+
- `createEdgeResolver` — Direct Edge GraphQL resolver (legacy)
|
|
172
|
+
- `createEdgeProxyResolver` — Edge proxy resolver (Context ID mode)
|
|
173
|
+
|
|
174
|
+
**Browser ID**
|
|
175
|
+
- `getBrowserId` — Legacy local cookie
|
|
176
|
+
- `getEdgeBrowserId` — Edge proxy init
|
|
177
|
+
|
|
178
|
+
**Editing**
|
|
179
|
+
- `isEditingMode` — Page Builder detection
|
|
180
|
+
|
|
181
|
+
**Debug**
|
|
182
|
+
- `setDebug`, `isDebugEnabled` — Control logging
|
|
183
|
+
|
|
184
|
+
**Types**
|
|
185
|
+
- `PersonalizeConnectConfig`, `PersonalizeConnectProviderProps`, `PersonalizeContextValue`, `ComponentFields`, `CallFlowsRequest`, etc.
|
package/dist/index.d.mts
CHANGED
|
@@ -55,6 +55,8 @@ interface PersonalizeConnectProviderProps {
|
|
|
55
55
|
edgeUrl?: string;
|
|
56
56
|
/** Sitecore API key for Experience Edge (legacy). Not needed with Context ID. */
|
|
57
57
|
apiKey?: string;
|
|
58
|
+
/** Override for the site root content tree path (e.g. "/sitecore/content/company/company"). Normally auto-discovered from the page item — only needed if auto-discovery fails. */
|
|
59
|
+
sitePath?: string;
|
|
58
60
|
channel?: string;
|
|
59
61
|
language?: string;
|
|
60
62
|
currencyCode?: string;
|
|
@@ -88,9 +90,13 @@ interface PersonalizeContextValue {
|
|
|
88
90
|
sitecoreEdgeContextId: string;
|
|
89
91
|
/** Site name (only set in Context ID mode) */
|
|
90
92
|
siteName: string;
|
|
93
|
+
/** Configs loaded from the content tree, keyed by normalized rendering instance ID */
|
|
94
|
+
configs: Map<string, PersonalizeConnectConfig>;
|
|
95
|
+
/** Whether configs have finished loading */
|
|
96
|
+
configsLoaded: boolean;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
declare function PersonalizeProvider({ children, sitecoreEdgeContextId, sitecoreEdgeUrl, siteName, clientKey, pointOfSale, edgeUrl, apiKey, channel, language, currencyCode, timeout, resolveDatasource, isEditing: isEditingProp, debug, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
|
|
99
|
+
declare function PersonalizeProvider({ children, sitecoreEdgeContextId, sitecoreEdgeUrl, siteName, sitePath, clientKey, pointOfSale, edgeUrl, apiKey, channel, language, currencyCode, timeout, resolveDatasource, isEditing: isEditingProp, debug, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
|
|
94
100
|
declare function usePersonalizeContext(): PersonalizeContextValue | null;
|
|
95
101
|
|
|
96
102
|
/**
|
|
@@ -134,9 +140,12 @@ declare function resolveContent(options: ResolveContentOptions): Promise<Resolve
|
|
|
134
140
|
type GetConfigFromProps<P> = (props: P) => PersonalizeConnectConfig | undefined;
|
|
135
141
|
/**
|
|
136
142
|
* HOC that wraps any JSS component.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
143
|
+
* Looks for config in this order:
|
|
144
|
+
* 1. props.rendering.personalizeConnect (inline on layout data)
|
|
145
|
+
* 2. context.configs map (loaded from content tree via Edge)
|
|
146
|
+
*
|
|
147
|
+
* If config is found, renders with defaultKey first, calls Personalize
|
|
148
|
+
* asynchronously, and re-renders with personalized content.
|
|
140
149
|
*
|
|
141
150
|
* In Page Builder, renders a visual indicator (border + badge) on
|
|
142
151
|
* components that have personalization configured.
|
|
@@ -155,6 +164,26 @@ interface UsePersonalizeExperienceResult {
|
|
|
155
164
|
*/
|
|
156
165
|
declare function usePersonalizeExperience(config: PersonalizeConnectConfig | undefined): UsePersonalizeExperienceResult;
|
|
157
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Loads PersonalizeConnect configs for all components on a page
|
|
169
|
+
* from the content tree via Experience Edge GraphQL.
|
|
170
|
+
*
|
|
171
|
+
* Auto-discovers the site path by querying the page item's content tree path,
|
|
172
|
+
* then fetches configs from: {sitePath}/Data/PersonalizeConnect/{pageId}/
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Auto-discover site path by querying the page item's content tree path,
|
|
177
|
+
* then fetch all PersonalizeConnect configs for the page.
|
|
178
|
+
*
|
|
179
|
+
* @param edgeUrl Edge GraphQL endpoint (proxy or direct)
|
|
180
|
+
* @param pageItemId Page item GUID (from __NEXT_DATA__)
|
|
181
|
+
* @param language Language code
|
|
182
|
+
* @param headers Optional auth headers (sc_apikey for legacy mode)
|
|
183
|
+
* @param sitePathOverride Skip auto-discovery and use this path directly
|
|
184
|
+
*/
|
|
185
|
+
declare function loadPageConfigs(edgeUrl: string, pageItemId: string, language: string, headers?: Record<string, string>, sitePathOverride?: string): Promise<Map<string, PersonalizeConnectConfig>>;
|
|
186
|
+
|
|
158
187
|
/**
|
|
159
188
|
* Built-in Experience Edge datasource resolver.
|
|
160
189
|
*
|
|
@@ -216,4 +245,4 @@ declare function getEdgeBrowserId(edgeUrl: string, contextId: string, siteName:
|
|
|
216
245
|
/** Reset the cached init promise (for testing). */
|
|
217
246
|
declare function resetEdgeInitCache(): void;
|
|
218
247
|
|
|
219
|
-
export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type EdgeInitResponse, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, createEdgeProxyResolver, createEdgeResolver, getBrowserId, getEdgeBrowserId, isDebugEnabled, isEditingMode, resetEdgeInitCache, resetEditingDetectionCache, resolveContent, setDebug, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
|
|
248
|
+
export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type EdgeInitResponse, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, createEdgeProxyResolver, createEdgeResolver, getBrowserId, getEdgeBrowserId, isDebugEnabled, isEditingMode, loadPageConfigs, resetEdgeInitCache, resetEditingDetectionCache, resolveContent, setDebug, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ interface PersonalizeConnectProviderProps {
|
|
|
55
55
|
edgeUrl?: string;
|
|
56
56
|
/** Sitecore API key for Experience Edge (legacy). Not needed with Context ID. */
|
|
57
57
|
apiKey?: string;
|
|
58
|
+
/** Override for the site root content tree path (e.g. "/sitecore/content/company/company"). Normally auto-discovered from the page item — only needed if auto-discovery fails. */
|
|
59
|
+
sitePath?: string;
|
|
58
60
|
channel?: string;
|
|
59
61
|
language?: string;
|
|
60
62
|
currencyCode?: string;
|
|
@@ -88,9 +90,13 @@ interface PersonalizeContextValue {
|
|
|
88
90
|
sitecoreEdgeContextId: string;
|
|
89
91
|
/** Site name (only set in Context ID mode) */
|
|
90
92
|
siteName: string;
|
|
93
|
+
/** Configs loaded from the content tree, keyed by normalized rendering instance ID */
|
|
94
|
+
configs: Map<string, PersonalizeConnectConfig>;
|
|
95
|
+
/** Whether configs have finished loading */
|
|
96
|
+
configsLoaded: boolean;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
declare function PersonalizeProvider({ children, sitecoreEdgeContextId, sitecoreEdgeUrl, siteName, clientKey, pointOfSale, edgeUrl, apiKey, channel, language, currencyCode, timeout, resolveDatasource, isEditing: isEditingProp, debug, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
|
|
99
|
+
declare function PersonalizeProvider({ children, sitecoreEdgeContextId, sitecoreEdgeUrl, siteName, sitePath, clientKey, pointOfSale, edgeUrl, apiKey, channel, language, currencyCode, timeout, resolveDatasource, isEditing: isEditingProp, debug, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
|
|
94
100
|
declare function usePersonalizeContext(): PersonalizeContextValue | null;
|
|
95
101
|
|
|
96
102
|
/**
|
|
@@ -134,9 +140,12 @@ declare function resolveContent(options: ResolveContentOptions): Promise<Resolve
|
|
|
134
140
|
type GetConfigFromProps<P> = (props: P) => PersonalizeConnectConfig | undefined;
|
|
135
141
|
/**
|
|
136
142
|
* HOC that wraps any JSS component.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
143
|
+
* Looks for config in this order:
|
|
144
|
+
* 1. props.rendering.personalizeConnect (inline on layout data)
|
|
145
|
+
* 2. context.configs map (loaded from content tree via Edge)
|
|
146
|
+
*
|
|
147
|
+
* If config is found, renders with defaultKey first, calls Personalize
|
|
148
|
+
* asynchronously, and re-renders with personalized content.
|
|
140
149
|
*
|
|
141
150
|
* In Page Builder, renders a visual indicator (border + badge) on
|
|
142
151
|
* components that have personalization configured.
|
|
@@ -155,6 +164,26 @@ interface UsePersonalizeExperienceResult {
|
|
|
155
164
|
*/
|
|
156
165
|
declare function usePersonalizeExperience(config: PersonalizeConnectConfig | undefined): UsePersonalizeExperienceResult;
|
|
157
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Loads PersonalizeConnect configs for all components on a page
|
|
169
|
+
* from the content tree via Experience Edge GraphQL.
|
|
170
|
+
*
|
|
171
|
+
* Auto-discovers the site path by querying the page item's content tree path,
|
|
172
|
+
* then fetches configs from: {sitePath}/Data/PersonalizeConnect/{pageId}/
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Auto-discover site path by querying the page item's content tree path,
|
|
177
|
+
* then fetch all PersonalizeConnect configs for the page.
|
|
178
|
+
*
|
|
179
|
+
* @param edgeUrl Edge GraphQL endpoint (proxy or direct)
|
|
180
|
+
* @param pageItemId Page item GUID (from __NEXT_DATA__)
|
|
181
|
+
* @param language Language code
|
|
182
|
+
* @param headers Optional auth headers (sc_apikey for legacy mode)
|
|
183
|
+
* @param sitePathOverride Skip auto-discovery and use this path directly
|
|
184
|
+
*/
|
|
185
|
+
declare function loadPageConfigs(edgeUrl: string, pageItemId: string, language: string, headers?: Record<string, string>, sitePathOverride?: string): Promise<Map<string, PersonalizeConnectConfig>>;
|
|
186
|
+
|
|
158
187
|
/**
|
|
159
188
|
* Built-in Experience Edge datasource resolver.
|
|
160
189
|
*
|
|
@@ -216,4 +245,4 @@ declare function getEdgeBrowserId(edgeUrl: string, contextId: string, siteName:
|
|
|
216
245
|
/** Reset the cached init promise (for testing). */
|
|
217
246
|
declare function resetEdgeInitCache(): void;
|
|
218
247
|
|
|
219
|
-
export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type EdgeInitResponse, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, createEdgeProxyResolver, createEdgeResolver, getBrowserId, getEdgeBrowserId, isDebugEnabled, isEditingMode, resetEdgeInitCache, resetEditingDetectionCache, resolveContent, setDebug, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
|
|
248
|
+
export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type EdgeInitResponse, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, createEdgeProxyResolver, createEdgeResolver, getBrowserId, getEdgeBrowserId, isDebugEnabled, isEditingMode, loadPageConfigs, resetEdgeInitCache, resetEditingDetectionCache, resolveContent, setDebug, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
getEdgeBrowserId: () => getEdgeBrowserId,
|
|
29
29
|
isDebugEnabled: () => isDebugEnabled,
|
|
30
30
|
isEditingMode: () => isEditingMode,
|
|
31
|
+
loadPageConfigs: () => loadPageConfigs,
|
|
31
32
|
resetEdgeInitCache: () => resetEdgeInitCache,
|
|
32
33
|
resetEditingDetectionCache: () => resetEditingDetectionCache,
|
|
33
34
|
resolveContent: () => resolveContent,
|
|
@@ -42,7 +43,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
42
43
|
var import_react = require("react");
|
|
43
44
|
|
|
44
45
|
// src/logger.ts
|
|
45
|
-
var PREFIX = "[
|
|
46
|
+
var PREFIX = "[PersonalizeConnectSDK]";
|
|
46
47
|
var enabled = false;
|
|
47
48
|
function setDebug(on) {
|
|
48
49
|
enabled = on;
|
|
@@ -261,6 +262,145 @@ function resetEditingDetectionCache() {
|
|
|
261
262
|
cachedResult = null;
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
// src/configLoader.ts
|
|
266
|
+
var PAGE_ITEM_PATH_QUERY = `
|
|
267
|
+
query GetPageItemPath($itemId: String!, $language: String!) {
|
|
268
|
+
item(path: $itemId, language: $language) {
|
|
269
|
+
path
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
var PAGE_CONFIGS_QUERY = `
|
|
274
|
+
query GetPagePersonalizeConfigs($path: String!, $language: String!) {
|
|
275
|
+
item(path: $path, language: $language) {
|
|
276
|
+
children(first: 50) {
|
|
277
|
+
results {
|
|
278
|
+
name
|
|
279
|
+
fields(ownFields: true) {
|
|
280
|
+
name
|
|
281
|
+
jsonValue
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
function normalizeGuid(id) {
|
|
289
|
+
return id.replace(/[{}]/g, "").toLowerCase();
|
|
290
|
+
}
|
|
291
|
+
function deriveSitePath(pageContentPath) {
|
|
292
|
+
const parts = pageContentPath.split("/").filter(Boolean);
|
|
293
|
+
if (parts.length >= 4) {
|
|
294
|
+
return "/" + parts.slice(0, 4).join("/");
|
|
295
|
+
}
|
|
296
|
+
return pageContentPath;
|
|
297
|
+
}
|
|
298
|
+
function extractFieldValue(fields, fieldName) {
|
|
299
|
+
if (!fields) return void 0;
|
|
300
|
+
const field = fields.find((f) => f.name === fieldName);
|
|
301
|
+
if (!field) return void 0;
|
|
302
|
+
const jv = field.jsonValue;
|
|
303
|
+
if (typeof jv === "string") return jv;
|
|
304
|
+
if (jv && typeof jv === "object" && "value" in jv) return String(jv.value);
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
function parseConfigJson(json, renderingId) {
|
|
308
|
+
try {
|
|
309
|
+
const raw = JSON.parse(json);
|
|
310
|
+
const contentMap = raw.contentMap ?? raw.variantMap;
|
|
311
|
+
const friendlyId = raw.friendlyId ?? raw.experienceFriendlyId;
|
|
312
|
+
if (!contentMap || typeof contentMap !== "object" || !friendlyId) return null;
|
|
313
|
+
const keys = Object.keys(contentMap);
|
|
314
|
+
return {
|
|
315
|
+
friendlyId,
|
|
316
|
+
contentMap,
|
|
317
|
+
defaultKey: raw.defaultKey ?? keys[0] ?? ""
|
|
318
|
+
};
|
|
319
|
+
} catch {
|
|
320
|
+
warn("Failed to parse config JSON for rendering", renderingId);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function queryEdge2(edgeUrl, headers, query, variables) {
|
|
325
|
+
const res = await fetch(edgeUrl, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
328
|
+
body: JSON.stringify({ query, variables })
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
const text = await res.text().catch(() => "");
|
|
332
|
+
warn("Edge query non-OK:", res.status, text);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return res.json();
|
|
336
|
+
}
|
|
337
|
+
async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, sitePathOverride) {
|
|
338
|
+
const configs = /* @__PURE__ */ new Map();
|
|
339
|
+
const normalizedPageId = normalizeGuid(pageItemId);
|
|
340
|
+
group("Config loader");
|
|
341
|
+
let sitePath = sitePathOverride;
|
|
342
|
+
if (!sitePath) {
|
|
343
|
+
log("Auto-discovering site path from page item:", pageItemId);
|
|
344
|
+
const pathResponse = await queryEdge2(
|
|
345
|
+
edgeUrl,
|
|
346
|
+
headers,
|
|
347
|
+
PAGE_ITEM_PATH_QUERY,
|
|
348
|
+
{ itemId: pageItemId, language }
|
|
349
|
+
);
|
|
350
|
+
const pageContentPath = pathResponse?.data?.item?.path;
|
|
351
|
+
if (!pageContentPath) {
|
|
352
|
+
warn("Config loader: could not resolve page item path from Edge \u2014 page item may not be published");
|
|
353
|
+
log("Query was for itemId:", pageItemId);
|
|
354
|
+
groupEnd();
|
|
355
|
+
return configs;
|
|
356
|
+
}
|
|
357
|
+
sitePath = deriveSitePath(pageContentPath);
|
|
358
|
+
log("Auto-discovered site path:", sitePath, "(from page path:", pageContentPath + ")");
|
|
359
|
+
} else {
|
|
360
|
+
log("Using provided sitePath override:", sitePath);
|
|
361
|
+
}
|
|
362
|
+
const configFolderPath = `${sitePath}/Data/PersonalizeConnect/${normalizedPageId}`;
|
|
363
|
+
log("Fetching configs from:", configFolderPath);
|
|
364
|
+
try {
|
|
365
|
+
const json = await queryEdge2(
|
|
366
|
+
edgeUrl,
|
|
367
|
+
headers,
|
|
368
|
+
PAGE_CONFIGS_QUERY,
|
|
369
|
+
{ path: configFolderPath, language }
|
|
370
|
+
);
|
|
371
|
+
if (!json) {
|
|
372
|
+
groupEnd();
|
|
373
|
+
return configs;
|
|
374
|
+
}
|
|
375
|
+
log("Config loader raw response:", json);
|
|
376
|
+
if (json.errors?.length) {
|
|
377
|
+
warn("Config loader GraphQL errors:", json.errors.map((e) => e.message ?? String(e)).join("; "));
|
|
378
|
+
}
|
|
379
|
+
const children = json.data?.item?.children?.results ?? [];
|
|
380
|
+
log("Config loader: found", children.length, "config items");
|
|
381
|
+
for (const child of children) {
|
|
382
|
+
const configJson = extractFieldValue(child.fields, "Config") ?? extractFieldValue(child.fields, "Value");
|
|
383
|
+
const renderingId = extractFieldValue(child.fields, "RenderingId") ?? child.name.replace(/^config-/, "");
|
|
384
|
+
if (!configJson) {
|
|
385
|
+
warn("Config loader: skipping child", child.name, "\u2014 no Config field");
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const normalizedRid = normalizeGuid(renderingId);
|
|
389
|
+
const parsed = parseConfigJson(configJson, normalizedRid);
|
|
390
|
+
if (parsed) {
|
|
391
|
+
configs.set(normalizedRid, parsed);
|
|
392
|
+
log("Config loader: loaded config for rendering", normalizedRid, "\u2192 experience", parsed.friendlyId);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
log("Config loader: total configs loaded:", configs.size);
|
|
396
|
+
} catch (e) {
|
|
397
|
+
error("Config loader fetch error:", e);
|
|
398
|
+
}
|
|
399
|
+
groupEnd();
|
|
400
|
+
log("Config loader: result", Object.fromEntries(configs));
|
|
401
|
+
return configs;
|
|
402
|
+
}
|
|
403
|
+
|
|
264
404
|
// src/PersonalizeProvider.tsx
|
|
265
405
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
266
406
|
var PersonalizeContext = (0, import_react.createContext)(null);
|
|
@@ -270,11 +410,22 @@ var DEFAULT_CURRENCY = "USD";
|
|
|
270
410
|
var DEFAULT_TIMEOUT = 600;
|
|
271
411
|
var DEFAULT_EDGE_URL = "https://edge-platform.sitecorecloud.io";
|
|
272
412
|
var noopResolver = async () => ({});
|
|
413
|
+
var EMPTY_CONFIGS = /* @__PURE__ */ new Map();
|
|
414
|
+
function getPageItemIdFromNextData() {
|
|
415
|
+
if (typeof window === "undefined") return null;
|
|
416
|
+
try {
|
|
417
|
+
const nd = window.__NEXT_DATA__;
|
|
418
|
+
return nd?.props?.pageProps?.layoutData?.sitecore?.route?.itemId ?? null;
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
273
423
|
function PersonalizeProvider({
|
|
274
424
|
children,
|
|
275
425
|
sitecoreEdgeContextId,
|
|
276
426
|
sitecoreEdgeUrl = DEFAULT_EDGE_URL,
|
|
277
427
|
siteName = "",
|
|
428
|
+
sitePath,
|
|
278
429
|
clientKey = "",
|
|
279
430
|
pointOfSale = "",
|
|
280
431
|
edgeUrl,
|
|
@@ -290,6 +441,8 @@ function PersonalizeProvider({
|
|
|
290
441
|
const useEdgeProxy = Boolean(sitecoreEdgeContextId);
|
|
291
442
|
const [browserId, setBrowserId] = (0, import_react.useState)("");
|
|
292
443
|
const [detectedEditing, setDetectedEditing] = (0, import_react.useState)(false);
|
|
444
|
+
const [configs, setConfigs] = (0, import_react.useState)(EMPTY_CONFIGS);
|
|
445
|
+
const [configsLoaded, setConfigsLoaded] = (0, import_react.useState)(false);
|
|
293
446
|
(0, import_react.useEffect)(() => {
|
|
294
447
|
setDebug(debug);
|
|
295
448
|
}, [debug]);
|
|
@@ -300,6 +453,7 @@ function PersonalizeProvider({
|
|
|
300
453
|
sitecoreEdgeContextId: sitecoreEdgeContextId ?? "(none)",
|
|
301
454
|
sitecoreEdgeUrl,
|
|
302
455
|
siteName: siteName || "(none)",
|
|
456
|
+
sitePath: sitePath ?? "(none \u2014 configs will not be loaded from Edge)",
|
|
303
457
|
clientKey: clientKey ? `${clientKey.slice(0, 8)}...` : "(none)",
|
|
304
458
|
pointOfSale: pointOfSale || "(none)",
|
|
305
459
|
edgeUrl: edgeUrl ?? "(none)",
|
|
@@ -336,6 +490,36 @@ function PersonalizeProvider({
|
|
|
336
490
|
log("Editing mode overridden via prop:", isEditingProp);
|
|
337
491
|
}
|
|
338
492
|
}, [isEditingProp]);
|
|
493
|
+
(0, import_react.useEffect)(() => {
|
|
494
|
+
const pageItemId = getPageItemIdFromNextData();
|
|
495
|
+
if (!pageItemId) {
|
|
496
|
+
warn("Config loader: could not read page item ID from __NEXT_DATA__.sitecore.route.itemId \u2014 cannot load configs");
|
|
497
|
+
setConfigsLoaded(true);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
let graphqlUrl;
|
|
501
|
+
let headers = {};
|
|
502
|
+
if (useEdgeProxy) {
|
|
503
|
+
const base = sitecoreEdgeUrl.replace(/\/$/, "");
|
|
504
|
+
graphqlUrl = `${base}/v1/content/api/graphql/v1?sitecoreContextId=${encodeURIComponent(sitecoreEdgeContextId)}`;
|
|
505
|
+
} else if (edgeUrl && apiKey) {
|
|
506
|
+
graphqlUrl = edgeUrl;
|
|
507
|
+
headers = { sc_apikey: apiKey };
|
|
508
|
+
} else {
|
|
509
|
+
warn("Config loader: no Edge endpoint available \u2014 cannot load configs");
|
|
510
|
+
setConfigsLoaded(true);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
log("Config loader: starting", { pageItemId, sitePathOverride: sitePath ?? "(auto-discover)" });
|
|
514
|
+
loadPageConfigs(graphqlUrl, pageItemId, language, headers, sitePath).then((loaded) => {
|
|
515
|
+
log("Config loader: complete,", loaded.size, "configs loaded");
|
|
516
|
+
setConfigs(loaded);
|
|
517
|
+
setConfigsLoaded(true);
|
|
518
|
+
}).catch((err) => {
|
|
519
|
+
warn("Config loader: failed", err);
|
|
520
|
+
setConfigsLoaded(true);
|
|
521
|
+
});
|
|
522
|
+
}, [sitePath, useEdgeProxy, sitecoreEdgeContextId, sitecoreEdgeUrl, edgeUrl, apiKey, language]);
|
|
339
523
|
const effectiveEditing = isEditingProp ?? detectedEditing;
|
|
340
524
|
const effectiveResolver = (0, import_react.useCallback)(() => {
|
|
341
525
|
if (resolveDatasource) {
|
|
@@ -350,7 +534,7 @@ function PersonalizeProvider({
|
|
|
350
534
|
log("Resolver: using direct Edge GraphQL", { edgeUrl });
|
|
351
535
|
return createEdgeResolver(edgeUrl, apiKey, language);
|
|
352
536
|
}
|
|
353
|
-
warn("Resolver: no resolver configured \u2014 resolveDatasource will return {}.
|
|
537
|
+
warn("Resolver: no resolver configured \u2014 resolveDatasource will return {}.");
|
|
354
538
|
return noopResolver;
|
|
355
539
|
}, [resolveDatasource, useEdgeProxy, sitecoreEdgeUrl, sitecoreEdgeContextId, edgeUrl, apiKey, language])();
|
|
356
540
|
const effectiveBrowserId = browserId || (!useEdgeProxy && clientKey && typeof window !== "undefined" ? getBrowserId(clientKey) : "");
|
|
@@ -368,7 +552,9 @@ function PersonalizeProvider({
|
|
|
368
552
|
useEdgeProxy,
|
|
369
553
|
edgeProxyUrl: useEdgeProxy ? sitecoreEdgeUrl : "",
|
|
370
554
|
sitecoreEdgeContextId: sitecoreEdgeContextId ?? "",
|
|
371
|
-
siteName
|
|
555
|
+
siteName,
|
|
556
|
+
configs,
|
|
557
|
+
configsLoaded
|
|
372
558
|
}),
|
|
373
559
|
[
|
|
374
560
|
clientKey,
|
|
@@ -383,7 +569,9 @@ function PersonalizeProvider({
|
|
|
383
569
|
useEdgeProxy,
|
|
384
570
|
sitecoreEdgeUrl,
|
|
385
571
|
sitecoreEdgeContextId,
|
|
386
|
-
siteName
|
|
572
|
+
siteName,
|
|
573
|
+
configs,
|
|
574
|
+
configsLoaded
|
|
387
575
|
]
|
|
388
576
|
);
|
|
389
577
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PersonalizeContext.Provider, { value, children });
|
|
@@ -541,6 +729,13 @@ async function resolveContent(options) {
|
|
|
541
729
|
var import_react2 = require("react");
|
|
542
730
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
543
731
|
var DEFAULT_GET_CONFIG = (props) => props.rendering?.personalizeConnect;
|
|
732
|
+
function normalizeGuid2(id) {
|
|
733
|
+
return id.replace(/[{}]/g, "").toLowerCase();
|
|
734
|
+
}
|
|
735
|
+
function getRenderingUid(props) {
|
|
736
|
+
const rendering = props.rendering;
|
|
737
|
+
return rendering?.uid;
|
|
738
|
+
}
|
|
544
739
|
var INDICATOR_BORDER = {
|
|
545
740
|
position: "relative",
|
|
546
741
|
border: "2px dashed #6B5CE7",
|
|
@@ -568,17 +763,36 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
568
763
|
const componentName = WrappedComponent.displayName ?? WrappedComponent.name ?? "Component";
|
|
569
764
|
function PersonalizeConnectWrapper(props) {
|
|
570
765
|
const context = usePersonalizeContext();
|
|
571
|
-
const config = getConfig(props);
|
|
572
766
|
const [resolvedFields, setResolvedFields] = (0, import_react2.useState)(null);
|
|
573
767
|
const mountedRef = (0, import_react2.useRef)(true);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
768
|
+
let config = getConfig(props);
|
|
769
|
+
if (!config && context) {
|
|
770
|
+
const uid = getRenderingUid(props);
|
|
771
|
+
if (uid) {
|
|
772
|
+
const normalizedUid = normalizeGuid2(uid);
|
|
773
|
+
const fromContext = context.configs.get(normalizedUid);
|
|
774
|
+
if (fromContext) {
|
|
775
|
+
log(`[${componentName}] Config found in context for rendering uid ${normalizedUid}:`, {
|
|
776
|
+
friendlyId: fromContext.friendlyId,
|
|
777
|
+
defaultKey: fromContext.defaultKey,
|
|
778
|
+
keys: Object.keys(fromContext.contentMap)
|
|
779
|
+
});
|
|
780
|
+
config = fromContext;
|
|
781
|
+
} else if (context.configsLoaded) {
|
|
782
|
+
log(`[${componentName}] No config match for uid "${normalizedUid}". All configs:`, Object.fromEntries(context.configs));
|
|
783
|
+
} else {
|
|
784
|
+
log(`[${componentName}] Configs still loading for uid ${normalizedUid}...`);
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
log(`[${componentName}] No rendering uid on props \u2014 cannot look up config from context`);
|
|
788
|
+
}
|
|
578
789
|
}
|
|
579
|
-
if (!context) {
|
|
790
|
+
if (!config && !context) {
|
|
580
791
|
warn(`[${componentName}] PersonalizeContext is null \u2014 is PersonalizeProvider mounted?`);
|
|
581
792
|
}
|
|
793
|
+
if (config) {
|
|
794
|
+
log(`[${componentName}] Config active:`, { friendlyId: config.friendlyId, defaultKey: config.defaultKey, keys: Object.keys(config.contentMap) });
|
|
795
|
+
}
|
|
582
796
|
const runPersonalization = (0, import_react2.useCallback)(async () => {
|
|
583
797
|
if (!config || !context) return;
|
|
584
798
|
group(`[${componentName}] personalization flow`);
|
|
@@ -599,7 +813,10 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
599
813
|
return;
|
|
600
814
|
}
|
|
601
815
|
if (resolved) {
|
|
602
|
-
log(`[${componentName}] Fields resolved \u2014 swapping props.fields`, {
|
|
816
|
+
log(`[${componentName}] Fields resolved \u2014 swapping props.fields`, {
|
|
817
|
+
datasourceId: resolved.datasourceId,
|
|
818
|
+
fieldNames: Object.keys(resolved.fields)
|
|
819
|
+
});
|
|
603
820
|
setResolvedFields(resolved.fields);
|
|
604
821
|
} else {
|
|
605
822
|
warn(`[${componentName}] Content resolution returned null \u2014 component keeps original fields`);
|
|
@@ -608,20 +825,20 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
608
825
|
}, [config, context]);
|
|
609
826
|
(0, import_react2.useEffect)(() => {
|
|
610
827
|
mountedRef.current = true;
|
|
611
|
-
if (config && context) {
|
|
828
|
+
if (config && context && context.browserId) {
|
|
612
829
|
runPersonalization();
|
|
613
830
|
}
|
|
614
831
|
return () => {
|
|
615
832
|
mountedRef.current = false;
|
|
616
833
|
};
|
|
617
|
-
}, [config?.friendlyId, context?.
|
|
834
|
+
}, [config?.friendlyId, context?.browserId, context?.configsLoaded, runPersonalization]);
|
|
618
835
|
if (!config || !context) {
|
|
619
836
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...props });
|
|
620
837
|
}
|
|
621
838
|
const mergedProps = resolvedFields ? { ...props, fields: resolvedFields } : props;
|
|
622
839
|
const component = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...mergedProps });
|
|
623
840
|
if (context.isEditing) {
|
|
624
|
-
log(`[${componentName}] Editing mode \u2014 rendering indicator badge`);
|
|
841
|
+
log(`[${componentName}] Editing mode \u2014 rendering indicator badge for ${config.friendlyId}`);
|
|
625
842
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: INDICATOR_BORDER, "data-personalize-connect": config.friendlyId, children: [
|
|
626
843
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: INDICATOR_BADGE, title: `Personalize: ${config.friendlyId}`, children: "\u26A1 Personalized" }),
|
|
627
844
|
component
|
|
@@ -694,6 +911,7 @@ function usePersonalizeExperience(config) {
|
|
|
694
911
|
getEdgeBrowserId,
|
|
695
912
|
isDebugEnabled,
|
|
696
913
|
isEditingMode,
|
|
914
|
+
loadPageConfigs,
|
|
697
915
|
resetEdgeInitCache,
|
|
698
916
|
resetEditingDetectionCache,
|
|
699
917
|
resolveContent,
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
3
|
|
|
4
4
|
// src/logger.ts
|
|
5
|
-
var PREFIX = "[
|
|
5
|
+
var PREFIX = "[PersonalizeConnectSDK]";
|
|
6
6
|
var enabled = false;
|
|
7
7
|
function setDebug(on) {
|
|
8
8
|
enabled = on;
|
|
@@ -221,6 +221,145 @@ function resetEditingDetectionCache() {
|
|
|
221
221
|
cachedResult = null;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// src/configLoader.ts
|
|
225
|
+
var PAGE_ITEM_PATH_QUERY = `
|
|
226
|
+
query GetPageItemPath($itemId: String!, $language: String!) {
|
|
227
|
+
item(path: $itemId, language: $language) {
|
|
228
|
+
path
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
var PAGE_CONFIGS_QUERY = `
|
|
233
|
+
query GetPagePersonalizeConfigs($path: String!, $language: String!) {
|
|
234
|
+
item(path: $path, language: $language) {
|
|
235
|
+
children(first: 50) {
|
|
236
|
+
results {
|
|
237
|
+
name
|
|
238
|
+
fields(ownFields: true) {
|
|
239
|
+
name
|
|
240
|
+
jsonValue
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
function normalizeGuid(id) {
|
|
248
|
+
return id.replace(/[{}]/g, "").toLowerCase();
|
|
249
|
+
}
|
|
250
|
+
function deriveSitePath(pageContentPath) {
|
|
251
|
+
const parts = pageContentPath.split("/").filter(Boolean);
|
|
252
|
+
if (parts.length >= 4) {
|
|
253
|
+
return "/" + parts.slice(0, 4).join("/");
|
|
254
|
+
}
|
|
255
|
+
return pageContentPath;
|
|
256
|
+
}
|
|
257
|
+
function extractFieldValue(fields, fieldName) {
|
|
258
|
+
if (!fields) return void 0;
|
|
259
|
+
const field = fields.find((f) => f.name === fieldName);
|
|
260
|
+
if (!field) return void 0;
|
|
261
|
+
const jv = field.jsonValue;
|
|
262
|
+
if (typeof jv === "string") return jv;
|
|
263
|
+
if (jv && typeof jv === "object" && "value" in jv) return String(jv.value);
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
function parseConfigJson(json, renderingId) {
|
|
267
|
+
try {
|
|
268
|
+
const raw = JSON.parse(json);
|
|
269
|
+
const contentMap = raw.contentMap ?? raw.variantMap;
|
|
270
|
+
const friendlyId = raw.friendlyId ?? raw.experienceFriendlyId;
|
|
271
|
+
if (!contentMap || typeof contentMap !== "object" || !friendlyId) return null;
|
|
272
|
+
const keys = Object.keys(contentMap);
|
|
273
|
+
return {
|
|
274
|
+
friendlyId,
|
|
275
|
+
contentMap,
|
|
276
|
+
defaultKey: raw.defaultKey ?? keys[0] ?? ""
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
warn("Failed to parse config JSON for rendering", renderingId);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function queryEdge2(edgeUrl, headers, query, variables) {
|
|
284
|
+
const res = await fetch(edgeUrl, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
287
|
+
body: JSON.stringify({ query, variables })
|
|
288
|
+
});
|
|
289
|
+
if (!res.ok) {
|
|
290
|
+
const text = await res.text().catch(() => "");
|
|
291
|
+
warn("Edge query non-OK:", res.status, text);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return res.json();
|
|
295
|
+
}
|
|
296
|
+
async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, sitePathOverride) {
|
|
297
|
+
const configs = /* @__PURE__ */ new Map();
|
|
298
|
+
const normalizedPageId = normalizeGuid(pageItemId);
|
|
299
|
+
group("Config loader");
|
|
300
|
+
let sitePath = sitePathOverride;
|
|
301
|
+
if (!sitePath) {
|
|
302
|
+
log("Auto-discovering site path from page item:", pageItemId);
|
|
303
|
+
const pathResponse = await queryEdge2(
|
|
304
|
+
edgeUrl,
|
|
305
|
+
headers,
|
|
306
|
+
PAGE_ITEM_PATH_QUERY,
|
|
307
|
+
{ itemId: pageItemId, language }
|
|
308
|
+
);
|
|
309
|
+
const pageContentPath = pathResponse?.data?.item?.path;
|
|
310
|
+
if (!pageContentPath) {
|
|
311
|
+
warn("Config loader: could not resolve page item path from Edge \u2014 page item may not be published");
|
|
312
|
+
log("Query was for itemId:", pageItemId);
|
|
313
|
+
groupEnd();
|
|
314
|
+
return configs;
|
|
315
|
+
}
|
|
316
|
+
sitePath = deriveSitePath(pageContentPath);
|
|
317
|
+
log("Auto-discovered site path:", sitePath, "(from page path:", pageContentPath + ")");
|
|
318
|
+
} else {
|
|
319
|
+
log("Using provided sitePath override:", sitePath);
|
|
320
|
+
}
|
|
321
|
+
const configFolderPath = `${sitePath}/Data/PersonalizeConnect/${normalizedPageId}`;
|
|
322
|
+
log("Fetching configs from:", configFolderPath);
|
|
323
|
+
try {
|
|
324
|
+
const json = await queryEdge2(
|
|
325
|
+
edgeUrl,
|
|
326
|
+
headers,
|
|
327
|
+
PAGE_CONFIGS_QUERY,
|
|
328
|
+
{ path: configFolderPath, language }
|
|
329
|
+
);
|
|
330
|
+
if (!json) {
|
|
331
|
+
groupEnd();
|
|
332
|
+
return configs;
|
|
333
|
+
}
|
|
334
|
+
log("Config loader raw response:", json);
|
|
335
|
+
if (json.errors?.length) {
|
|
336
|
+
warn("Config loader GraphQL errors:", json.errors.map((e) => e.message ?? String(e)).join("; "));
|
|
337
|
+
}
|
|
338
|
+
const children = json.data?.item?.children?.results ?? [];
|
|
339
|
+
log("Config loader: found", children.length, "config items");
|
|
340
|
+
for (const child of children) {
|
|
341
|
+
const configJson = extractFieldValue(child.fields, "Config") ?? extractFieldValue(child.fields, "Value");
|
|
342
|
+
const renderingId = extractFieldValue(child.fields, "RenderingId") ?? child.name.replace(/^config-/, "");
|
|
343
|
+
if (!configJson) {
|
|
344
|
+
warn("Config loader: skipping child", child.name, "\u2014 no Config field");
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const normalizedRid = normalizeGuid(renderingId);
|
|
348
|
+
const parsed = parseConfigJson(configJson, normalizedRid);
|
|
349
|
+
if (parsed) {
|
|
350
|
+
configs.set(normalizedRid, parsed);
|
|
351
|
+
log("Config loader: loaded config for rendering", normalizedRid, "\u2192 experience", parsed.friendlyId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
log("Config loader: total configs loaded:", configs.size);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
error("Config loader fetch error:", e);
|
|
357
|
+
}
|
|
358
|
+
groupEnd();
|
|
359
|
+
log("Config loader: result", Object.fromEntries(configs));
|
|
360
|
+
return configs;
|
|
361
|
+
}
|
|
362
|
+
|
|
224
363
|
// src/PersonalizeProvider.tsx
|
|
225
364
|
import { jsx } from "react/jsx-runtime";
|
|
226
365
|
var PersonalizeContext = createContext(null);
|
|
@@ -230,11 +369,22 @@ var DEFAULT_CURRENCY = "USD";
|
|
|
230
369
|
var DEFAULT_TIMEOUT = 600;
|
|
231
370
|
var DEFAULT_EDGE_URL = "https://edge-platform.sitecorecloud.io";
|
|
232
371
|
var noopResolver = async () => ({});
|
|
372
|
+
var EMPTY_CONFIGS = /* @__PURE__ */ new Map();
|
|
373
|
+
function getPageItemIdFromNextData() {
|
|
374
|
+
if (typeof window === "undefined") return null;
|
|
375
|
+
try {
|
|
376
|
+
const nd = window.__NEXT_DATA__;
|
|
377
|
+
return nd?.props?.pageProps?.layoutData?.sitecore?.route?.itemId ?? null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
233
382
|
function PersonalizeProvider({
|
|
234
383
|
children,
|
|
235
384
|
sitecoreEdgeContextId,
|
|
236
385
|
sitecoreEdgeUrl = DEFAULT_EDGE_URL,
|
|
237
386
|
siteName = "",
|
|
387
|
+
sitePath,
|
|
238
388
|
clientKey = "",
|
|
239
389
|
pointOfSale = "",
|
|
240
390
|
edgeUrl,
|
|
@@ -250,6 +400,8 @@ function PersonalizeProvider({
|
|
|
250
400
|
const useEdgeProxy = Boolean(sitecoreEdgeContextId);
|
|
251
401
|
const [browserId, setBrowserId] = useState("");
|
|
252
402
|
const [detectedEditing, setDetectedEditing] = useState(false);
|
|
403
|
+
const [configs, setConfigs] = useState(EMPTY_CONFIGS);
|
|
404
|
+
const [configsLoaded, setConfigsLoaded] = useState(false);
|
|
253
405
|
useEffect(() => {
|
|
254
406
|
setDebug(debug);
|
|
255
407
|
}, [debug]);
|
|
@@ -260,6 +412,7 @@ function PersonalizeProvider({
|
|
|
260
412
|
sitecoreEdgeContextId: sitecoreEdgeContextId ?? "(none)",
|
|
261
413
|
sitecoreEdgeUrl,
|
|
262
414
|
siteName: siteName || "(none)",
|
|
415
|
+
sitePath: sitePath ?? "(none \u2014 configs will not be loaded from Edge)",
|
|
263
416
|
clientKey: clientKey ? `${clientKey.slice(0, 8)}...` : "(none)",
|
|
264
417
|
pointOfSale: pointOfSale || "(none)",
|
|
265
418
|
edgeUrl: edgeUrl ?? "(none)",
|
|
@@ -296,6 +449,36 @@ function PersonalizeProvider({
|
|
|
296
449
|
log("Editing mode overridden via prop:", isEditingProp);
|
|
297
450
|
}
|
|
298
451
|
}, [isEditingProp]);
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
const pageItemId = getPageItemIdFromNextData();
|
|
454
|
+
if (!pageItemId) {
|
|
455
|
+
warn("Config loader: could not read page item ID from __NEXT_DATA__.sitecore.route.itemId \u2014 cannot load configs");
|
|
456
|
+
setConfigsLoaded(true);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
let graphqlUrl;
|
|
460
|
+
let headers = {};
|
|
461
|
+
if (useEdgeProxy) {
|
|
462
|
+
const base = sitecoreEdgeUrl.replace(/\/$/, "");
|
|
463
|
+
graphqlUrl = `${base}/v1/content/api/graphql/v1?sitecoreContextId=${encodeURIComponent(sitecoreEdgeContextId)}`;
|
|
464
|
+
} else if (edgeUrl && apiKey) {
|
|
465
|
+
graphqlUrl = edgeUrl;
|
|
466
|
+
headers = { sc_apikey: apiKey };
|
|
467
|
+
} else {
|
|
468
|
+
warn("Config loader: no Edge endpoint available \u2014 cannot load configs");
|
|
469
|
+
setConfigsLoaded(true);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
log("Config loader: starting", { pageItemId, sitePathOverride: sitePath ?? "(auto-discover)" });
|
|
473
|
+
loadPageConfigs(graphqlUrl, pageItemId, language, headers, sitePath).then((loaded) => {
|
|
474
|
+
log("Config loader: complete,", loaded.size, "configs loaded");
|
|
475
|
+
setConfigs(loaded);
|
|
476
|
+
setConfigsLoaded(true);
|
|
477
|
+
}).catch((err) => {
|
|
478
|
+
warn("Config loader: failed", err);
|
|
479
|
+
setConfigsLoaded(true);
|
|
480
|
+
});
|
|
481
|
+
}, [sitePath, useEdgeProxy, sitecoreEdgeContextId, sitecoreEdgeUrl, edgeUrl, apiKey, language]);
|
|
299
482
|
const effectiveEditing = isEditingProp ?? detectedEditing;
|
|
300
483
|
const effectiveResolver = useCallback(() => {
|
|
301
484
|
if (resolveDatasource) {
|
|
@@ -310,7 +493,7 @@ function PersonalizeProvider({
|
|
|
310
493
|
log("Resolver: using direct Edge GraphQL", { edgeUrl });
|
|
311
494
|
return createEdgeResolver(edgeUrl, apiKey, language);
|
|
312
495
|
}
|
|
313
|
-
warn("Resolver: no resolver configured \u2014 resolveDatasource will return {}.
|
|
496
|
+
warn("Resolver: no resolver configured \u2014 resolveDatasource will return {}.");
|
|
314
497
|
return noopResolver;
|
|
315
498
|
}, [resolveDatasource, useEdgeProxy, sitecoreEdgeUrl, sitecoreEdgeContextId, edgeUrl, apiKey, language])();
|
|
316
499
|
const effectiveBrowserId = browserId || (!useEdgeProxy && clientKey && typeof window !== "undefined" ? getBrowserId(clientKey) : "");
|
|
@@ -328,7 +511,9 @@ function PersonalizeProvider({
|
|
|
328
511
|
useEdgeProxy,
|
|
329
512
|
edgeProxyUrl: useEdgeProxy ? sitecoreEdgeUrl : "",
|
|
330
513
|
sitecoreEdgeContextId: sitecoreEdgeContextId ?? "",
|
|
331
|
-
siteName
|
|
514
|
+
siteName,
|
|
515
|
+
configs,
|
|
516
|
+
configsLoaded
|
|
332
517
|
}),
|
|
333
518
|
[
|
|
334
519
|
clientKey,
|
|
@@ -343,7 +528,9 @@ function PersonalizeProvider({
|
|
|
343
528
|
useEdgeProxy,
|
|
344
529
|
sitecoreEdgeUrl,
|
|
345
530
|
sitecoreEdgeContextId,
|
|
346
|
-
siteName
|
|
531
|
+
siteName,
|
|
532
|
+
configs,
|
|
533
|
+
configsLoaded
|
|
347
534
|
]
|
|
348
535
|
);
|
|
349
536
|
return /* @__PURE__ */ jsx(PersonalizeContext.Provider, { value, children });
|
|
@@ -501,6 +688,13 @@ async function resolveContent(options) {
|
|
|
501
688
|
import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
502
689
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
503
690
|
var DEFAULT_GET_CONFIG = (props) => props.rendering?.personalizeConnect;
|
|
691
|
+
function normalizeGuid2(id) {
|
|
692
|
+
return id.replace(/[{}]/g, "").toLowerCase();
|
|
693
|
+
}
|
|
694
|
+
function getRenderingUid(props) {
|
|
695
|
+
const rendering = props.rendering;
|
|
696
|
+
return rendering?.uid;
|
|
697
|
+
}
|
|
504
698
|
var INDICATOR_BORDER = {
|
|
505
699
|
position: "relative",
|
|
506
700
|
border: "2px dashed #6B5CE7",
|
|
@@ -528,17 +722,36 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
528
722
|
const componentName = WrappedComponent.displayName ?? WrappedComponent.name ?? "Component";
|
|
529
723
|
function PersonalizeConnectWrapper(props) {
|
|
530
724
|
const context = usePersonalizeContext();
|
|
531
|
-
const config = getConfig(props);
|
|
532
725
|
const [resolvedFields, setResolvedFields] = useState2(null);
|
|
533
726
|
const mountedRef = useRef(true);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
727
|
+
let config = getConfig(props);
|
|
728
|
+
if (!config && context) {
|
|
729
|
+
const uid = getRenderingUid(props);
|
|
730
|
+
if (uid) {
|
|
731
|
+
const normalizedUid = normalizeGuid2(uid);
|
|
732
|
+
const fromContext = context.configs.get(normalizedUid);
|
|
733
|
+
if (fromContext) {
|
|
734
|
+
log(`[${componentName}] Config found in context for rendering uid ${normalizedUid}:`, {
|
|
735
|
+
friendlyId: fromContext.friendlyId,
|
|
736
|
+
defaultKey: fromContext.defaultKey,
|
|
737
|
+
keys: Object.keys(fromContext.contentMap)
|
|
738
|
+
});
|
|
739
|
+
config = fromContext;
|
|
740
|
+
} else if (context.configsLoaded) {
|
|
741
|
+
log(`[${componentName}] No config match for uid "${normalizedUid}". All configs:`, Object.fromEntries(context.configs));
|
|
742
|
+
} else {
|
|
743
|
+
log(`[${componentName}] Configs still loading for uid ${normalizedUid}...`);
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
log(`[${componentName}] No rendering uid on props \u2014 cannot look up config from context`);
|
|
747
|
+
}
|
|
538
748
|
}
|
|
539
|
-
if (!context) {
|
|
749
|
+
if (!config && !context) {
|
|
540
750
|
warn(`[${componentName}] PersonalizeContext is null \u2014 is PersonalizeProvider mounted?`);
|
|
541
751
|
}
|
|
752
|
+
if (config) {
|
|
753
|
+
log(`[${componentName}] Config active:`, { friendlyId: config.friendlyId, defaultKey: config.defaultKey, keys: Object.keys(config.contentMap) });
|
|
754
|
+
}
|
|
542
755
|
const runPersonalization = useCallback2(async () => {
|
|
543
756
|
if (!config || !context) return;
|
|
544
757
|
group(`[${componentName}] personalization flow`);
|
|
@@ -559,7 +772,10 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
559
772
|
return;
|
|
560
773
|
}
|
|
561
774
|
if (resolved) {
|
|
562
|
-
log(`[${componentName}] Fields resolved \u2014 swapping props.fields`, {
|
|
775
|
+
log(`[${componentName}] Fields resolved \u2014 swapping props.fields`, {
|
|
776
|
+
datasourceId: resolved.datasourceId,
|
|
777
|
+
fieldNames: Object.keys(resolved.fields)
|
|
778
|
+
});
|
|
563
779
|
setResolvedFields(resolved.fields);
|
|
564
780
|
} else {
|
|
565
781
|
warn(`[${componentName}] Content resolution returned null \u2014 component keeps original fields`);
|
|
@@ -568,20 +784,20 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
|
|
|
568
784
|
}, [config, context]);
|
|
569
785
|
useEffect2(() => {
|
|
570
786
|
mountedRef.current = true;
|
|
571
|
-
if (config && context) {
|
|
787
|
+
if (config && context && context.browserId) {
|
|
572
788
|
runPersonalization();
|
|
573
789
|
}
|
|
574
790
|
return () => {
|
|
575
791
|
mountedRef.current = false;
|
|
576
792
|
};
|
|
577
|
-
}, [config?.friendlyId, context?.
|
|
793
|
+
}, [config?.friendlyId, context?.browserId, context?.configsLoaded, runPersonalization]);
|
|
578
794
|
if (!config || !context) {
|
|
579
795
|
return /* @__PURE__ */ jsx2(WrappedComponent, { ...props });
|
|
580
796
|
}
|
|
581
797
|
const mergedProps = resolvedFields ? { ...props, fields: resolvedFields } : props;
|
|
582
798
|
const component = /* @__PURE__ */ jsx2(WrappedComponent, { ...mergedProps });
|
|
583
799
|
if (context.isEditing) {
|
|
584
|
-
log(`[${componentName}] Editing mode \u2014 rendering indicator badge`);
|
|
800
|
+
log(`[${componentName}] Editing mode \u2014 rendering indicator badge for ${config.friendlyId}`);
|
|
585
801
|
return /* @__PURE__ */ jsxs("div", { style: INDICATOR_BORDER, "data-personalize-connect": config.friendlyId, children: [
|
|
586
802
|
/* @__PURE__ */ jsx2("span", { style: INDICATOR_BADGE, title: `Personalize: ${config.friendlyId}`, children: "\u26A1 Personalized" }),
|
|
587
803
|
component
|
|
@@ -653,6 +869,7 @@ export {
|
|
|
653
869
|
getEdgeBrowserId,
|
|
654
870
|
isDebugEnabled,
|
|
655
871
|
isEditingMode,
|
|
872
|
+
loadPageConfigs,
|
|
656
873
|
resetEdgeInitCache,
|
|
657
874
|
resetEditingDetectionCache,
|
|
658
875
|
resolveContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "personalize-connect-sdk",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Runtime SDK for Personalize Connect - resolves active experience outcomes and datasources in your rendering host",
|
|
5
5
|
"author": "Dylan Young",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/
|
|
17
|
+
"url": "https://github.com/Sitecore-Hackathon/2026-Team-Solo.git",
|
|
18
18
|
"directory": "packages/sdk"
|
|
19
19
|
},
|
|
20
20
|
"main": "dist/index.js",
|