Skip to content

Commit

Permalink
[Initializer] Personalize Initializer Add-on (#939)
Browse files Browse the repository at this point in the history
* added the nextjs-personalize poc as it is to the tempaltes

* removed common files

* moved public keys to env file

* added graphql files

* removed component-props, synced with up the code plugin architecture

* corrected imports, added env variables

* added yarn.lock

* removed styleguide components, updated env

* removed redundant files

* added types back to personalize intializer add-on

* removed evn variables

* removed redendant files, added personalize plugin to page-props

* removed graphql components, added normalMode plugin, removed package.json

* added experiences type to personalize add on

* added components props to personalize intializer

* renamed and modified component-props file
  • Loading branch information
addy-pathania authored Mar 4, 2022
1 parent 5237dc7 commit 996d72e
Show file tree
Hide file tree
Showing 11 changed files with 596 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import path, { sep } from 'path';
import {
Initializer,
openPackageJson,
transform,
DEFAULT_APPNAME,
ClientAppArgs,
} from '../../common';

export default class NextjsPersonalizeInitializer implements Initializer {
get isBase(): boolean {
return false;
}

async init(args: ClientAppArgs) {
const pkg = openPackageJson(`${args.destination}${sep}package.json`);

// TODO: prompts for Personalize and argument types
// const answers = await prompt<StyleguideAnswer>(styleguidePrompts, args);

const mergedArgs = {
...args,
appName: args.appName || pkg?.config?.appName || DEFAULT_APPNAME,
appPrefix: args.appPrefix || pkg?.config?.prefix || false,
};

const templatePath = path.resolve(__dirname, '../../templates/nextjs-personalize');

await transform(templatePath, mergedArgs);

const response = {
nextSteps: [],
appName: mergedArgs.appName,
};

return response;
}
}
5 changes: 5 additions & 0 deletions packages/create-sitecore-jss/src/initializers/nextjs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export default class NextjsInitializer implements Initializer {
name: 'nextjs-sxa - Includes example components and setup for working using SXA',
value: 'nextjs-sxa',
},
{
name:
'nextjs-personalize - Includes example components and setup for working using Personalize',
value: 'nextjs-personalize',
},
],
});
addInitializers = addInitAnswer.addInitializers;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BOXEVER_CLIENT_KEY=
BOXEVER_API=
BOXEVER_TARGET_URL=
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RouteData } from '@sitecore-jss/sitecore-jss/layout';
import Script from 'next/script';
import { useEffect } from 'react';

declare const _boxeverq: any;
declare const Boxever: any;

function createPageView(locale: string | undefined, routeName: string) {
// POS must be valid in order to save events (domain name might be taken but it must be defined in CDP settings)
const pos = 'spintel.com';

_boxeverq.push(function () {
const pageViewEvent = {
browser_id: Boxever.getID(),
channel: 'WEB',
type: 'VIEW',
language: locale,
page: routeName,
pos: pos,
};

// eslint-disable-next-line @typescript-eslint/no-empty-function
Boxever.eventCreate(pageViewEvent, function () {}, 'json');
});
}

interface CdpIntegrationProps {
pageEditing: boolean | undefined;
route: RouteData;
}

const CdpIntegrationScript = ({
route: { itemLanguage, name },
pageEditing,
}: CdpIntegrationProps): JSX.Element => {
const clientKey = process.env.BOXEVER_CLIENT_KEY
const targetUrl = process.env.BOXEVER_TARGET_URL

useEffect(() => {
// Do not create events in editing mode
if (pageEditing) {
return;
}

createPageView(itemLanguage, name);
}, []);

// Boxever is not needed during page editing
if (pageEditing) {
return null as any;
}

return (
<>
<Script
id="cdp_settings"
type="text/javascript"
dangerouslySetInnerHTML={{
__html: `
var _boxeverq = _boxeverq || [];
var _boxever_settings = {
client_key: '${clientKey}',
target: '${targetUrl}',
cookie_domain: ''
};
`,
}}
/>
<Script src="https://d1mj578wat5n4o.cloudfront.net/boxever-1.4.8.min.js" />
</>
);
};

export default CdpIntegrationScript;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {
ComponentRendering,
} from '@sitecore-jss/sitecore-jss-nextjs';

// NULL means Hidden by this experience
export type ComponentRenderingWithExpiriences = ComponentRendering & {
experiences: { [name: string]: ComponentRenderingWithExpiriences | null };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { LayoutServiceData } from '@sitecore-jss/sitecore-jss/layout';
import { ComponentRendering, HtmlElementRendering } from '@sitecore-jss/sitecore-jss-nextjs';
import type { ComponentRenderingWithExpiriences } from './component-props';

// recursive go through all placeholders/components and check expirinces node, replace default with object from specific experience
export function personalizeLayout(layout: LayoutServiceData, segment: string): void {
const placeholders = layout.sitecore.route?.placeholders;
if (!placeholders) {
return;
}
Object.keys(placeholders).forEach((placeholder) => {
placeholders[placeholder] = personalizePlaceholder(placeholders[placeholder], segment);
});
}

function personalizePlaceholder(
components: Array<ComponentRendering | HtmlElementRendering>,
segment: string
): Array<ComponentRendering | HtmlElementRendering> {
const newComponents = new Array<ComponentRendering | HtmlElementRendering>();
for (let i = 0; i < components.length; i++) {
if ((<ComponentRenderingWithExpiriences>components[i]).experiences !== undefined) {
const personalizedComponent = personalizeComponent(
<ComponentRenderingWithExpiriences>components[i],
segment
);
if (personalizedComponent) {
newComponents.push(personalizedComponent);
}
} else {
newComponents.push(components[i]);
}
}
return newComponents;
}

function personalizeComponent(
component: ComponentRenderingWithExpiriences,
segment: string
): ComponentRendering | null {
const segmentVariant = component.experiences[segment];
if (segmentVariant === null) {
// HIDDEN
return null;
} else if (segmentVariant === undefined && component.componentName === undefined) {
// DEFAULT IS HIDDEN
return null;
} else if (segmentVariant) {
component = segmentVariant;
}

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

return component;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ParsedUrlQuery } from 'querystring';
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { DictionaryService, LayoutService } from '@sitecore-jss/sitecore-jss-nextjs';
import { dictionaryServiceFactory } from 'lib/dictionary-service-factory';
import { layoutServiceFactory } from 'lib/layout-service-factory';
import { SitecorePageProps } from 'lib/page-props';
import { Plugin, isServerSidePropsContext } from '..';
import pkg from '../../../../package.json';

/**
* Extract normalized Sitecore item path from query
* @param {ParsedUrlQuery | undefined} params
*/
function extractPath(params: ParsedUrlQuery | undefined): string {
if (params === undefined) {
return '/';
}
let path = Array.isArray(params.path) ? params.path.join('/') : params.path ?? '/';

// Ensure leading '/'
if (!path.startsWith('/')) {
path = '/' + path;
}

// Remove SegmentId part from path, otherwise layout service will not find layout data
if (path.includes('_segmentId_')) {
const result = path.match('_segmentId_.*?\\/');
path = result === null ? '/' : path.replace(result[0], '');
}

return path;
}

class NormalModePlugin implements Plugin {
private dictionaryService: DictionaryService;
private layoutService: LayoutService;

order = 0;

constructor() {
this.dictionaryService = dictionaryServiceFactory.create();
this.layoutService = layoutServiceFactory.create();
}

async exec(props: SitecorePageProps, context: GetServerSidePropsContext | GetStaticPropsContext) {
if (context.preview) return props;

/**
* Normal mode
*/
// Get normalized Sitecore item path
const path = extractPath(context.params);

// Use context locale if Next.js i18n is configured, otherwise use language defined in package.json
props.locale = context.locale ?? pkg.config.language;

// Fetch layout data, passing on req/res for SSR
props.layoutData = await this.layoutService.fetchLayoutData(
path,
props.locale,
// eslint-disable-next-line prettier/prettier
isServerSidePropsContext(context) ? (context as GetServerSidePropsContext).req : undefined,
isServerSidePropsContext(context) ? (context as GetServerSidePropsContext).res : undefined
);

if (!props.layoutData.sitecore.route) {
// A missing route value signifies an invalid path, so set notFound.
// Our page routes will return this in getStatic/ServerSideProps,
// which will trigger our custom 404 page with proper 404 status code.
// You could perform additional logging here to track these if desired.
props.notFound = true;
}

// Fetch dictionary data
props.dictionary = await this.dictionaryService.fetchDictionaryData(props.locale);

return props;
}
}

export const normalModePlugin = new NormalModePlugin();
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { Plugin } from '..';
import { personalizeLayout } from 'lib/layout-personalizer';
import { SitecorePageProps } from 'lib/page-props';

class PersonalizePlugin implements Plugin {
order = 2;

async exec(props: SitecorePageProps, context: GetServerSidePropsContext | GetStaticPropsContext) {

// Get segment for personalization (from path)
let filtered = null;
if (context !== null) {
// temporary disable null assertion
if (Array.isArray(context!.params!.path)) {
filtered = context!.params!.path.filter((e) => e.includes('_segmentId_'));
}
}

const segment =
filtered === null || filtered.length == 0
? '_default'
: filtered[0].replace('_segmentId_', '');

// modify layoutData to use specific segment instead of default
personalizeLayout(props.layoutData, segment);

return props;
}
}

export const personalizePlugin = new PersonalizePlugin();
Loading

0 comments on commit 996d72e

Please sign in to comment.