Skip to content

Commit

Permalink
[Nextjs][Personalize] Productize layout response transform (#962)
Browse files Browse the repository at this point in the history
* created submodule for layout-personalizer

* added unit tests

* fixed nextjs middleware

* added test data to diff folder, added doc comments

* changed test data variable names

* fixed review comments, added more test coverage

* fixed review comments

* updated jsDoc, refactored personalizePlaceholder function
  • Loading branch information
addy-pathania authored Apr 1, 2022
1 parent 42c73e2 commit f43645f
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 76 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { Plugin } from '..';
import { personalizeLayout } from 'lib/layout-personalizer';
import { personalizeLayout } from '@sitecore-jss/sitecore-jss-nextjs';
import { SitecorePageProps } from 'lib/page-props';

class PersonalizePlugin implements Plugin {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server';
import type { NextRequest, NextFetchEvent } from 'next/server';
import middleware from 'lib/middleware';

// eslint-disable-next-line
export default async function (req: NextRequest) {
return middleware(req);
export default async function (req: NextRequest, ev: NextFetchEvent) {
return middleware(req, ev);
}
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export {
RestDictionaryService,
RestDictionaryServiceConfig,
} from '@sitecore-jss/sitecore-jss/i18n';
export { personalizeLayout } from '@sitecore-jss/sitecore-jss/personalize';
export {
RobotsQueryResult,
GraphQLRobotsService,
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss/personalize.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/personalize/index';
1 change: 1 addition & 0 deletions packages/sitecore-jss/personalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/cjs/personalize/index');
1 change: 1 addition & 0 deletions packages/sitecore-jss/src/personalize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { personalizeLayout } from './layout-personalizer';
68 changes: 68 additions & 0 deletions packages/sitecore-jss/src/personalize/layout-personalizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect } from 'chai';
import * as personalize from './layout-personalizer';
import { ComponentRenderingWithExperiences } from './layout-personalizer';
import {
layoutData,
componentsArray,
component,
componentsWithExperiencesArray,
componentWithExperiences,
layoutDataWithoutPlaceholder,
withoutComponentName,
segmentIsNull,
} from '../test-data/personalizeData';

const { personalizeLayout, personalizePlaceholder, personalizeComponent } = personalize;

describe('layout-personalizer', () => {
describe('personalizeLayout', () => {
it('should not return anything', () => {
const segment = 'test';
const personalizedLayoutResult = personalizeLayout(layoutData, segment);
expect(personalizedLayoutResult).to.equal(undefined);
});

it('should return undefined if no placeholders', () => {
const segment = 'test';
const personalizedLayoutResult = personalizeLayout(layoutDataWithoutPlaceholder, segment);
expect(personalizedLayoutResult).to.equal(undefined);
});
});

describe('personalizePlaceholder', () => {
it('should return array of personalized components', () => {
const segment = 'mountain_bike_audience';
const personalizedPlaceholderResult = personalizePlaceholder(componentsArray, segment);
expect(personalizedPlaceholderResult).to.deep.equal(componentsWithExperiencesArray);
});
});

describe('personalizeComponent', () => {
it('should return personalized component', () => {
const segment = 'mountain_bike_audience';
const personalizedComponentResult = personalizeComponent(
(component as unknown) as ComponentRenderingWithExperiences,
segment
);
expect(personalizedComponentResult).to.deep.equal(componentWithExperiences);
});

it('should return null when segmentVariant is null', () => {
const segment = 'mountain_bike_audience';
const personalizedComponentResult = personalizeComponent(
(segmentIsNull as unknown) as ComponentRenderingWithExperiences,
segment
);
expect(personalizedComponentResult).to.equal(null);
});

it('should return null when segmentVariant and componentName is undefined', () => {
const segment = 'test';
const personalizedComponentResult = personalizeComponent(
(withoutComponentName as unknown) as ComponentRenderingWithExperiences,
segment
);
expect(personalizedComponentResult).to.equal(null);
});
});
});
76 changes: 76 additions & 0 deletions packages/sitecore-jss/src/personalize/layout-personalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { LayoutServiceData, ComponentRendering, HtmlElementRendering } from './../layout/models';

// NULL means Hidden by this experience
export type ComponentRenderingWithExperiences = ComponentRendering & {
experiences: { [name: string]: ComponentRenderingWithExperiences | null };
};

/**
* 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} segment segmentId
*/
export function personalizeLayout(layout: LayoutServiceData, segment: string): void {
const placeholders = layout.sitecore.route?.placeholders;
if (Object.keys(placeholders ?? {}).length === 0) {
return;
}
if (placeholders) {
Object.keys(placeholders).forEach((placeholder) => {
placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], segment);
});
}
}

/**
* @param {Array} components components within placeholder
* @param {string} segment segmentId
*/
export function personalizePlaceholder(
components: Array<ComponentRendering | HtmlElementRendering>,
segment: string
): Array<ComponentRendering | HtmlElementRendering> {
return components
.map((component) =>
(component as ComponentRenderingWithExperiences).experiences !== undefined
? (personalizeComponent(component as ComponentRenderingWithExperiences, segment) as
| ComponentRendering
| HtmlElementRendering)
: component
)
.filter(Boolean);
}

/**
* @param {ComponentRenderingWithExperiences} component component with experiences
* @param {string} segment segmentId
*/
export function personalizeComponent(
component: ComponentRenderingWithExperiences,
segment: string
): ComponentRendering | null {
const segmentVariant = component.experiences[segment];
if (segmentVariant === undefined && component.componentName === undefined) {
// DEFAULT IS HIDDEN
return null;
} else if (Object.keys(segmentVariant ?? {}).length === 0) {
// HIDDEN
return null;
} else if (segmentVariant) {
component = segmentVariant;
}

if (!component.placeholders) return component;

Object.keys(component?.placeholders).forEach((placeholder) => {
if (component.placeholders) {
component.placeholders[placeholder] = personalizePlaceholder(
component.placeholders[placeholder],
segment
);
}
});

return component;
}
111 changes: 111 additions & 0 deletions packages/sitecore-jss/src/test-data/personalizeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const mountain_bike_audience = {
uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0',
componentName: 'ContentBlock',
dataSource: '20679cd4-356b-4452-b507-453beeb0be39',
fields: {
content: {
value:
'<p><img src="https://edge-beta.sitecorecloud.io/ser-edge-personalization/media/JssNextWeb/Mountain-Bike.jpg?h=675&amp;w=1200" style="width:1200px;height:675px;" /></p>',
},
heading: { value: 'Mountain Bike' },
},
};

const city_bike_audience = {
uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0',
componentName: 'ContentBlock',
dataSource: '36e02581-2056-4c55-a4d5-f4b700ba1ae2',
fields: {
content: {
value:
'<p><img src="https://edge-beta.sitecorecloud.io/ser-edge-personalization/media/JssNextWeb/Mountain-Bike.jpg?h=675&amp;w=1200" style="width:1200px;height:675px;" /></p>',
},
heading: { value: 'Mountain Bike' },
},
};

export const layoutData = {
sitecore: {
context: {
pageEditing: false,
site: { name: 'JssNextWeb' },
visitorIdentificationTimestamp: 1038543,
language: 'en',
},
route: {
name: 'landingpage',
placeholders: {
'jss-main': [
{
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,
city_bike_audience: city_bike_audience,
},
},
],
},
},
},
};

