Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Initializer] Personalize Initializer Add-on #939

Merged
merged 17 commits into from
Mar 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=
ambrauer marked this conversation as resolved.
Show resolved Hide resolved
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