@jss-rule-engine/edge
Version:
154 lines (125 loc) • 5.56 kB
text/typescript
import { NextResponse, NextRequest, userAgent } from 'next/server';
import {
ExperienceParams,
GraphQLPersonalizeServiceConfig,
} from '@sitecore-jss/sitecore-jss/personalize';
import {GraphQLSCPersonalizeService} from './ScPersonalizeService'
import { MiddlewareBase, MiddlewareBaseConfig } from './middleware';
import { getScPersonalizedRewrite} from '../../lib/personalizationUtils'
export type ScPersonalizeMiddlewareConfig = MiddlewareBaseConfig & {
edgeConfig: Omit<GraphQLPersonalizeServiceConfig, 'fetch'>;
};
/**
* Middleware / handler to support Sitecore Personalize
*/
export class ScPersonalizeMiddleware extends MiddlewareBase {
private _personalizeService: GraphQLSCPersonalizeService;
/**
* @param {ScPersonalizeMiddlewareConfig} [config] Personalize middleware config
*/
constructor(protected config: ScPersonalizeMiddlewareConfig) {
super(config);
this._personalizeService = new GraphQLSCPersonalizeService({
...config.edgeConfig,
fetch: fetch
});
}
/**
* Gets the Next.js middleware handler with error handling
* @returns middleware handler
*/
public getHandler(): (req: NextRequest, res?: NextResponse) => Promise<NextResponse> {
return async (req, res) => {
try {
return await this.handler(req, res);
} catch (error:any) {
console.log('Personalize middleware failed:');
console.log(error);
return res || NextResponse.next();
}
};
}
protected getExperienceParams(req: NextRequest): ExperienceParams {
return {
// It's expected that the header name "referer" is actually a misspelling of the word "referrer"
// req.referrer is used during fetching to determine the value of the Referer header of the request being made,
// used as a fallback
referrer: req.headers.get('referer') || req.referrer,
utm: {
campaign: req.nextUrl.searchParams.get('utm_campaign'),
content: req.nextUrl.searchParams.get('utm_content'),
medium: req.nextUrl.searchParams.get('utm_medium'),
source: req.nextUrl.searchParams.get('utm_source'),
},
};
}
protected excludeRoute(pathname: string): boolean | undefined {
// ignore files
return pathname.includes('.') || super.excludeRoute(pathname);
}
private handler = async (req: NextRequest, res?: NextResponse): Promise<NextResponse> => {
const pathname = req.nextUrl.pathname;
const language = this.getLanguage(req);
const hostname = this.getHostHeader(req) || this.defaultHostname;
const queryString = req.nextUrl.search;
const startTimestamp = Date.now();
console.log('scPersonalizeHandler', hostname, startTimestamp, queryString)
// Response will be provided if other middleware is run before us (e.g. redirects)
let response = res || NextResponse.next();
if (this.config.disabled && this.config.disabled(req, response)) {
console.log('skipped (personalize middleware is disabled)');
return response;
}
if (
response.redirected || // Don't attempt to personalize a redirect
this.isPreview(req) || // No need to personalize for preview (layout data is already prepared for preview)
this.excludeRoute(pathname)
) {
console.log(
'skipped (%s)',
response.redirected ? 'redirected' : this.isPreview(req) ? 'preview' : 'route excluded'
);
return response;
}
const site = this.getSite(req, res);
console.log('getPersonalizeInfo', pathname, language, site.name);
// Get personalization info from Experience Edge
this._personalizeService.setPersonalizeContext({url: req.url, hostname: req.headers.get('hostname')});
const personalizeInfo = await this._personalizeService.getPersonalizeInfo(
pathname,
language,
site.name
);
if (!personalizeInfo) {
// Likely an invalid route / language
console.log('skipped (personalize info not found)');
return response;
}
// Execute targeted experience in CDP
const { ua } = userAgent(req);
const params = this.getExperienceParams(req);
console.log('params', ua, params)
const variantId = personalizeInfo.activeVariantid;
if (!variantId) {
console.log('skipped (no variant identified)');
return response;
}
// Path can be rewritten by previously executed middleware
const basePath = res?.headers.get('x-sc-rewrite') || pathname;
// Rewrite to persononalized path
const rewritePath = getScPersonalizedRewrite(basePath, variantId);
console.log('Rewritten path - ', rewritePath)
// Note an absolute URL is required: https://nextjs.org/docs/messages/middleware-relative-urls
const rewriteUrl = req.nextUrl.clone();
rewriteUrl.pathname = rewritePath;
response = NextResponse.rewrite(rewriteUrl);
// Disable preflight caching to force revalidation on client-side navigation (personalization may be influenced)
// See https://github.com/vercel/next.js/issues/32727
response.headers.set('x-middleware-cache', 'no-cache');
// Share rewrite path with following executed middlewares
response.headers.set('x-sc-rewrite', rewritePath);
// Share site name with the following executed middlewares
response.cookies.set(this.SITE_SYMBOL, site.name);
return response;
};
}