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

[Next.js] Personalize Middleware #1008

Merged
merged 45 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
00c9543
Use config values
ambrauer Mar 18, 2022
548a75f
WIP
ambrauer Apr 4, 2022
d0d89b7
Progress on sitecore-jss/personalize service layer and sitecore-jss-n…
ambrauer Apr 14, 2022
690d51b
Merge branch 'dev' into feature/517648-personalize-middleware
ambrauer Apr 14, 2022
4918df2
use native fetch override
ambrauer Apr 18, 2022
922ae1a
Merge branch 'dev' into feature/517648-personalize-middleware
ambrauer Apr 18, 2022
31b9f6f
enable fetch config pass-through
ambrauer Apr 19, 2022
818dc88
Introduce personalize utils for rewrite url path de/construction
ambrauer Apr 19, 2022
6fb233b
Replace nextjs-personalize plugin code with SDK middleware use
ambrauer Apr 19, 2022
b4298f5
"BOXEVER" > "CDP" env var naming and added comments
ambrauer Apr 20, 2022
4e56033
fix lint issues
ambrauer Apr 20, 2022
eaee226
added unit tests to personalize utils
addy-pathania Apr 21, 2022
edbc5a4
added unit tests for graphql personalize
addy-pathania Apr 22, 2022
4080e79
updated graphql tests
addy-pathania Apr 22, 2022
bf6259f
Add personalize and site submodules to doc generation
ambrauer Apr 22, 2022
bcb3c7e
Merge branch 'dev' into feature/517648-personalize-middleware
ambrauer Apr 22, 2022
894f57d
BOXEVER_SCRIPT_URL > CDP_SCRIPT_URL
ambrauer Apr 22, 2022
2a493be
add edge submodule to doc generation
ambrauer Apr 22, 2022
0f4536b
move personalize-middleware to edge
ambrauer Apr 22, 2022
4eaebbf
fixed failing test
addy-pathania Apr 25, 2022
f5fccd8
added unit test for cdp-service
addy-pathania Apr 25, 2022
fd348aa
Fix some issues with CdpIntegrationScript.tsx
ambrauer Apr 26, 2022
d34503d
Switch from native fetch to use our AxiosDataFetcher with the axios-f…
ambrauer Apr 27, 2022
02b4992
bump next version
ambrauer Apr 28, 2022
76ffd91
Switching to "variantIds" (Edge schema is already updated)
ambrauer Apr 29, 2022
550ed63
expanded debug logging for middleware
ambrauer May 2, 2022
6cd23aa
Use absolute URL in middleware, fix tests
ambrauer May 3, 2022
f9798da
Bake CDP API version into cdp-service and default env values
ambrauer May 4, 2022
b8c236e
Use NEXT_PUBLIC_ prefix for CDP env variables (so they can be used in…
ambrauer May 4, 2022
33a875c
also skip if segment identified (by CDP) is not in list of configured…
ambrauer May 4, 2022
74285a7
Merge branch 'dev' into feature/517648-personalize-middleware
ambrauer May 5, 2022
a2fad64
Use config.defaultLanguage
ambrauer May 5, 2022
a1b25e9
more jsdoc comments
ambrauer May 5, 2022
3caf08c
bump to latest next
ambrauer May 10, 2022
eef5e47
Forgo axios-fetch-adapter in favor of our own NativeDataFetcher
ambrauer May 19, 2022
f03bac6
Edge next.config plugin for webpack config customizations
ambrauer May 19, 2022
39bdd04
Merge branch 'dev' into feature/517648-personalize-middleware
ambrauer May 19, 2022
0ec6a99
update doc comments, pass-through new fetcher types
ambrauer May 19, 2022
47323c8
Use getPersonalizedRewrite helper in graphql-sitemap-service
ambrauer May 23, 2022
2a8ced0
Revert DataFetcher interface, too much confusion for now. Just sticki…
ambrauer May 23, 2022
f1109d1
unit tests for native-fetcher
ambrauer May 24, 2022
0553665
Fix omit for dataFetcherResolver
ambrauer May 24, 2022
84a8a29
Remove export of PersonalizedRewriteData type.
ambrauer May 24, 2022
c285f88
PR review feedback updates
ambrauer May 25, 2022
3f632c6
Handle response.json() errors in native-fetcher
ambrauer May 25, 2022
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
19 changes: 14 additions & 5 deletions packages/create-sitecore-jss/src/templates/nextjs-personalize/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
BOXEVER_CLIENT_KEY=
BOXEVER_API=
BOXEVER_TARGET_URL=
BOXEVER_SCRIPT_URL=
CDP_POINTOFSALE=
# Your Sitecore CDP REST API base URL
NEXT_PUBLIC_CDP_API_URL=https://api.boxever.com

# Your Sitecore CDP client key
NEXT_PUBLIC_CDP_CLIENT_KEY=

# Your Sitecore CDP target URL
NEXT_PUBLIC_CDP_TARGET_URL=https://api.boxever.com/v1.2

# Your Sitecore CDP JavaScript library URL
NEXT_PUBLIC_CDP_SCRIPT_URL=https://d1mj578wat5n4o.cloudfront.net/boxever-1.4.8.min.js

# Sitecore CDP point of sale
NEXT_PUBLIC_CDP_POINTOFSALE=
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useSitecoreContext } from '@sitecore-jss/sitecore-jss-nextjs';
import Script from 'next/script';
import { useEffect } from 'react';
import config from 'temp/config';

