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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # Personalize Connect SDK
2
2
 
3
- A lightweight Next.js package that bridges Sitecore Personalize Interactive Experiences with XM Cloud component datasources at runtime. The SDK reads configuration authored by the Marketplace app, calls Personalize for a decision, and swaps component content accordingly — with zero per-component code.
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
- ## Usage
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 RootLayout({ children }) {
24
+ export default function App({ children }) {
21
25
  return (
22
26
  <PersonalizeProvider
23
- clientKey={process.env.NEXT_PUBLIC_PERSONALIZE_CLIENT_KEY!}
24
- pointOfSale={process.env.NEXT_PUBLIC_PERSONALIZE_POINT_OF_SALE!}
25
- resolveDatasource={async (datasourceId) => {
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
- // Config is read from props.rendering.personalizeConnect (or customize via getConfig)
44
- export default withPersonalizeConnect(MyComponent);
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 renders immediately with the default datasource, calls Personalize asynchronously, and re-renders with personalized content when resolved. No changes needed inside the component.
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({ personalizeConnect }) {
55
- const { contentKey, resolvedFields, isLoading, error } = usePersonalizeExperience(personalizeConnect);
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
- ## Config shape
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
- Config is attached to each rendering in XMC layout data (authored by the Marketplace app):
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; // Personalize Interactive Experience ID
137
+ friendlyId: string; // Personalize Interactive Experience ID
69
138
  contentMap: Record<string, string>; // contentKey -> datasource GUID
70
- defaultKey: string; // Fallback key if experience fails
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
- - `PersonalizeProvider`, `usePersonalizeContext` — Provider and context
77
- - `withPersonalizeConnect` — HOC for zero-code personalization
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
- - `callPersonalize` — Low-level API client
80
- - `resolveContent` — Map contentKey to datasource fields
81
- - `getBrowserId` — Browser ID cookie helper
82
- - Types: `PersonalizeConnectConfig`, `PersonalizeConnectProviderProps`, etc.
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
- * If the rendering has a personalizeConnect config, it renders with defaultKey first,
138
- * then asynchronously fetches the personalized content and re-renders.
139
- * If no config, passes through unchanged.
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
- * If the rendering has a personalizeConnect config, it renders with defaultKey first,
138
- * then asynchronously fetches the personalized content and re-renders.
139
- * If no config, passes through unchanged.
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 = "[PersonalizeConnect]";
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 {}. Provide sitecoreEdgeContextId, edgeUrl+apiKey, or a custom resolveDatasource.");
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
- if (!config) {
575
- log(`[${componentName}] No personalizeConnect config on rendering \u2014 passthrough`);
576
- } else {
577
- log(`[${componentName}] Config found:`, { friendlyId: config.friendlyId, defaultKey: config.defaultKey, keys: Object.keys(config.contentMap) });
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`, { datasourceId: resolved.datasourceId, fieldNames: Object.keys(resolved.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?.clientKey, runPersonalization]);
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 = "[PersonalizeConnect]";
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 {}. Provide sitecoreEdgeContextId, edgeUrl+apiKey, or a custom resolveDatasource.");
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
- if (!config) {
535
- log(`[${componentName}] No personalizeConnect config on rendering \u2014 passthrough`);
536
- } else {
537
- log(`[${componentName}] Config found:`, { friendlyId: config.friendlyId, defaultKey: config.defaultKey, keys: Object.keys(config.contentMap) });
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`, { datasourceId: resolved.datasourceId, fieldNames: Object.keys(resolved.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?.clientKey, runPersonalization]);
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.1",
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/2026-Team-Solo/2026-Team-Solo.git",
17
+ "url": "https://github.com/Sitecore-Hackathon/2026-Team-Solo.git",
18
18
  "directory": "packages/sdk"
19
19
  },
20
20
  "main": "dist/index.js",