export const layoutDataWithoutPlaceholder = {
sitecore: {
context: {
pageEditing: false,
site: { name: 'JssNextWeb' },
visitorIdentificationTimestamp: 1038543,
language: 'en',
},
route: {
name: 'landingpage',
placeholders: {},
},
},
};

export const componentWithExperiences = {
uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0',
componentName: 'ContentBlock',
dataSource: '20679cd4-356b-4452-b507-453beeb0be39',
fields: mountain_bike_audience.fields,
};

export const componentsWithExperiencesArray = [componentWithExperiences];

export const component = {
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,
city_bike_audience: city_bike_audience,
},
};

export const componentsArray = [component];

export const withoutComponentName = {
uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0',
componentName: undefined,
dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2',
fields: { content: { value: '' }, heading: { value: 'Default Content' } },
experiences: {
mountain_bike_audience: mountain_bike_audience,
city_bike_audience: city_bike_audience,
},
};

export const segmentIsNull = {
uid: '0b6d23d8-c50e-4e79-9eca-317ec43e82b0',
componentName: undefined,
dataSource: 'e020fb58-1be8-4537-aab8-67916452ecf2',
fields: { content: { value: '' }, heading: { value: 'Default Content' } },
experiences: {
mountain_bike_audience: {},
},
};
1 change: 1 addition & 0 deletions packages/sitecore-jss/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"i18n.d.ts",
"tracking.d.ts",
"site.d.ts",
"personalize.d.ts",
"src/**/*.test.ts",
"src/testData/*",
]
Expand Down

0 comments on commit f43645f

Please sign in to comment.