From cb7eb5388c14921f182532987793403410628ec2 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 11:02:41 +0300 Subject: [PATCH 01/14] update normal rendering process for component level personalization --- .../page-props-factory/plugins/personalize.ts | 10 ++++-- .../src/personalize/layout-personalizer.ts | 35 ++++++++++++------- .../sitecore-jss/src/personalize/utils.ts | 18 ++++++++-- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/personalize.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/personalize.ts index cc398acef7..ff66ffdc76 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/personalize.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/personalize.ts @@ -16,12 +16,16 @@ class PersonalizePlugin implements Plugin { ? context.params.path.join('/') : context.params.path ?? '/'; - // Get variant for personalization (from path) + // Get variant(s) for personalization (from path) const personalizeData = getPersonalizedRewriteData(path); - // Modify layoutData to use specific variant instead of default + // Modify layoutData to use specific variant(s) instead of default // This will also set the variantId on the Sitecore context so that it is accessible here - personalizeLayout(props.layoutData, personalizeData.variantId); + personalizeLayout( + props.layoutData, + personalizeData.variantId, + personalizeData.componentVariantIds + ); return props; } diff --git a/packages/sitecore-jss/src/personalize/layout-personalizer.ts b/packages/sitecore-jss/src/personalize/layout-personalizer.ts index 74fe335769..bb9bb34b91 100644 --- a/packages/sitecore-jss/src/personalize/layout-personalizer.ts +++ b/packages/sitecore-jss/src/personalize/layout-personalizer.ts @@ -13,9 +13,14 @@ export type ComponentRenderingWithExperiences = ComponentRendering & { * Apply personalization to layout data. This will recursively go through all placeholders/components, check experiences nodes and replace default with object from specific experience. * @param {LayoutServiceData} layout Layout data * @param {string} variantId variant id + * @param {string[]} [componentVariantIds] component variant ids */ -export function personalizeLayout(layout: LayoutServiceData, variantId: string): void { - // Add variantId to Sitecore context so that it is accessible here +export function personalizeLayout( + layout: LayoutServiceData, + variantId: string, + componentVariantIds?: string[] +): void { + // Add (page-level) variantId to Sitecore context so that it is accessible here layout.sitecore.context.variantId = variantId; const placeholders = layout.sitecore.route?.placeholders; if (Object.keys(placeholders ?? {}).length === 0) { @@ -23,34 +28,36 @@ export function personalizeLayout(layout: LayoutServiceData, variantId: string): } if (placeholders) { Object.keys(placeholders).forEach((placeholder) => { - placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], variantId); + placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], [ + variantId, + ...(componentVariantIds || []), + ]); }); } } /** - * @param {Array} components components within placeholder - * @param {string} variantId variant id + * @param {string[]} variantIds variant ids * @returns {Array} components with personalization applied */ export function personalizePlaceholder( components: Array, - variantId: string + variantIds: string[] ): Array { return components .map((component) => { const rendering = component as ComponentRendering; if ((rendering as ComponentRenderingWithExperiences).experiences !== undefined) { - return personalizeComponent(rendering as ComponentRenderingWithExperiences, variantId) as + return personalizeComponent(rendering as ComponentRenderingWithExperiences, variantIds) as | ComponentRendering | HtmlElementRendering; } else if (rendering.placeholders) { const placeholders = rendering.placeholders as PlaceholdersData; Object.keys(placeholders).forEach((placeholder) => { - placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], variantId); + placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], variantIds); }); } @@ -61,14 +68,18 @@ export function personalizePlaceholder( /** * @param {ComponentRenderingWithExperiences} component component with experiences - * @param {string} variantId variant id + * @param {string[]} variantIds variant ids * @returns {ComponentRendering | null} component with personalization applied or null if hidden */ export function personalizeComponent( component: ComponentRenderingWithExperiences, - variantId: string + variantIds: string[] ): ComponentRendering | null { - const variant = component.experiences[variantId]; + // Check if we have an experience matching any of the variants (there should be at most 1) + const match = Object.keys(component.experiences).find((variantId) => + variantIds.includes(variantId) + ); + const variant = match && component.experiences[match]; if (variant === undefined && component.componentName === undefined) { // DEFAULT IS HIDDEN return null; @@ -90,7 +101,7 @@ export function personalizeComponent( if (component.placeholders) { component.placeholders[placeholder] = personalizePlaceholder( component.placeholders[placeholder], - variantId + variantIds ); } }); diff --git a/packages/sitecore-jss/src/personalize/utils.ts b/packages/sitecore-jss/src/personalize/utils.ts index eb9d9feb53..94e4453da0 100644 --- a/packages/sitecore-jss/src/personalize/utils.ts +++ b/packages/sitecore-jss/src/personalize/utils.ts @@ -3,6 +3,7 @@ export const VARIANT_PREFIX = '_variantId_'; export type PersonalizedRewriteData = { variantId: string; + componentVariantIds?: string[]; }; /** @@ -24,11 +25,21 @@ export function getPersonalizedRewrite(pathname: string, data: PersonalizedRewri export function getPersonalizedRewriteData(pathname: string): PersonalizedRewriteData { const data: PersonalizedRewriteData = { variantId: DEFAULT_VARIANT, + componentVariantIds: [], }; const path = pathname.endsWith('/') ? pathname : pathname + '/'; const result = path.match(`${VARIANT_PREFIX}(.*?)\\/`); if (result) { - data.variantId = result[1]; + const variantId = result[1]; + if (variantId.includes('_')) { + // Component-level personalization in format "_" + // There can be multiple + data.componentVariantIds?.push(variantId); + } else { + // Embedded (page-level) personalization in format "" + // There should be only one + data.variantId = variantId; + } } return data; } @@ -42,8 +53,9 @@ export function normalizePersonalizedRewrite(pathname: string): string { if (!pathname.includes(VARIANT_PREFIX)) { return pathname; } - const result = pathname.match(`${VARIANT_PREFIX}.*?(?:\\/|$)`); - return result === null ? pathname : pathname.replace(result[0], ''); + let segments = pathname.split('/'); + segments = segments.filter((segment) => !segment.includes(VARIANT_PREFIX)); + return segments.join('/'); } /** From 702985185a29330f2d5502c8356ed118fcf02ef3 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 11:20:25 +0300 Subject: [PATCH 02/14] update getPersonalizedRewriteData --- .../sitecore-jss/src/personalize/utils.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/sitecore-jss/src/personalize/utils.ts b/packages/sitecore-jss/src/personalize/utils.ts index 94e4453da0..e7f6f98149 100644 --- a/packages/sitecore-jss/src/personalize/utils.ts +++ b/packages/sitecore-jss/src/personalize/utils.ts @@ -27,20 +27,22 @@ export function getPersonalizedRewriteData(pathname: string): PersonalizedRewrit variantId: DEFAULT_VARIANT, componentVariantIds: [], }; - const path = pathname.endsWith('/') ? pathname : pathname + '/'; - const result = path.match(`${VARIANT_PREFIX}(.*?)\\/`); - if (result) { - const variantId = result[1]; - if (variantId.includes('_')) { - // Component-level personalization in format "_" - // There can be multiple - data.componentVariantIds?.push(variantId); - } else { - // Embedded (page-level) personalization in format "" - // There should be only one - data.variantId = variantId; + const segments = pathname.split('/'); + segments.forEach((segment) => { + const result = segment.match(`${VARIANT_PREFIX}(.*$)`); + if (result) { + const variantId = result[1]; + if (variantId.includes('_')) { + // Component-level personalization in format "_" + // There can be multiple + data.componentVariantIds?.push(variantId); + } else { + // Embedded (page-level) personalization in format "" + // There should be only one + data.variantId = variantId; + } } - } + }); return data; } From 87bc79dee4151a50d4076c365f3b894cc8cca561 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 12:48:20 +0300 Subject: [PATCH 03/14] unit tests for personalize utils; small update to normalizePersonalizedRewrite function --- .../src/personalize/utils.test.ts | 49 ++++++++++++++++++- .../sitecore-jss/src/personalize/utils.ts | 4 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss/src/personalize/utils.test.ts b/packages/sitecore-jss/src/personalize/utils.test.ts index f923fc3b5d..99c5ab1ba0 100644 --- a/packages/sitecore-jss/src/personalize/utils.test.ts +++ b/packages/sitecore-jss/src/personalize/utils.test.ts @@ -57,6 +57,25 @@ describe('utils', () => { const path1 = `/${VARIANT_PREFIX}${testId}/some/path/`; const path2 = `/_site_mysite/${VARIANT_PREFIX}${testId}/some/path/`; + expect(getPersonalizedRewriteData(path1)).to.deep.equal(getPersonalizedRewriteData(path2)); + }); + it('should return the personalized data with component variant ids from the rewrite path', () => { + const pathname = `/some/path/${VARIANT_PREFIX}123/${VARIANT_PREFIX}comp1_var2/${VARIANT_PREFIX}comp2_var1/`; + const expectedResult = { + variantId: '123', + componentVariantIds: ['comp1_var2', 'comp2_var1'], + }; + const result = getPersonalizedRewriteData(pathname); + expect(result).to.deep.equal(expectedResult); + }); + it('should return the personalized data with component variant ids from any position in pathname', () => { + const persSegmentOne = `${VARIANT_PREFIX}123`; + const persSegmentTwo = `${VARIANT_PREFIX}comp1_var2`; + const persSegmentThree = `${VARIANT_PREFIX}comp2_var1'`; + + const path1 = `/${persSegmentOne}/${persSegmentTwo}/${persSegmentThree}/some/path/`; + const path2 = `/_site_mysite/${persSegmentOne}/${persSegmentTwo}/${persSegmentThree}/some/path/`; + expect(getPersonalizedRewriteData(path1)).to.deep.equal(getPersonalizedRewriteData(path2)); }); }); @@ -87,9 +106,35 @@ describe('utils', () => { }); it('should normalize path with other prefixes present', () => { const pathname = `/_site_mysite/${VARIANT_PREFIX}foo`; - const pathNameInversed = `/${VARIANT_PREFIX}foo/_site_mysite/`; + const pathNameInversed = `/${VARIANT_PREFIX}foo/_site_mysite`; + const result = normalizePersonalizedRewrite(pathname); + expect(result).to.equal('/_site_mysite'); + expect(normalizePersonalizedRewrite(pathNameInversed)).to.equal(result); + }); + + it('should return pathname without variant id and component variant ids', () => { + const pathname = `/${VARIANT_PREFIX}foo/${VARIANT_PREFIX}comp1_var1/${VARIANT_PREFIX}comp2_var3/some/path`; + const result = normalizePersonalizedRewrite(pathname); + expect(result).to.equal('/some/path'); + }); + + it('should return the root pathname without the variant id and component ids', () => { + const pathname = `/${VARIANT_PREFIX}foo/${VARIANT_PREFIX}comp1_var1/${VARIANT_PREFIX}comp2_var3/`; + const result = normalizePersonalizedRewrite(pathname); + expect(result).to.equal('/'); + }); + + it('should return the root pathname without the variant id and component ids when pathname not ends with "/"', () => { + const pathname = `/${VARIANT_PREFIX}foo/${VARIANT_PREFIX}comp1_var1/${VARIANT_PREFIX}comp2_var3`; + const result = normalizePersonalizedRewrite(pathname); + expect(result).to.equal('/'); + }); + + it('should normalize path with multiple component variant ids with other prefixes present', () => { + const pathname = `/_site_mysite/${VARIANT_PREFIX}foo/${VARIANT_PREFIX}comp1_var1/${VARIANT_PREFIX}comp2_var3`; + const pathNameInversed = `/${VARIANT_PREFIX}foo/${VARIANT_PREFIX}comp1_var1/${VARIANT_PREFIX}comp2_var3/_site_mysite`; const result = normalizePersonalizedRewrite(pathname); - expect(result).to.equal('/_site_mysite/'); + expect(result).to.equal('/_site_mysite'); expect(normalizePersonalizedRewrite(pathNameInversed)).to.equal(result); }); }); diff --git a/packages/sitecore-jss/src/personalize/utils.ts b/packages/sitecore-jss/src/personalize/utils.ts index e7f6f98149..fbdde69173 100644 --- a/packages/sitecore-jss/src/personalize/utils.ts +++ b/packages/sitecore-jss/src/personalize/utils.ts @@ -57,7 +57,9 @@ export function normalizePersonalizedRewrite(pathname: string): string { } let segments = pathname.split('/'); segments = segments.filter((segment) => !segment.includes(VARIANT_PREFIX)); - return segments.join('/'); + const result = segments.join('/'); + // return root path if all segments were personalize data + return result ? result : '/'; } /** From 58ecc93f13c65210d26b18186155043ef04b4745 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 15:15:23 +0300 Subject: [PATCH 04/14] layout personalizer unit tests component level personalization --- .../personalize/layout-personalizer.test.ts | 455 ++++++++++++++---- .../src/personalize/layout-personalizer.ts | 2 +- .../src/test-data/personalizeData.ts | 48 ++ 3 files changed, 399 insertions(+), 106 deletions(-) diff --git a/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts b/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts index 844522f3e4..c6632c0a7a 100644 --- a/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts +++ b/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts @@ -11,6 +11,8 @@ import { withoutComponentName, variantIsHidden, component2, + component3, + component4, mountain_bike_audience, city_bike_audience, } from '../test-data/personalizeData'; @@ -36,140 +38,383 @@ describe('layout-personalizer', () => { personalizeLayout(layoutData, variant); expect(layoutData.sitecore.context.variantId).to.equal(variant); }); + + describe('with component variant ids', () => { + it('should not return anything', () => { + const variant = 'test'; + const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedLayoutResult = personalizeLayout( + layoutData, + variant, + componentVariantIds + ); + expect(personalizedLayoutResult).to.equal(undefined); + }); + + it('should return undefined if no placeholders', () => { + const variant = 'test'; + const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedLayoutResult = personalizeLayout( + layoutDataWithoutPlaceholder, + variant, + componentVariantIds + ); + expect(personalizedLayoutResult).to.equal(undefined); + }); + + it('should set variantId on Sitecore context', () => { + const variant = 'test'; + const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + personalizeLayout(layoutData, variant, componentVariantIds); + expect(layoutData.sitecore.context.variantId).to.equal(variant); + }); + }); }); describe('personalizePlaceholder', () => { - it('should return array of personalized components', () => { - const variant = 'mountain_bike_audience'; - const personalizedPlaceholderResult = personalizePlaceholder(componentsArray, variant); - expect(personalizedPlaceholderResult).to.deep.equal(componentsWithExperiencesArray); - }); + describe('with single variant Id', () => { + it('should return array of personalized components', () => { + const variant = 'mountain_bike_audience'; + const personalizedPlaceholderResult = personalizePlaceholder(componentsArray, [variant]); + expect(personalizedPlaceholderResult).to.deep.equal(componentsWithExperiencesArray); + }); - it('should personalize component and nested components', () => { - const rootComponent = { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock', - dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', - fields: { content: { value: '' }, heading: { value: 'Default Content' } }, - experiences: { - mountain_bike_audience: { - ...mountain_bike_audience, + it('should personalize component and nested components', () => { + const rootComponent = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, + experiences: { + mountain_bike_audience: { + ...mountain_bike_audience, + placeholders: { + main: [component, component2], + }, + }, + city_bike_audience, + }, + }; + const variant = 'mountain_bike_audience'; + const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], [variant]); + expect(personalizedPlaceholderResult).to.deep.equal([ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, placeholders: { - main: [component, component2], + main: [ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 2', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + ], }, }, - city_bike_audience, - }, - }; - const variant = 'mountain_bike_audience'; - const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variant); - expect(personalizedPlaceholderResult).to.deep.equal([ - { + ]); + }); + + it('should personalize nested components', () => { + const rootComponent = { uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', componentName: 'ContentBlock', - dataSource: '20679cd4-356b-4452-b507-453beeb0be39', - fields: mountain_bike_audience.fields, + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, placeholders: { - main: [ - { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock', - dataSource: '20679cd4-356b-4452-b507-453beeb0be39', - fields: mountain_bike_audience.fields, - }, - { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock 2', - dataSource: '20679cd4-356b-4452-b507-453beeb0be39', - fields: mountain_bike_audience.fields, - }, - ], + main: [component, component2], }, - }, - ]); + }; + + const variant = 'mountain_bike_audience'; + const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], [variant]); + expect(personalizedPlaceholderResult).to.deep.equal([ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, + placeholders: { + main: [ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 2', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + ], + }, + }, + ]); + }); }); - it('should personalize nested components', () => { - const rootComponent = { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock', - dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', - fields: { content: { value: '' }, heading: { value: 'Default Content' } }, - placeholders: { - main: [component, component2], - }, - }; - - const variant = 'mountain_bike_audience'; - const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variant); - expect(personalizedPlaceholderResult).to.deep.equal([ - { + describe('with multiple variant Ids', () => { + it('should return array of personalized components', () => { + const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedPlaceholderResult = personalizePlaceholder(componentsArray, variantIds); + expect(personalizedPlaceholderResult).to.deep.equal(componentsWithExperiencesArray); + }); + + it('should personalize component and nested components', () => { + const rootComponent = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, + experiences: { + mountain_bike_audience: { + ...mountain_bike_audience, + placeholders: { + main: [component, component2], + }, + }, + city_bike_audience, + }, + }; + const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variantIds); + expect(personalizedPlaceholderResult).to.deep.equal([ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + placeholders: { + main: [ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 2', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + ], + }, + }, + ]); + }); + + it('should personalize nested components', () => { + const rootComponent = { uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', componentName: 'ContentBlock', dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', fields: { content: { value: '' }, heading: { value: 'Default Content' } }, placeholders: { - main: [ - { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock', - dataSource: '20679cd4-356b-4452-b507-453beeb0be39', - fields: mountain_bike_audience.fields, + main: [component, component2], + }, + }; + + const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variantIds); + expect(personalizedPlaceholderResult).to.deep.equal([ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, + placeholders: { + main: [ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 2', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: mountain_bike_audience.fields, + }, + ], + }, + }, + ]); + }); + + it('should personalize multiple components', () => { + const rootComponent = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2', + fields: { content: { value: '' }, heading: { value: 'Default Content' } }, + experiences: { + mountain_bike_audience: { + ...mountain_bike_audience, + placeholders: { + main: [component3, component4], }, - { - uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', - componentName: 'ContentBlock 2', - dataSource: '20679cd4-356b-4452-b507-453beeb0be39', - fields: mountain_bike_audience.fields, + }, + city_bike_audience, + }, + }; + const variantIds = ['mountain_bike_audience', 'sand_bike_audience', 'snow_bike_audience']; + const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variantIds); + expect(personalizedPlaceholderResult).to.deep.equal([ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '20679cd4-356b-4452-b507-453beeb0be39', + fields: { + content: { + value: + '

', + }, + heading: { + value: 'Mountain Bike', }, - ], + }, + placeholders: { + main: [ + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 3', + dataSource: '36e02581-2056-4c55-a4d5-f4b700ba1ae2', + fields: { + content: { + value: + '

', + }, + heading: { + value: 'Snow Bike', + }, + }, + }, + { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock 4', + dataSource: '36e02581-2056-4c55-a4d5-f4b700ba1ae2', + fields: { + content: { + value: + '

', + }, + heading: { + value: 'Sand Bike', + }, + }, + }, + ], + }, }, - }, - ]); + ]); + }); }); }); describe('personalizeComponent', () => { - it('should return personalized component without experiences', () => { - const variant = 'mountain_bike_audience'; - const personalizedComponentResult = personalizeComponent( - (component as unknown) as ComponentRenderingWithExperiences, - variant - ); - expect(personalizedComponentResult).to.deep.equal(componentWithExperiences); - expect( - (personalizedComponentResult as ComponentRenderingWithExperiences).experiences - ).to.deep.equal(undefined); - }); + describe('with single variantId', () => { + // clone the original component since it will be modified + const testComponent = structuredClone(component); - it('should return default component without experiences when variant is undefined', () => { - const variant = '_default'; - const personalizedComponentResult = personalizeComponent( - (component as unknown) as ComponentRenderingWithExperiences, - variant - ); - expect(personalizedComponentResult).to.deep.equal(component); - expect( - (personalizedComponentResult as ComponentRenderingWithExperiences).experiences - ).to.deep.equal({}); - }); + it('should return personalized component without experiences', () => { + const variant = 'mountain_bike_audience'; + const personalizedComponentResult = personalizeComponent( + (testComponent as unknown) as ComponentRenderingWithExperiences, + [variant] + ); + expect(personalizedComponentResult).to.deep.equal(componentWithExperiences); + expect( + (personalizedComponentResult as ComponentRenderingWithExperiences).experiences + ).to.deep.equal(undefined); + }); - it('should return null when variantVariant is hidden', () => { - const variant = 'mountain_bike_audience'; - const personalizedComponentResult = personalizeComponent( - (variantIsHidden as unknown) as ComponentRenderingWithExperiences, - variant - ); - expect(personalizedComponentResult).to.equal(null); + it('should return default component without experiences when variant is undefined', () => { + const variant = '_default'; + const personalizedComponentResult = personalizeComponent( + (testComponent as unknown) as ComponentRenderingWithExperiences, + [variant] + ); + expect(personalizedComponentResult).to.deep.equal(testComponent); + expect( + (personalizedComponentResult as ComponentRenderingWithExperiences).experiences + ).to.deep.equal({}); + }); + + it('should return null when variantVariant is hidden', () => { + const variant = 'mountain_bike_audience'; + const personalizedComponentResult = personalizeComponent( + (variantIsHidden as unknown) as ComponentRenderingWithExperiences, + [variant] + ); + expect(personalizedComponentResult).to.equal(null); + }); + + it('should return null when variantVariant and componentName is undefined', () => { + const variant = 'test'; + const personalizedComponentResult = personalizeComponent( + (withoutComponentName as unknown) as ComponentRenderingWithExperiences, + [variant] + ); + expect(personalizedComponentResult).to.equal(null); + }); }); - it('should return null when variantVariant and componentName is undefined', () => { - const variant = 'test'; - const personalizedComponentResult = personalizeComponent( - (withoutComponentName as unknown) as ComponentRenderingWithExperiences, - variant - ); - expect(personalizedComponentResult).to.equal(null); + describe('with multiple variant Ids', () => { + const testComponent = structuredClone(component); + + it('should return personalized component without experiences', () => { + const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedComponentResult = personalizeComponent( + (testComponent as unknown) as ComponentRenderingWithExperiences, + variantIds + ); + expect(personalizedComponentResult).to.deep.equal(componentWithExperiences); + expect( + (personalizedComponentResult as ComponentRenderingWithExperiences).experiences + ).to.deep.equal(undefined); + }); + + it('should return default component without experiences when variant is undefined', () => { + const variantIds = ['_default', 'another_variant', 'third_variant']; + const personalizedComponentResult = personalizeComponent( + (testComponent as unknown) as ComponentRenderingWithExperiences, + variantIds + ); + expect(personalizedComponentResult).to.deep.equal(testComponent); + expect( + (personalizedComponentResult as ComponentRenderingWithExperiences).experiences + ).to.deep.equal({}); + }); + + it('should return null when variantVariant is hidden', () => { + const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + const personalizedComponentResult = personalizeComponent( + (variantIsHidden as unknown) as ComponentRenderingWithExperiences, + variantIds + ); + expect(personalizedComponentResult).to.equal(null); + }); + + it('should return null when variantVariant and componentName is undefined', () => { + const variantIds = ['test', 'another_variant', 'third_variant']; + const personalizedComponentResult = personalizeComponent( + (withoutComponentName as unknown) as ComponentRenderingWithExperiences, + variantIds + ); + expect(personalizedComponentResult).to.equal(null); + }); }); }); }); diff --git a/packages/sitecore-jss/src/personalize/layout-personalizer.ts b/packages/sitecore-jss/src/personalize/layout-personalizer.ts index bb9bb34b91..7551169554 100644 --- a/packages/sitecore-jss/src/personalize/layout-personalizer.ts +++ b/packages/sitecore-jss/src/personalize/layout-personalizer.ts @@ -84,7 +84,7 @@ export function personalizeComponent( // DEFAULT IS HIDDEN return null; } else if (variant && variant.componentName === null && variant.dataSource === null) { - // HIDDEN + // VARIANT IS HIDDEN return null; } else if (variant) { component = variant; diff --git a/packages/sitecore-jss/src/test-data/personalizeData.ts b/packages/sitecore-jss/src/test-data/personalizeData.ts index 3333105f76..53c95ce103 100644 --- a/packages/sitecore-jss/src/test-data/personalizeData.ts +++ b/packages/sitecore-jss/src/test-data/personalizeData.ts @@ -24,6 +24,32 @@ export const city_bike_audience = { }, }; +export const snow_bike_audience = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '36e02581-2056-4c55-a4d5-f4b700ba1ae2', + fields: { + content: { + value: + '

', + }, + heading: { value: 'Snow Bike' }, + }, +}; + +export const sand_bike_audience = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', + componentName: 'ContentBlock', + dataSource: '36e02581-2056-4c55-a4d5-f4b700ba1ae2', + fields: { + content: { + value: + '

', + }, + heading: { value: 'Sand Bike' }, + }, +}; + export const layoutData = { sitecore: { context: { @@ -99,6 +125,28 @@ export const component2 = { }, }; +export const component3 = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82c1', + componentName: 'ContentBlock 3', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecg3', + fields: { content: { value: '' }, heading: { value: 'Default Content 3' } }, + experiences: { + snow_bike_audience: { ...snow_bike_audience, componentName: 'ContentBlock 3' }, + city_bike_audience: { ...city_bike_audience, componentName: 'ContentBlock 3' }, + }, +}; + +export const component4 = { + uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82c1', + componentName: 'ContentBlock 4', + dataSource: 'e020fb58-1be8-4537-aab8-67916452ecg3', + fields: { content: { value: '' }, heading: { value: 'Default Content 4' } }, + experiences: { + city_bike_audience: { ...mountain_bike_audience, componentName: 'ContentBlock 4' }, + sand_bike_audience: { ...sand_bike_audience, componentName: 'ContentBlock 4' }, + }, +}; + export const componentsArray = [component]; export const withoutComponentName = { From 70d11b7b67a7744b36da91bd619bcfddd84d1720 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 15:53:04 +0300 Subject: [PATCH 05/14] introduce getGroomedVariantIds function --- .../sitecore-jss/src/personalize/utils.ts | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/sitecore-jss/src/personalize/utils.ts b/packages/sitecore-jss/src/personalize/utils.ts index fbdde69173..94c5729142 100644 --- a/packages/sitecore-jss/src/personalize/utils.ts +++ b/packages/sitecore-jss/src/personalize/utils.ts @@ -23,26 +23,41 @@ export function getPersonalizedRewrite(pathname: string, data: PersonalizedRewri * @returns {PersonalizedRewriteData} the personalize data from the rewrite */ export function getPersonalizedRewriteData(pathname: string): PersonalizedRewriteData { - const data: PersonalizedRewriteData = { - variantId: DEFAULT_VARIANT, - componentVariantIds: [], - }; const segments = pathname.split('/'); + const variantIds: string[] = []; segments.forEach((segment) => { const result = segment.match(`${VARIANT_PREFIX}(.*$)`); if (result) { - const variantId = result[1]; - if (variantId.includes('_')) { - // Component-level personalization in format "_" - // There can be multiple - data.componentVariantIds?.push(variantId); - } else { - // Embedded (page-level) personalization in format "" - // There should be only one - data.variantId = variantId; - } + variantIds.push(result[1]); } }); + + return getGroomedVariantIds(variantIds); +} + +/** + * Parses a list of variantIds and divides into layout and component variants + * @param {string[]} variantIds the list of variant IDs for a page + * @returns {PersonalizedRewriteData} object with variant IDs sorted + */ +export function getGroomedVariantIds(variantIds: string[]) { + const data: PersonalizedRewriteData = { + variantId: DEFAULT_VARIANT, + componentVariantIds: [], + }; + + variantIds.forEach((variantId) => { + if (variantId.includes('_')) { + // Component-level personalization in format "_" + // There can be multiple + data.componentVariantIds?.push(variantId); + } else { + // Embedded (page-level) personalization in format "" + // There should be only one + data.variantId = variantId; + } + }); + return data; } From 6a2163ef50b51cd40c43b67998a182158bfd3aa2 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Fri, 19 Jul 2024 09:02:35 -0400 Subject: [PATCH 06/14] unit test getGroomedVariantIds --- .../src/personalize/utils.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/sitecore-jss/src/personalize/utils.test.ts b/packages/sitecore-jss/src/personalize/utils.test.ts index 99c5ab1ba0..40d71e8dea 100644 --- a/packages/sitecore-jss/src/personalize/utils.test.ts +++ b/packages/sitecore-jss/src/personalize/utils.test.ts @@ -6,6 +6,8 @@ import { CdpHelper, VARIANT_PREFIX, DEFAULT_VARIANT, + PersonalizedRewriteData, + getGroomedVariantIds, } from './utils'; describe('utils', () => { @@ -80,6 +82,35 @@ describe('utils', () => { }); }); + describe('getGroomedVariantIds', () => { + it('should return object with DEFAULT_VARIANT only for empty collection', () => { + const input: string[] = []; + const expectedOutput: PersonalizedRewriteData = { + variantId: DEFAULT_VARIANT, + componentVariantIds: [], + }; + expect(getGroomedVariantIds(input)).to.deep.equal(expectedOutput); + }); + + it('should return object with page-level variandId when matching ID found', () => { + const input = ['standard-page-level-varid']; + const expectedOutput: PersonalizedRewriteData = { + variantId: 'standard-page-level-varid', + componentVariantIds: [], + }; + expect(getGroomedVariantIds(input)).to.deep.equal(expectedOutput); + }); + + it('should return object with component variant ids, when matching IDs found', () => { + const input = ['component-id_variantid-1', 'other-component-id_variantid-2']; + const expectedOutput: PersonalizedRewriteData = { + variantId: DEFAULT_VARIANT, + componentVariantIds: input, + }; + expect(getGroomedVariantIds(input)).to.deep.equal(expectedOutput); + }); + }); + describe('normalizePersonalizedRewrite', () => { it('should return a string', () => { expect(normalizePersonalizedRewrite('/some/path')).to.be.a('string'); From 49eafb30df750bbcdef5edf38a17cca21dcb3bd9 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 16:10:27 +0300 Subject: [PATCH 07/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 424e1339f5..3ec10ebd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Our versioning strategy is as follows: * `[sitecore-jss]` _GraphQLRequestClient_ now can accept custom 'headers' in the constructor or via _createClientFactory_ ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[templates/nextjs]` Removed cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[sitecore-jss-nextjs]` Updates to Next.js editing integration to further support secure hosting scenarios (on XM Cloud & Vercel) ([#1832](https://github.com/Sitecore/jss/pull/1832)) +* `[templates/nextjs-xmcloud]` `[sitecore-jss]` Updates the render process in normal mode for component level personalization. ([#1844](https://github.com/Sitecore/jss/pull/1844)) ### 🛠 Breaking Change From 97944328d8620dd0ede50460151ed5ba678f86b5 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 19 Jul 2024 16:16:40 +0300 Subject: [PATCH 08/14] remove unnecesary line --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe63401864..44ad534157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,6 @@ Our versioning strategy is as follows: * `[templates/nextjs-xmcloud]` `[sitecore-jss]` Updates the render process in normal mode for component level personalization. ([#1844](https://github.com/Sitecore/jss/pull/1844)) * `[sitecore-jss]` `[nextjs-xmcloud]` DictionaryService can now use a `site` GraphQL query instead of `search` one to improve performance. This is currently only available for XMCloud deployments and is enabled with `nextjs-xmcloud` add-on by default ([#1804](https://github.com/Sitecore/jss/pull/1804)) - ### 🛠 Breaking Change * Editing Integration Support: ([#1776](https://github.com/Sitecore/jss/pull/1776))([#1792](https://github.com/Sitecore/jss/pull/1792))([#1773](https://github.com/Sitecore/jss/pull/1773))([#1797](https://github.com/Sitecore/jss/pull/1797))([#1800](https://github.com/Sitecore/jss/pull/1800))([#1803](https://github.com/Sitecore/jss/pull/1803))([#1806](https://github.com/Sitecore/jss/pull/1806))([#1809](https://github.com/Sitecore/jss/pull/1809))([#1814](https://github.com/Sitecore/jss/pull/1814))([#1816](https://github.com/Sitecore/jss/pull/1816))([#1819](https://github.com/Sitecore/jss/pull/1819))([#1828](https://github.com/Sitecore/jss/pull/1828))([#1835](https://github.com/Sitecore/jss/pull/1835)) From dbe34737a1fac49bc2aa9e5ba7066971966bc1c6 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 21 Jul 2024 20:54:32 -0400 Subject: [PATCH 09/14] adjust layout personalizer, utils tests --- .../personalize/layout-personalizer.test.ts | 67 ++++++++----------- .../src/personalize/layout-personalizer.ts | 3 +- .../src/personalize/utils.test.ts | 5 +- 3 files changed, 33 insertions(+), 42 deletions(-) diff --git a/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts b/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts index c6632c0a7a..f10a5545e5 100644 --- a/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts +++ b/packages/sitecore-jss/src/personalize/layout-personalizer.test.ts @@ -19,54 +19,39 @@ import { const { personalizeLayout, personalizePlaceholder, personalizeComponent } = personalize; -describe('layout-personalizer', () => { +describe.only('layout-personalizer', () => { + const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; + describe('personalizeLayout', () => { - it('should not return anything', () => { + it('should return unmodified placeholder data when no component variantIds passed', () => { const variant = 'test'; - const personalizedLayoutResult = personalizeLayout(layoutData, variant); - expect(personalizedLayoutResult).to.equal(undefined); + // personalizeLayout modifies the incoming layoutData - we need to clone it to keep tests truthful + const testLayoutData = structuredClone(layoutData); + const personalizedLayoutResult = personalizeLayout(testLayoutData, variant); + expect(personalizedLayoutResult).to.equal(testLayoutData.sitecore.route.placeholders); }); it('should return undefined if no placeholders', () => { const variant = 'test'; - const personalizedLayoutResult = personalizeLayout(layoutDataWithoutPlaceholder, variant); + const testLayoutData = structuredClone(layoutDataWithoutPlaceholder); + const personalizedLayoutResult = personalizeLayout(testLayoutData, variant); expect(personalizedLayoutResult).to.equal(undefined); }); it('should set variantId on Sitecore context', () => { const variant = 'test'; - personalizeLayout(layoutData, variant); - expect(layoutData.sitecore.context.variantId).to.equal(variant); + const testLayoutData = structuredClone(layoutData); + personalizeLayout(testLayoutData, variant); + expect(testLayoutData.sitecore.context.variantId).to.equal(variant); }); describe('with component variant ids', () => { - it('should not return anything', () => { - const variant = 'test'; - const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; - const personalizedLayoutResult = personalizeLayout( - layoutData, - variant, - componentVariantIds - ); - expect(personalizedLayoutResult).to.equal(undefined); - }); - - it('should return undefined if no placeholders', () => { - const variant = 'test'; - const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; - const personalizedLayoutResult = personalizeLayout( - layoutDataWithoutPlaceholder, - variant, - componentVariantIds - ); - expect(personalizedLayoutResult).to.equal(undefined); - }); - - it('should set variantId on Sitecore context', () => { + it('should apply component variant Ids to placeholders', () => { const variant = 'test'; - const componentVariantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; - personalizeLayout(layoutData, variant, componentVariantIds); - expect(layoutData.sitecore.context.variantId).to.equal(variant); + const testLayoutData = structuredClone(layoutData); + const result = personalizeLayout(testLayoutData, variant, componentVariantIds); + console.log(JSON.stringify(result)); + expect(result).to.be.deep.equal({ 'jss-main': [...componentsWithExperiencesArray] }); }); }); }); @@ -186,8 +171,10 @@ describe('layout-personalizer', () => { city_bike_audience, }, }; - const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; - const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variantIds); + const personalizedPlaceholderResult = personalizePlaceholder( + [rootComponent], + componentVariantIds + ); expect(personalizedPlaceholderResult).to.deep.equal([ { uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', @@ -224,9 +211,10 @@ describe('layout-personalizer', () => { main: [component, component2], }, }; - - const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; - const personalizedPlaceholderResult = personalizePlaceholder([rootComponent], variantIds); + const personalizedPlaceholderResult = personalizePlaceholder( + [rootComponent], + componentVariantIds + ); expect(personalizedPlaceholderResult).to.deep.equal([ { uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0', @@ -375,10 +363,9 @@ describe('layout-personalizer', () => { const testComponent = structuredClone(component); it('should return personalized component without experiences', () => { - const variantIds = ['mountain_bike_audience', 'another_variant', 'third_variant']; const personalizedComponentResult = personalizeComponent( (testComponent as unknown) as ComponentRenderingWithExperiences, - variantIds + componentVariantIds ); expect(personalizedComponentResult).to.deep.equal(componentWithExperiences); expect( diff --git a/packages/sitecore-jss/src/personalize/layout-personalizer.ts b/packages/sitecore-jss/src/personalize/layout-personalizer.ts index 7551169554..53424a921a 100644 --- a/packages/sitecore-jss/src/personalize/layout-personalizer.ts +++ b/packages/sitecore-jss/src/personalize/layout-personalizer.ts @@ -19,7 +19,7 @@ export function personalizeLayout( layout: LayoutServiceData, variantId: string, componentVariantIds?: string[] -): void { +): PlaceholdersData | undefined { // Add (page-level) variantId to Sitecore context so that it is accessible here layout.sitecore.context.variantId = variantId; const placeholders = layout.sitecore.route?.placeholders; @@ -34,6 +34,7 @@ export function personalizeLayout( ]); }); } + return placeholders; } /** diff --git a/packages/sitecore-jss/src/personalize/utils.test.ts b/packages/sitecore-jss/src/personalize/utils.test.ts index 40d71e8dea..0a8b7da696 100644 --- a/packages/sitecore-jss/src/personalize/utils.test.ts +++ b/packages/sitecore-jss/src/personalize/utils.test.ts @@ -59,7 +59,10 @@ describe('utils', () => { const path1 = `/${VARIANT_PREFIX}${testId}/some/path/`; const path2 = `/_site_mysite/${VARIANT_PREFIX}${testId}/some/path/`; - expect(getPersonalizedRewriteData(path1)).to.deep.equal(getPersonalizedRewriteData(path2)); + const actual = getPersonalizedRewriteData(path1); + const expected = getPersonalizedRewriteData(path2); + + expect(actual).to.deep.equal(expected); }); it('should return the personalized data with component variant ids from the rewrite path', () => { const pathname = `/some/path/${VARIANT_PREFIX}123/${VARIANT_PREFIX}comp1_var2/${VARIANT_PREFIX}comp2_var1/`; From c18c529b9d3a9545e69e08a3921e869345b0582f Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 22 Jul 2024 15:43:03 +0300 Subject: [PATCH 10/14] add upgrade guide entry --- docs/upgrades/unreleased.md | 314 ++++++++++++++++++++---------------- 1 file changed, 171 insertions(+), 143 deletions(-) diff --git a/docs/upgrades/unreleased.md b/docs/upgrades/unreleased.md index 736e73ce80..2c382155a3 100644 --- a/docs/upgrades/unreleased.md +++ b/docs/upgrades/unreleased.md @@ -1,179 +1,207 @@ ## Unreleased -* If you are importing any _editing_ utils from `@sitecore-jss/sitecore-jss/utils` in your code, please update the import path to `@sitecore-jss/sitecore-jss/editing`. For now these exports are still available in the old path and marked as deprecated. They will be removed in the next major version release. Specifically for the following utils: - * ExperienceEditor - * HorizonEditor - * isEditorActive - * resetEditorChromes - * handleEditorAnchors - * Metadata - * DefaultEditFrameButton - * DefaultEditFrameButtons - * DefaultEditFrameButtonIds - * EditFrameDataSource - * ChromeCommand - * FieldEditButton - * WebEditButton - * EditButtonTypes - * mapButtonToCommand +- If you are importing any _editing_ utils from `@sitecore-jss/sitecore-jss/utils` in your code, please update the import path to `@sitecore-jss/sitecore-jss/editing`. For now these exports are still available in the old path and marked as deprecated. They will be removed in the next major version release. Specifically for the following utils: + - ExperienceEditor + - HorizonEditor + - isEditorActive + - resetEditorChromes + - handleEditorAnchors + - Metadata + - DefaultEditFrameButton + - DefaultEditFrameButtons + - DefaultEditFrameButtonIds + - EditFrameDataSource + - ChromeCommand + - FieldEditButton + - WebEditButton + - EditButtonTypes + - mapButtonToCommand # react -* With the simplification of Editing Support work we have added the following breaking changes to the `sitecore-jss-react` package. Please make the necessary updates. +- With the simplification of Editing Support work we have added the following breaking changes to the `sitecore-jss-react` package. Please make the necessary updates. - `ComponentConsumerProps` is removed. You might need to reuse _WithSitecoreContextProps_ type. ### headless-ssr-experience-edge -* Replace `scripts/generate-config.js` if you have not modified it. Otherwise: - * Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: - ```ts - configText += `config.${prop} = process.env.REACT_APP_${constantCase(prop)} || "${ - config[prop]?.trim() - }";\n`; - ``` +- Replace `scripts/generate-config.js` if you have not modified it. Otherwise: -# angular - -* Update Angular and core dependencies to ~17.3.1, related dependencies - -* Update Typescript to ~5.2.2 - -* Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: - * Add a `trim()` call to `config[prop]` (use toString() to avoid type conflicts) and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: - - ```ts - configText += `config.${prop} = process.env.${constantCase(prop)} || "${config[prop]?.toString().trim()}";\n`; - ``` - -* Update import in _src/templates/angular/server.bundle.ts_ - Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ - - ```ts - import 'zone.js'; - ``` -* Update import in _src/templates/angular/src/polyfills.ts_ - Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ + - Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: - ```ts - import 'zone.js'; - ``` - -# vue - -* Replace `scripts/generate-config.js` if you have not modified it. Otherwise: - * Add a `trim()` call to `config[prop]` and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: + ```ts + configText += `config.${prop} = process.env.REACT_APP_${constantCase(prop)} || "${config[ + prop + ]?.trim()}";\n`; + ``` - ```ts - configText += `config.${prop} = process.env.VUE_APP_${constantCase(prop)} || "${ - config[prop]?.trim() - }";\n`; - ``` +# angular -# nextjs +- Update Angular and core dependencies to ~17.3.1, related dependencies -* Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: - * Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: +- Update Typescript to ~5.2.2 - ```ts - configText += `config.${prop} = process.env.${constantCase(prop)} || '${config[prop]?.trim()}';\n`; - ``` +- Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: -* Remove cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares: + - Add a `trim()` call to `config[prop]` (use toString() to avoid type conflicts) and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: ```ts - { - source: '/api/:path*', - headers: [ - { - key: 'Access-Control-Allow-Origin', - value: config.sitecoreApiHost.replace(/\/$/, ''), - }, - ], - }, + configText += `config.${prop} = process.env.${constantCase(prop)} || "${config[prop] + ?.toString() + .trim()}";\n`; ``` -* Update _pages/api/editing/render.ts_ API handler initialization signature, since _resolvePageUrl_ function now accepts an object and _serverUrl_ now is optional, it's ommited when Pages Metadata Edit Mode is used. Update the handler initialization as follows: +- Update import in _src/templates/angular/server.bundle.ts_ + Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ - ```ts - const handler = new EditingRenderMiddleware({ - resolvePageUrl: ({ serverUrl, itemPath }) => `${serverUrl}${itemPath}`, - }).getHandler(); - ``` + ```ts + import 'zone.js'; + ``` -* The implementation of 'EditingComponentPlaceholder' has been removed. Its purpose to avoid refreshing the entire page during component editing in Pages had never been fully utilized. The references to it and to `RenderingType` enum in `[[...path]].tsx` of the nextjs app (and in any custom code) should be removed: +- Update import in _src/templates/angular/src/polyfills.ts_ + Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ - ```ts - import Layout from 'src/Layout'; - import { RenderingType, EditingComponentPlaceholder } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - const isComponentRendering = - layoutData.sitecore.context.renderingType === RenderingType.Component; - ... - {isComponentRendering ? ( - - ) : ( - - )} - ... - ``` + ```ts + import 'zone.js'; + ``` -# nextjs-xmcloud +# vue + +- Replace `scripts/generate-config.js` if you have not modified it. Otherwise: -* Render a new `EditingScripts` component in your `Scripts.ts` file to support a new Editing Integration feature. + - Add a `trim()` call to `config[prop]` and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: ```ts - import { EditingScripts } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - const Scripts = (): JSX.Element | null => ( - <> - - ... - - ); + configText += `config.${prop} = process.env.VUE_APP_${constantCase(prop)} || "${config[ + prop + ]?.trim()}";\n`; ``` -* Add a `useSiteQuery` parameter when `GraphQLDictionaryService` is initialized in `/src/lib/dictionary-service-factory.ts` : - ``` - new GraphQLDictionaryService({ - siteName, - clientFactory, - ..... - useSiteQuery: true, - }) +# nextjs -* We have introduced a new configuration option, `pagesEditMode`, in the `\src\pages\api\editing\config.ts` file to support the new editing metadata architecture for Pages (XMCloud). This option allows you to specify the editing mode used by Pages. It is set to `metadata` by default. However, if you are not ready to use a new integration and continue using the existing architecture, you can explicitly set the `pagesEditMode` to `chromes`. +- Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: - ```ts - import { EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; + - Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: - const handler = new EditingConfigMiddleware({ - ... - pagesEditMode: EditMode.Chromes, - }).getHandler(); + ```ts + configText += `config.${prop} = process.env.${constantCase(prop)} || '${config[ + prop + ]?.trim()}';\n`; ``` -* Introduce a new _lib/graphql-editing-service.ts_ file to initialize a _graphQLEditingService_ to support a new Editing Metadata Mode. Can be done by adding this file from the latest version introduced in _nextjs-xmcloud_ base template. +- Remove cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares: + + ```ts + { + source: '/api/:path*', + headers: [ + { + key: 'Access-Control-Allow-Origin', + value: config.sitecoreApiHost.replace(/\/$/, ''), + }, + ], + }, + ``` + +- Update _pages/api/editing/render.ts_ API handler initialization signature, since _resolvePageUrl_ function now accepts an object and _serverUrl_ now is optional, it's ommited when Pages Metadata Edit Mode is used. Update the handler initialization as follows: + + ```ts + const handler = new EditingRenderMiddleware({ + resolvePageUrl: ({ serverUrl, itemPath }) => `${serverUrl}${itemPath}`, + }).getHandler(); + ``` + +- The implementation of 'EditingComponentPlaceholder' has been removed. Its purpose to avoid refreshing the entire page during component editing in Pages had never been fully utilized. The references to it and to `RenderingType` enum in `[[...path]].tsx` of the nextjs app (and in any custom code) should be removed: + + ```ts + import Layout from 'src/Layout'; + import { RenderingType, EditingComponentPlaceholder } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + const isComponentRendering = + layoutData.sitecore.context.renderingType === RenderingType.Component; + ... + {isComponentRendering ? ( + + ) : ( + + )} + ... + ``` -* Update _lib/page-props-factory/plugins/preview-mode_ plugin to support a new Editing Metadata Mode. Can be done by replacing this file with the latest version introduced in _nextjs-xmcloud_ base template. - -* To support editing for fields in Pages, the new editing metadata architecture relies on the new metadata property 'field.metadata' (instead of on 'field.editable', which won't be used in this scenario). If you are using the new editing arhitecture in Pages (EditMode.Metadata) and have custom field component that manipulates or relies on 'field.editable' in some way, it may need to be reworked. Experience Editor still relies on 'field.editable', so it needs to be supported. See example below from SXA's Banner component: +# nextjs-xmcloud - ```ts - import { useSitecoreContext, EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - export const Banner = (props: ImageProps): JSX.Element => { - const { sitecoreContext } = useSitecoreContext(); - const isMetadataMode = sitecoreContext?.editMode === EditMode.Metadata; - ... - const modifyImageProps = !isMetadataMode - ? { - ...props.fields.Image, - editable: props?.fields?.Image?.editable - ?.replace(`width="${props?.fields?.Image?.value?.width}"`, 'width="100%"') - .replace(`height="${props?.fields?.Image?.value?.height}"`, 'height="100%"'), - } - : { ...props.fields.Image }; - ... - } - ... - ``` +- Render a new `EditingScripts` component in your `Scripts.ts` file to support a new Editing Integration feature. + + ```ts + import { EditingScripts } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + const Scripts = (): JSX.Element | null => ( + <> + + ... + + ); + ``` + +- Add a `useSiteQuery` parameter when `GraphQLDictionaryService` is initialized in `/src/lib/dictionary-service-factory.ts` : + + ``` + new GraphQLDictionaryService({ + siteName, + clientFactory, + ..... + useSiteQuery: true, + }) + + ``` + +- We have introduced a new configuration option, `pagesEditMode`, in the `\src\pages\api\editing\config.ts` file to support the new editing metadata architecture for Pages (XMCloud). This option allows you to specify the editing mode used by Pages. It is set to `metadata` by default. However, if you are not ready to use a new integration and continue using the existing architecture, you can explicitly set the `pagesEditMode` to `chromes`. + + ```ts + import { EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; + + const handler = new EditingConfigMiddleware({ + ... + pagesEditMode: EditMode.Chromes, + }).getHandler(); + ``` + +- Introduce a new _lib/graphql-editing-service.ts_ file to initialize a _graphQLEditingService_ to support a new Editing Metadata Mode. Can be done by adding this file from the latest version introduced in _nextjs-xmcloud_ base template. + +- Update _lib/page-props-factory/plugins/preview-mode_ plugin to support a new Editing Metadata Mode. Can be done by replacing this file with the latest version introduced in _nextjs-xmcloud_ base template. + +- To support editing for fields in Pages, the new editing metadata architecture relies on the new metadata property 'field.metadata' (instead of on 'field.editable', which won't be used in this scenario). If you are using the new editing arhitecture in Pages (EditMode.Metadata) and have custom field component that manipulates or relies on 'field.editable' in some way, it may need to be reworked. Experience Editor still relies on 'field.editable', so it needs to be supported. See example below from SXA's Banner component: + + ```ts + import { useSitecoreContext, EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + export const Banner = (props: ImageProps): JSX.Element => { + const { sitecoreContext } = useSitecoreContext(); + const isMetadataMode = sitecoreContext?.editMode === EditMode.Metadata; + ... + const modifyImageProps = !isMetadataMode + ? { + ...props.fields.Image, + editable: props?.fields?.Image?.editable + ?.replace(`width="${props?.fields?.Image?.value?.width}"`, 'width="100%"') + .replace(`height="${props?.fields?.Image?.value?.height}"`, 'height="100%"'), + } + : { ...props.fields.Image }; + ... + } + ... + ``` + +- Jss now supports component level personalization for XM Cloud. See below code from personalize.ts. _personalizeData_ contains _componentVariantIds_ property which has the personalized components. It can be passed to the _personalizeLayout_ function to apply the personalization to the _layoutData_: + + ```ts + // Get variant(s) for personalization (from path) + const personalizeData = getPersonalizedRewriteData(path); + + // Modify layoutData to use specific variant(s) instead of default + // This will also set the variantId on the Sitecore context so that it is accessible here + personalizeLayout( + props.layoutData, + personalizeData.variantId, + personalizeData.componentVariantIds + ); + ``` From 40da58873ac2c11566076c2edfd35fb591144926 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 22 Jul 2024 15:58:30 +0300 Subject: [PATCH 11/14] Revert "add upgrade guide entry" This reverts commit c18c529b9d3a9545e69e08a3921e869345b0582f. --- docs/upgrades/unreleased.md | 314 ++++++++++++++++-------------------- 1 file changed, 143 insertions(+), 171 deletions(-) diff --git a/docs/upgrades/unreleased.md b/docs/upgrades/unreleased.md index 2c382155a3..736e73ce80 100644 --- a/docs/upgrades/unreleased.md +++ b/docs/upgrades/unreleased.md @@ -1,207 +1,179 @@ ## Unreleased -- If you are importing any _editing_ utils from `@sitecore-jss/sitecore-jss/utils` in your code, please update the import path to `@sitecore-jss/sitecore-jss/editing`. For now these exports are still available in the old path and marked as deprecated. They will be removed in the next major version release. Specifically for the following utils: - - ExperienceEditor - - HorizonEditor - - isEditorActive - - resetEditorChromes - - handleEditorAnchors - - Metadata - - DefaultEditFrameButton - - DefaultEditFrameButtons - - DefaultEditFrameButtonIds - - EditFrameDataSource - - ChromeCommand - - FieldEditButton - - WebEditButton - - EditButtonTypes - - mapButtonToCommand +* If you are importing any _editing_ utils from `@sitecore-jss/sitecore-jss/utils` in your code, please update the import path to `@sitecore-jss/sitecore-jss/editing`. For now these exports are still available in the old path and marked as deprecated. They will be removed in the next major version release. Specifically for the following utils: + * ExperienceEditor + * HorizonEditor + * isEditorActive + * resetEditorChromes + * handleEditorAnchors + * Metadata + * DefaultEditFrameButton + * DefaultEditFrameButtons + * DefaultEditFrameButtonIds + * EditFrameDataSource + * ChromeCommand + * FieldEditButton + * WebEditButton + * EditButtonTypes + * mapButtonToCommand # react -- With the simplification of Editing Support work we have added the following breaking changes to the `sitecore-jss-react` package. Please make the necessary updates. +* With the simplification of Editing Support work we have added the following breaking changes to the `sitecore-jss-react` package. Please make the necessary updates. - `ComponentConsumerProps` is removed. You might need to reuse _WithSitecoreContextProps_ type. ### headless-ssr-experience-edge +* Replace `scripts/generate-config.js` if you have not modified it. Otherwise: + * Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: -- Replace `scripts/generate-config.js` if you have not modified it. Otherwise: - - - Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: - - ```ts - configText += `config.${prop} = process.env.REACT_APP_${constantCase(prop)} || "${config[ - prop - ]?.trim()}";\n`; - ``` + ```ts + configText += `config.${prop} = process.env.REACT_APP_${constantCase(prop)} || "${ + config[prop]?.trim() + }";\n`; + ``` # angular -- Update Angular and core dependencies to ~17.3.1, related dependencies +* Update Angular and core dependencies to ~17.3.1, related dependencies -- Update Typescript to ~5.2.2 +* Update Typescript to ~5.2.2 -- Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: +* Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: + * Add a `trim()` call to `config[prop]` (use toString() to avoid type conflicts) and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: - - Add a `trim()` call to `config[prop]` (use toString() to avoid type conflicts) and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: + ```ts + configText += `config.${prop} = process.env.${constantCase(prop)} || "${config[prop]?.toString().trim()}";\n`; + ``` - ```ts - configText += `config.${prop} = process.env.${constantCase(prop)} || "${config[prop] - ?.toString() - .trim()}";\n`; - ``` - -- Update import in _src/templates/angular/server.bundle.ts_ +* Update import in _src/templates/angular/server.bundle.ts_ Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ - ```ts - import 'zone.js'; - ``` - -- Update import in _src/templates/angular/src/polyfills.ts_ + ```ts + import 'zone.js'; + ``` +* Update import in _src/templates/angular/src/polyfills.ts_ Use _'zone.js'_ instead of _'zone.js/dist/zone-node'_ - ```ts - import 'zone.js'; - ``` + ```ts + import 'zone.js'; + ``` # vue -- Replace `scripts/generate-config.js` if you have not modified it. Otherwise: +* Replace `scripts/generate-config.js` if you have not modified it. Otherwise: + * Add a `trim()` call to `config[prop]` and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: - - Add a `trim()` call to `config[prop]` and replace commas before a newline (`,`) with semicolon (`;`) in configText prop assignments so it would look like this: + ```ts + configText += `config.${prop} = process.env.VUE_APP_${constantCase(prop)} || "${ + config[prop]?.trim() + }";\n`; + ``` + +# nextjs + +* Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: + * Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: + + ```ts + configText += `config.${prop} = process.env.${constantCase(prop)} || '${config[prop]?.trim()}';\n`; + ``` + +* Remove cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares: ```ts - configText += `config.${prop} = process.env.VUE_APP_${constantCase(prop)} || "${config[ - prop - ]?.trim()}";\n`; + { + source: '/api/:path*', + headers: [ + { + key: 'Access-Control-Allow-Origin', + value: config.sitecoreApiHost.replace(/\/$/, ''), + }, + ], + }, ``` -# nextjs +* Update _pages/api/editing/render.ts_ API handler initialization signature, since _resolvePageUrl_ function now accepts an object and _serverUrl_ now is optional, it's ommited when Pages Metadata Edit Mode is used. Update the handler initialization as follows: -- Replace `scripts/generate-config.ts` if you have not modified it. Otherwise: + ```ts + const handler = new EditingRenderMiddleware({ + resolvePageUrl: ({ serverUrl, itemPath }) => `${serverUrl}${itemPath}`, + }).getHandler(); + ``` - - Add a `trim()` call to `config[prop]` and replace comma before a newline (`,`) with semicolon (`;`) in configText prop assignment so it would look like this: +* The implementation of 'EditingComponentPlaceholder' has been removed. Its purpose to avoid refreshing the entire page during component editing in Pages had never been fully utilized. The references to it and to `RenderingType` enum in `[[...path]].tsx` of the nextjs app (and in any custom code) should be removed: ```ts - configText += `config.${prop} = process.env.${constantCase(prop)} || '${config[ - prop - ]?.trim()}';\n`; + import Layout from 'src/Layout'; + import { RenderingType, EditingComponentPlaceholder } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + const isComponentRendering = + layoutData.sitecore.context.renderingType === RenderingType.Component; + ... + {isComponentRendering ? ( + + ) : ( + + )} + ... ``` -- Remove cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares: - - ```ts - { - source: '/api/:path*', - headers: [ - { - key: 'Access-Control-Allow-Origin', - value: config.sitecoreApiHost.replace(/\/$/, ''), - }, - ], - }, - ``` - -- Update _pages/api/editing/render.ts_ API handler initialization signature, since _resolvePageUrl_ function now accepts an object and _serverUrl_ now is optional, it's ommited when Pages Metadata Edit Mode is used. Update the handler initialization as follows: - - ```ts - const handler = new EditingRenderMiddleware({ - resolvePageUrl: ({ serverUrl, itemPath }) => `${serverUrl}${itemPath}`, - }).getHandler(); - ``` - -- The implementation of 'EditingComponentPlaceholder' has been removed. Its purpose to avoid refreshing the entire page during component editing in Pages had never been fully utilized. The references to it and to `RenderingType` enum in `[[...path]].tsx` of the nextjs app (and in any custom code) should be removed: - - ```ts - import Layout from 'src/Layout'; - import { RenderingType, EditingComponentPlaceholder } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - const isComponentRendering = - layoutData.sitecore.context.renderingType === RenderingType.Component; - ... - {isComponentRendering ? ( - - ) : ( - - )} - ... - ``` - # nextjs-xmcloud -- Render a new `EditingScripts` component in your `Scripts.ts` file to support a new Editing Integration feature. - - ```ts - import { EditingScripts } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - const Scripts = (): JSX.Element | null => ( - <> - - ... - - ); - ``` - -- Add a `useSiteQuery` parameter when `GraphQLDictionaryService` is initialized in `/src/lib/dictionary-service-factory.ts` : - - ``` - new GraphQLDictionaryService({ - siteName, - clientFactory, - ..... - useSiteQuery: true, - }) - - ``` - -- We have introduced a new configuration option, `pagesEditMode`, in the `\src\pages\api\editing\config.ts` file to support the new editing metadata architecture for Pages (XMCloud). This option allows you to specify the editing mode used by Pages. It is set to `metadata` by default. However, if you are not ready to use a new integration and continue using the existing architecture, you can explicitly set the `pagesEditMode` to `chromes`. - - ```ts - import { EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; - - const handler = new EditingConfigMiddleware({ - ... - pagesEditMode: EditMode.Chromes, - }).getHandler(); - ``` - -- Introduce a new _lib/graphql-editing-service.ts_ file to initialize a _graphQLEditingService_ to support a new Editing Metadata Mode. Can be done by adding this file from the latest version introduced in _nextjs-xmcloud_ base template. - -- Update _lib/page-props-factory/plugins/preview-mode_ plugin to support a new Editing Metadata Mode. Can be done by replacing this file with the latest version introduced in _nextjs-xmcloud_ base template. - -- To support editing for fields in Pages, the new editing metadata architecture relies on the new metadata property 'field.metadata' (instead of on 'field.editable', which won't be used in this scenario). If you are using the new editing arhitecture in Pages (EditMode.Metadata) and have custom field component that manipulates or relies on 'field.editable' in some way, it may need to be reworked. Experience Editor still relies on 'field.editable', so it needs to be supported. See example below from SXA's Banner component: - - ```ts - import { useSitecoreContext, EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; - ... - export const Banner = (props: ImageProps): JSX.Element => { - const { sitecoreContext } = useSitecoreContext(); - const isMetadataMode = sitecoreContext?.editMode === EditMode.Metadata; - ... - const modifyImageProps = !isMetadataMode - ? { - ...props.fields.Image, - editable: props?.fields?.Image?.editable - ?.replace(`width="${props?.fields?.Image?.value?.width}"`, 'width="100%"') - .replace(`height="${props?.fields?.Image?.value?.height}"`, 'height="100%"'), - } - : { ...props.fields.Image }; - ... - } - ... - ``` - -- Jss now supports component level personalization for XM Cloud. See below code from personalize.ts. _personalizeData_ contains _componentVariantIds_ property which has the personalized components. It can be passed to the _personalizeLayout_ function to apply the personalization to the _layoutData_: - - ```ts - // Get variant(s) for personalization (from path) - const personalizeData = getPersonalizedRewriteData(path); - - // Modify layoutData to use specific variant(s) instead of default - // This will also set the variantId on the Sitecore context so that it is accessible here - personalizeLayout( - props.layoutData, - personalizeData.variantId, - personalizeData.componentVariantIds - ); - ``` +* Render a new `EditingScripts` component in your `Scripts.ts` file to support a new Editing Integration feature. + + ```ts + import { EditingScripts } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + const Scripts = (): JSX.Element | null => ( + <> + + ... + + ); + ``` + +* Add a `useSiteQuery` parameter when `GraphQLDictionaryService` is initialized in `/src/lib/dictionary-service-factory.ts` : + ``` + new GraphQLDictionaryService({ + siteName, + clientFactory, + ..... + useSiteQuery: true, + }) + +* We have introduced a new configuration option, `pagesEditMode`, in the `\src\pages\api\editing\config.ts` file to support the new editing metadata architecture for Pages (XMCloud). This option allows you to specify the editing mode used by Pages. It is set to `metadata` by default. However, if you are not ready to use a new integration and continue using the existing architecture, you can explicitly set the `pagesEditMode` to `chromes`. + + ```ts + import { EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; + + const handler = new EditingConfigMiddleware({ + ... + pagesEditMode: EditMode.Chromes, + }).getHandler(); + ``` + +* Introduce a new _lib/graphql-editing-service.ts_ file to initialize a _graphQLEditingService_ to support a new Editing Metadata Mode. Can be done by adding this file from the latest version introduced in _nextjs-xmcloud_ base template. + +* Update _lib/page-props-factory/plugins/preview-mode_ plugin to support a new Editing Metadata Mode. Can be done by replacing this file with the latest version introduced in _nextjs-xmcloud_ base template. + +* To support editing for fields in Pages, the new editing metadata architecture relies on the new metadata property 'field.metadata' (instead of on 'field.editable', which won't be used in this scenario). If you are using the new editing arhitecture in Pages (EditMode.Metadata) and have custom field component that manipulates or relies on 'field.editable' in some way, it may need to be reworked. Experience Editor still relies on 'field.editable', so it needs to be supported. See example below from SXA's Banner component: + + ```ts + import { useSitecoreContext, EditMode } from '@sitecore-jss/sitecore-jss-nextjs'; + ... + export const Banner = (props: ImageProps): JSX.Element => { + const { sitecoreContext } = useSitecoreContext(); + const isMetadataMode = sitecoreContext?.editMode === EditMode.Metadata; + ... + const modifyImageProps = !isMetadataMode + ? { + ...props.fields.Image, + editable: props?.fields?.Image?.editable + ?.replace(`width="${props?.fields?.Image?.value?.width}"`, 'width="100%"') + .replace(`height="${props?.fields?.Image?.value?.height}"`, 'height="100%"'), + } + : { ...props.fields.Image }; + ... + } + ... + ``` From c0c1e006b3dbe7aa980f38be916aee6ca51e026f Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 22 Jul 2024 16:00:46 +0300 Subject: [PATCH 12/14] update upgrade entry --- docs/upgrades/unreleased.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/upgrades/unreleased.md b/docs/upgrades/unreleased.md index 736e73ce80..a2dde5674e 100644 --- a/docs/upgrades/unreleased.md +++ b/docs/upgrades/unreleased.md @@ -177,3 +177,18 @@ } ... ``` + +* To enable AB testing and component level personalization support in JSS, ensure componentVariantIds are passed to personalizeLayout: + + ```ts + // Get variant(s) for personalization (from path) + const personalizeData = getPersonalizedRewriteData(path); + + // Modify layoutData to use specific variant(s) instead of default + // This will also set the variantId on the Sitecore context so that it is accessible here + personalizeLayout( + props.layoutData, + personalizeData.variantId, + personalizeData.componentVariantIds + ); + ``` \ No newline at end of file From 5167234e2e06e5d69e0dd27e5220e30231111d1f Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 22 Jul 2024 09:20:04 -0400 Subject: [PATCH 13/14] minor formatting --- docs/upgrades/unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrades/unreleased.md b/docs/upgrades/unreleased.md index a2dde5674e..2b28d6db3c 100644 --- a/docs/upgrades/unreleased.md +++ b/docs/upgrades/unreleased.md @@ -178,7 +178,7 @@ ... ``` -* To enable AB testing and component level personalization support in JSS, ensure componentVariantIds are passed to personalizeLayout: +* To enable AB testing and component level personalization support in JSS, ensure _componentVariantIds_ are passed to _personalizeLayout_ function call: ```ts // Get variant(s) for personalization (from path) From 21baf2025f9fd4d1131bf152aa706374b21bbd64 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 22 Jul 2024 16:46:11 +0300 Subject: [PATCH 14/14] changelog update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03c0e43c2..961dad7cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Our versioning strategy is as follows: * `[sitecore-jss]` _GraphQLRequestClient_ now can accept custom 'headers' in the constructor or via _createClientFactory_ ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[templates/nextjs]` Removed cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[sitecore-jss-nextjs]` Updates to Next.js editing integration to further support secure hosting scenarios (on XM Cloud & Vercel) ([#1832](https://github.com/Sitecore/jss/pull/1832)) -* `[templates/nextjs-xmcloud]` `[sitecore-jss]` Updates the render process in normal mode for component level personalization. ([#1844](https://github.com/Sitecore/jss/pull/1844)) +* `[templates/nextjs-xmcloud]` `[sitecore-jss]` AB testing and componente level personalization support. ([#1844](https://github.com/Sitecore/jss/pull/1844)) * `[sitecore-jss]` `[nextjs-xmcloud]` DictionaryService can now use a `site` GraphQL query instead of `search` one to improve performance. This is currently only available for XMCloud deployments and is enabled with `nextjs-xmcloud` add-on by default ([#1804](https://github.com/Sitecore/jss/pull/1804))([#1846](https://github.com/Sitecore/jss/pull/1846))([commit](https://github.com/Sitecore/jss/commit/5813a2df8ad6a9ee63dd74d5f206ed4b4f758753))([commit](https://github.com/Sitecore/jss/commit/d0ea3ac02c78343b5dd60277dbf7403410794a49))([commit](https://github.com/Sitecore/jss/commit/307b905ed60d7fff44b2dc799fd78c0842af6fbd))([commit](https://github.com/Sitecore/jss/commit/66164a42263aac8b55f0c5e47eda4bd4d7a72e87)) * `[templates/nextjs-sxa]` nextjs-sxa components now use the NextImage component instead of the react Image component from JSS lib for image optimization ([#1843](https://github.com/Sitecore/jss/pull/1843))