-
Notifications
You must be signed in to change notification settings - Fork 277
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Next.js] Personalize Middleware (#1008)
* Use config values * WIP * Progress on sitecore-jss/personalize service layer and sitecore-jss-nextjs/middleware for personalize * use native fetch override * enable fetch config pass-through * Introduce personalize utils for rewrite url path de/construction * Replace nextjs-personalize plugin code with SDK middleware use * "BOXEVER" > "CDP" env var naming and added comments * fix lint issues * added unit tests to personalize utils * added unit tests for graphql personalize * updated graphql tests * Add personalize and site submodules to doc generation * BOXEVER_SCRIPT_URL > CDP_SCRIPT_URL * add edge submodule to doc generation * move personalize-middleware to edge * fixed failing test * added unit test for cdp-service * Fix some issues with CdpIntegrationScript.tsx * Switch from native fetch to use our AxiosDataFetcher with the axios-fetch-adapter module to provide fetch compatibility * bump next version * Switching to "variantIds" (Edge schema is already updated) * expanded debug logging for middleware * Use absolute URL in middleware, fix tests * Bake CDP API version into cdp-service and default env values * Use NEXT_PUBLIC_ prefix for CDP env variables (so they can be used in the browser) * also skip if segment identified (by CDP) is not in list of configured segments (in layout data) * Use config.defaultLanguage * more jsdoc comments * bump to latest next * Forgo axios-fetch-adapter in favor of our own NativeDataFetcher * Edge next.config plugin for webpack config customizations * update doc comments, pass-through new fetcher types * Use getPersonalizedRewrite helper in graphql-sitemap-service * Revert DataFetcher interface, too much confusion for now. Just sticking with a custom data fetcher resolver. * unit tests for native-fetcher * Fix omit for dataFetcherResolver * Remove export of PersonalizedRewriteData type. * PR review feedback updates * Handle response.json() errors in native-fetcher Co-authored-by: Addy Pathania <[email protected]>
- Loading branch information
1 parent
9a8c77c
commit cd0d3a6
Showing
27 changed files
with
1,165 additions
and
262 deletions.
There are no files selected for viewing
19 changes: 14 additions & 5 deletions
19
packages/create-sitecore-jss/src/templates/nextjs-personalize/.env
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 25 additions & 144 deletions
169
...e-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 10 additions & 15 deletions
25
...re-jss/src/templates/nextjs-personalize/src/lib/page-props-factory/plugins/personalize.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
packages/create-sitecore-jss/src/templates/nextjs/src/lib/next-config/plugins/edge.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Oops, something went wrong.