declare const _boxeverq: { (): void }[];
declare const Boxever: Boxever;
Expand All @@ -21,7 +22,8 @@ interface BoxeverViewEventArgs {

function createPageView(locale: string, 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 pointOfSale = process.env.CDP_POINTOFSALE || window.location.host.replace(/^www\./, '');
const pointOfSale =
process.env.NEXT_PUBLIC_CDP_POINTOFSALE || window.location.host.replace(/^www\./, '');

_boxeverq.push(function () {
const pageViewEvent: BoxeverViewEventArgs = {
Expand All @@ -44,20 +46,22 @@ function createPageView(locale: string, routeName: string) {
}

const CdpIntegrationScript = (): JSX.Element => {
const { pageEditing, route } = useSitecoreContext();
const {
sitecoreContext: { pageEditing, route },
} = useSitecoreContext();

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

createPageView(route.itemLanguage, route.name);
route && createPageView(route.itemLanguage || config.defaultLanguage, route.name);
});

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

return (
Expand All @@ -70,14 +74,14 @@ const CdpIntegrationScript = (): JSX.Element => {
var _boxeverq = _boxeverq || [];

var _boxever_settings = {
client_key: '${process.env.BOXEVER_CLIENT_KEY}',
target: '${process.env.BOXEVER_TARGET_URL}',
client_key: '${process.env.NEXT_PUBLIC_CDP_CLIENT_KEY}',
target: '${process.env.NEXT_PUBLIC_CDP_TARGET_URL}',
cookie_domain: ''
};
`,
}}
/>
<Script src={process.env.BOXEVER_SCRIPT_URL} />
<Script src={process.env.NEXT_PUBLIC_CDP_SCRIPT_URL} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,150 +1,31 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { PersonalizeMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/edge';
import { MiddlewarePlugin } from '..';

export const personalizePlugin: MiddlewarePlugin = async function (
req: NextRequest,
res: NextResponse
) {
// no need to personalize for preview, layout data already prepared on XM Cloud for preview,
// personalizeLayout function will not perform any transformation if pass not existing segment code: e.g. _default
const isPreview = req.cookies['__prerender_bypass'] || req.cookies['__next_preview_data'];
let segment = '';
let cdpBrowserId = '';

const pathname = req.nextUrl?.pathname;
// exclude /api route as not page one
const isApiRoute = pathname?.indexOf('/api/') !== -1;

// middleware in the root intercepts requests for static assets (/public folder on app src)
// no need to personalize them, no way to distinguish asset based on request, see https://github.com/vercel/next.js/issues/31721
const isAsset = /\.(gif|jpg|jpeg|tiff|png|svg|ashx|ico)$/i.test(pathname || '');

if (!isAsset && !isApiRoute) {
if (isPreview) {
segment = '_default';
} else {
// logic inside call Exp Edge to get itemid, call Boxever API to get segment
const cdpResponse = await getSegmentForCurrentUser(req);
segment = cdpResponse.segmentCode;
cdpBrowserId = cdpResponse.browserId;
if (!segment) {
segment = '_default';
}
}

if (pathname) {
// _segmentId_ is just special word to distinguish path with segment code
// without local rewrite will not work, see bug: https://github.com/vercel-customer-feedback/edge-functions/issues/85
const rewriteTo = `/${req.nextUrl.locale || 'en'}/_segmentId_${segment}` + pathname;

const nextResponse = NextResponse.rewrite(rewriteTo);
// set Boxever identification cookie
// had better set boxeverid cookie on server, read https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/
if (cdpBrowserId) {
const boxeverClientKey = process.env.BOXEVER_CLIENT_KEY;
const browserIdCookieName = `bid_${boxeverClientKey}`;
SetCookie(nextResponse, cdpBrowserId, browserIdCookieName);
}

return nextResponse;
}
import config from 'temp/config';

class PersonalizePlugin implements MiddlewarePlugin {
private personalizeMiddleware: PersonalizeMiddleware;

// Using 1 to leave room for things like redirects to occur first
order = 1;

constructor() {
this.personalizeMiddleware = new PersonalizeMiddleware({
edgeConfig: {
endpoint: config.graphQLEndpoint,
apiKey: config.sitecoreApiKey,
siteName: config.jssAppName,
},
cdpConfig: {
endpoint: process.env.NEXT_PUBLIC_CDP_API_URL || '',
clientKey: process.env.NEXT_PUBLIC_CDP_CLIENT_KEY || '',
},
});
}

return res;
};

personalizePlugin.order = 0;

async function getSegmentForCurrentUser(req: NextRequest) {
// ALL THOSE KEYS ALL PUBLIC, move to env variables in production implementation
const boxeverApi = process.env.BOXEVER_API;
const boxeverClientKey = process.env.BOXEVER_CLIENT_KEY;
const expEdgeGraphql =
process.env.GRAPH_QL_ENDPOINT ||
(process.env.SITECORE_API_HOST || 'http://nextjsedge102') + '/sitecore/api/graph/edge';
const sc_apikey = process.env.SITECORE_API_KEY || '24B40E6D-B002-465B-91CF-A3EE37E584E2';
const site = process.env.JSS_APP_NAME || 'JssNextWeb';
const routePath = req.nextUrl?.pathname;
const language = req.nextUrl.locale || 'en';
let friendlyId = '';

// HERE WILL BE personalization field on item with segmentFriendlyIds,
// if segmentFriendlyIds is empty no need to call Boxever, page has not personalization
const init = {
body: JSON.stringify({
operationName: 'layout',
query: `query layout {
layout(site: "${site}", routePath: "${routePath}", language: "${language}") {
item {
id
version
}
}
}`,
variables: {},
}),
method: 'POST',
headers: {
'content-type': 'application/json',
sc_apikey: sc_apikey,
},
};
const edgeResponse = await fetch(`${expEdgeGraphql}`, init);
const edgeResult = await edgeResponse.json();

friendlyId =
`${edgeResult?.data?.layout?.item.id}_${language}_${edgeResult.data?.layout?.item?.version}`.toLowerCase();

return await GetSegmentFromCdp(req, boxeverApi, boxeverClientKey, friendlyId);
}

async function GetSegmentFromCdp(
req: NextRequest,
boxeverApi: string,
boxeverClientKey: string,
contentFriendlyId: string
) {
// Each user should have saved identifier to connect between session, Boxever use bid cookies + local storage
const browserIdCookieName = `bid_${boxeverClientKey}`;

const payload = { clientKey: boxeverClientKey, browserId: '', params: {} };
if (req.cookies[browserIdCookieName] !== null) {
payload.browserId = req.cookies[browserIdCookieName];
}
console.log(`Payload -> ${JSON.stringify(payload)}`);

const rawResponse = await fetch(boxeverApi + `/${contentFriendlyId}`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(payload),
});

if (!rawResponse.ok) {
return { segmentCode: '' };
async exec(req: NextRequest, res?: NextResponse): Promise<NextResponse> {
return this.personalizeMiddleware.getHandler()(req, res);
}

const cdpSegmentsResponseJson = await rawResponse.json();
console.log(`CDP response -> ${JSON.stringify(cdpSegmentsResponseJson)}`);

const segmentCode =
cdpSegmentsResponseJson?.segments && cdpSegmentsResponseJson?.segments.length
? cdpSegmentsResponseJson?.segments[0]
: '';
return {
segmentCode: segmentCode,
browserId: cdpSegmentsResponseJson.browserId,
};
}

function SetCookie(res: NextResponse, browserId: string, browserIdCookieName: string) {
if (typeof browserId !== 'undefined') {
const expiryDate = new Date(new Date().setFullYear(new Date().getFullYear() + 2));
const options = { expires: expiryDate, secure: true };

res.cookie(browserIdCookieName, browserId, options);
}
}
export const personalizePlugin = new PersonalizePlugin();
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ParsedUrlQuery } from 'querystring';
import { normalizePersonalizedRewrite } from '@sitecore-jss/sitecore-jss-nextjs';

/**
* Extract normalized Sitecore item path from query
Expand All @@ -15,11 +16,8 @@ export function extractPath(params: ParsedUrlQuery | undefined): string {
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], '');
}
// Ensure personalized rewrite data is removed
path = normalizePersonalizedRewrite(path);

return path;
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { Plugin } from '..';
import { personalizeLayout } from '@sitecore-jss/sitecore-jss-nextjs';
import { getPersonalizedRewriteData, personalizeLayout } from '@sitecore-jss/sitecore-jss-nextjs';
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_'));
}
if (!context?.params?.path) {
return props;
}
// Get segment for personalization (from path)
const path = Array.isArray(context.params.path)
? context.params.path.join('/')
: context.params.path ?? '/';

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

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

return props;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@sitecore-jss/sitecore-jss-nextjs": "^21.0.0-canary",
"graphql": "~15.4.0",
"graphql-tag": "^2.11.0",
"next": "12.1.5",
"next": "^12.1.6",
"next-localization": "^0.10.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const edgePlugin = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
webpack: (config, options) => {
if (options.isServer && options.nextRuntime === 'edge') {
// Next.js enforces a strict (browser-based) runtime on Edge.
// Point the Edge compiler in the right direction for 3rd-party module browser bundles.

// debug
config.resolve.alias.debug = require.resolve('debug/src/browser');

// graphql-request
config.resolve.alias['cross-fetch'] = require.resolve('cross-fetch/dist/browser-ponyfill');
config.resolve.alias['form-data'] = require.resolve('form-data/lib/browser');
}

// Overload the Webpack config if it was already overloaded
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}

return config;
}
});
};

module.exports = edgePlugin;
6 changes: 3 additions & 3 deletions packages/sitecore-jss-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "mocha --require ./test/setup.js \"./src/**/*.test.ts\" \"./src/**/*.test.tsx\" --exit",
"prepublishOnly": "npm run build",
"coverage": "nyc npm test",
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-nextjs --entryPoints src/index.ts --entryPoints src/middleware/index.ts --githubPages false"
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-nextjs --entryPoints src/index.ts --entryPoints src/edge/index.ts --entryPoints src/middleware/index.ts --githubPages false"
},
"engines": {
"node": ">=12",
Expand Down Expand Up @@ -52,7 +52,7 @@
"eslint-plugin-react": "^7.21.5",
"jsdom": "^15.1.1",
"mocha": "^9.1.3",
"next": "12.1.5",
"next": "^12.1.6",
"nock": "^13.0.5",
"nyc": "^15.1.0",
"react": "^17.0.2",
Expand All @@ -64,7 +64,7 @@
"typescript": "~4.3.5"
},
"peerDependencies": {
"next": "12.1.5",
"next": "^12.1.6",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/sitecore-jss-nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { RedirectsMiddleware } from './redirects-middleware';
export { RedirectsMiddleware, RedirectsMiddlewareConfig } from './redirects-middleware';
export { PersonalizeMiddleware, PersonalizeMiddlewareConfig } from '../edge/personalize-middleware';
Loading