Skip to content

Instantly share code, notes, and snippets.

@frueda1
Created September 17, 2023 04:54
Show Gist options
  • Save frueda1/a3827471a7a33cc452e717254a92c30f to your computer and use it in GitHub Desktop.
Save frueda1/a3827471a7a33cc452e717254a92c30f to your computer and use it in GitHub Desktop.
XM Cloud integration with Sitecore Personalize, and call GraphQL query with Decision Model Result as parameter.
export const ARTICLE_TEMPLATE = '4A7C0D95-EC1C-4C89-8C08-AA07248A9F0B';
export const ENGAGE_SETTINGS = {
clientKey: `${process.env.NEXT_PUBLIC_ENGAGE_CLIENT_KEY}`,
targetURL: `${process.env.NEXT_PUBLIC_ENGAGE_TARGET_URL}`,
pointOfSale: `${process.env.NEXT_PUBLIC_ENGAGE_POS}`,
forceServerCookieMode: true,
includeUTMParameters: true,
webPersonalization: true,
};
import { NextRequest, NextResponse } from 'next/server';
import { MiddlewarePlugin } from '..';
import { initServer } from '@sitecore/engage';
class EngagePlugin implements MiddlewarePlugin {
order = 2;
async exec(req: NextRequest, res?: NextResponse): Promise<NextResponse> {
res = NextResponse.next();
const engageSettings = {
clientKey: `${process.env.NEXT_PUBLIC_ENGAGE_CLIENT_KEY}`,
targetURL: `${process.env.NEXT_PUBLIC_ENGAGE_TARGET_URL}`,
pointOfSale: `${process.env.NEXT_PUBLIC_ENGAGE_POS}`,
cookieDomain: `${process.env.NEXT_PUBLIC_ENGAGE_DOMAIN}`,
cookieExpiryDays: 365,
forceServerCookieMode: true,
};
const engageServer = initServer(engageSettings);
await engageServer.handleCookie(req, res);
return res;
}
}
export const engagePlugin = new EngagePlugin();
import { GraphQLClient } from 'graphql-request';
import gql from 'graphql-tag';
import { sitecoreApiKey, graphQLEndpoint } from 'temp/config';
function getGraphQLClient(): GraphQLClient {
if (!sitecoreApiKey || !graphQLEndpoint) {
console.error('No Sitecore API Key, Sitemap Root ID and/or public URL configured for the site');
return {} as GraphQLClient;
}
const client = new GraphQLClient(graphQLEndpoint);
client.setHeader('sc_apikey', sitecoreApiKey);
return client;
}
const getRelatedItems = async (
mostVisitedPage: string,
templateId: string
): Promise<RelatedItemsQueryResult> => {
const graphQLClient = getGraphQLClient();
const visitedPage = mostVisitedPage !== '' ? mostVisitedPage.split('|')[0] : '';
const template = mostVisitedPage !== '' ? mostVisitedPage.split('|')[1] : '';
let conditions = '';
let templateIdsConditions = '';
templateIdsConditions = ` { OR: [ { name: "_templates", value: "${templateId}", operator: CONTAINS } ] } `;
if (template === 'IndustryPage') {
conditions = `{ OR: [ { name: "industry", value: "${visitedPage}", operator: CONTAINS } ] }`;
}
const queryToGQL = getQueryToGQL(conditions, templateIdsConditions);
const result = await graphQLClient.request<RelatedItemsQueryResult>(
gql`
${queryToGQL}
`
);
return result;
};
const getQueryToGQL = (conditions: string, templateIdsConditions: string) => {
return `
query {
search(
where: {
AND: [
${templateIdsConditions}
${conditions}
]
}
orderBy:{name:"publicationDate", direction:DESC}
# defaults to 10
first: 10
after: ""
) {
total
pageInfo {
endCursor
hasNext
}
results {
name: field(name: "name") {
jsonValue
}
id
url {
path
}
}
}
}
`;
};
export type RelatedItemsQueryResult = {
search: {
total: number;
pageInfo: {
endCursor: string;
hasNext: boolean;
};
results: [
{
name: {
jsonValue: {
value: string;
};
};
id: string;
url: {
path: string;
};
}
];
};
};
export { getRelatedItems };
/**
* This Layout is needed for Starter Kit.
*/
import React from 'react';
import Head from 'next/head';
import {
Placeholder,
getPublicUrl,
LayoutServiceData,
Field,
} from '@sitecore-jss/sitecore-jss-nextjs';
import Scripts from 'src/Scripts';
import { useEffect } from 'react';
import { ICustomEventInput, init } from '@sitecore/engage';
import { ENGAGE_SETTINGS } from 'lib/constants';
import { useRouter } from 'next/router';
// Prefix public assets with a public URL to enable compatibility with Sitecore Experience Editor.
// If you're not supporting the Experience Editor, you can remove this.
const publicUrl = getPublicUrl();
interface LayoutProps {
layoutData: LayoutServiceData;
}
interface RouteFields {
[key: string]: unknown;
Title?: Field;
}
const Layout = ({ layoutData }: LayoutProps): JSX.Element => {
const { route } = layoutData.sitecore;
const router = useRouter();
const fields = route?.fields as RouteFields;
const isPageEditing = layoutData.sitecore.context.pageEditing;
const mainClassPageEditing = isPageEditing ? 'editing-mode' : 'prod-mode';
useEffect(() => {
InitEngage();
}, [router, route, route?.itemId, route?.templateName, route?.displayName]);
const InitEngage = () => {
if (router.isReady && route && route.itemId && route.templateName && route.displayName) {
init(ENGAGE_SETTINGS).then((engage) => {
const eventData: ICustomEventInput = {
channel: 'WEB',
currency: 'USD',
pointOfSale: `${process.env.NEXT_PUBLIC_ENGAGE_POS}`,
page: route.itemId,
};
const extensionData = {
template: route.templateName,
pageName: route.displayName,
};
// Send VIEW events
engage.event('PAGE_VIEW', eventData, extensionData);
});
}
};
return (
<>
<Scripts />
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<title>{fields.Title?.value?.toString() || 'Page'}</title>
<link rel="icon" href={`${publicUrl}/favicon.ico`} />
</Head>
{/* root placeholder for the app, which we add components to using route data */}
<div className={mainClassPageEditing}>
<header>
<div id="header">
{route && (
<Placeholder name="headless-header" rendering={route} layoutData={layoutData} />
)}
</div>
</header>
<main id="top">
<div id="content">
{route && (
<Placeholder name="headless-main" rendering={route} layoutData={layoutData} />
)}
</div>
</main>
<footer>
<div id="footer">
{route && (
<Placeholder name="headless-footer" rendering={route} layoutData={layoutData} />
)}
</div>
</footer>
</div>
</>
);
};
export default Layout;
import { ARTICLE_TEMPLATE } from 'lib/constants';
import { RelatedItemsQueryResult, getRelatedItems } from 'lib/graphql';
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = {
suggestedInfo: RelatedItemsQueryResult;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
const body = JSON.parse(req.body);
const templateId = ARTICLE_TEMPLATE;
let relatedInfo = await getRelatedItems(body.visitedPage, templateId);
res.status(200).json({ suggestedInfo: relatedInfo });
}
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { ENGAGE_SETTINGS } from 'lib/constants';
import { init } from '@sitecore/engage';
import { RelatedItemsQueryResult } from 'lib/graphql';
type ExperienceResult = {
visitedPage: {
page: string;
};
message: string;
};
type RelatedItemsQueryProps = {
suggestedInfo: RelatedItemsQueryResult;
};
const RelatedItems = (): JSX.Element => {
const router = useRouter();
const [relatedItemsResult, setRelatedItemsResult] = useState<RelatedItemsQueryResult | undefined>(
undefined
);
const [decisionModelResult, setDecisionModelResult] = useState<ExperienceResult | undefined>(
undefined
);
useEffect(() => {
if (router.isReady) {
init(ENGAGE_SETTINGS).then(async (engage) => {
if (engage) {
const experienceResponse = (await engage.personalize({
friendlyId: 'suggested_for_you',
channel: 'WEB',
currency: 'USD',
pointOfSale: `${process.env.NEXT_PUBLIC_ENGAGE_POS}`,
})) as ExperienceResult;
console.log({ experienceResponse });
if (experienceResponse) {
setDecisionModelResult(experienceResponse);
const filters = {
visitedPage: experienceResponse.visitedPage
? experienceResponse.visitedPage?.page
: '',
};
fetch('/api/relatedItems', {
method: 'POST',
body: JSON.stringify(filters),
})
.then(async (res) => {
const { suggestedInfo } = (await res.json()) as RelatedItemsQueryProps;
setRelatedItemsResult(suggestedInfo);
console.log({ suggestedInfo });
})
.catch((error) => {
console.error(error);
});
}
}
});
}
}, [router]);
return (
<>
<br></br>
<h3>Results of query based on Sitecore Personalize Result</h3>
<br></br>
<h5>Sitecore Personalize Result:</h5>
<br></br>
<h5 className="txt-primary">{decisionModelResult?.visitedPage.page}</h5>
<br></br>
<h5>GraphQL Results:</h5>
<div className="contentContainer py-[50px]">
<div className="contentGrid ">
{relatedItemsResult?.search.results?.map((item, index) => (
<div key={index} className="suggestedCards-one col-span-4 md:col-span-4">
<div className=" bg-space h-full">
<div className="os-card os-card--small os-border h-full">
<section className="os-card-content">
<a href="#">
<p className="eyebrow txt-secondary txt-upper">GraphQL result</p>
<h5 className="txt-foundation">{item.name.jsonValue.value}</h5>
</a>
</section>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default RelatedItems;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment