Setup Next.js Website with Personalize - Launch
This guide will help you set up your Next.js website with Personalize, hosted on Launch.
Prerequisites
- Next.js website on the latest version (14 and above) with app router
- Website content sourced from a Stack
- Website deployed on Launch
- Personalize project created and connected to the Stack
Steps for Execution
- Get Personalized project UID
- Proxy requests with Launch
- Fetch variant content from the origin
- Set up attributes and trigger events
Get Personalize Project UID
To retrieve the project UID, log in to your Contentstack account and perform the steps given below:
Navigate to Personalize and select your preferred project.
Click the Settings icon in the left navigation panel.
In the General tab, under Project Details, you will find the 24-character project UID.
Click the Copy icon to copy the project UID to your clipboard. We will need this UID while setting up the Personalize Edge SDK in the next step.
Proxy Requests with Launch Edge Proxy
Launch Edge Proxy allows you to execute your code in proximity to your user’s location before a request is processed. Using this feature, we can make a call to the Personalize Edge API and fetch the User Manifest for each visitor. The User Manifest contains the selected Variant for each Experience. We can then pass these variants as the URL query parameters.
We would use the Launch Edge Proxy, since the Next.js Middleware executes on the server side and not in the Edge when hosted on Launch.
Install the Personalize SDK
To install the Personalize Edge SDK in your Next.js project:
$ npm install @contentstack/personalize-edge-sdk
Additional Resource: The API Reference for the SDK contains a lot of information on how to use the SDK.
Create the Edge Function Handler
The [proxy].edge.js is to be created in the /functions/ folder inside your website source code. To create the Edge Function Handler:
// [proxy].edge.js
export default async function handler(request, context) {
return fetch(request);
}
Initialize the SDK
Now modify the [proxy].edge.js file with the following code to initialize the Personalize Edge SDK:
import Personalize from '@contentstack/personalize-edge-sdk';
export default async function handler(request, context) {
// set a custom edge API URL
if (context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL) {
Personalize.setEdgeApiUrl(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL);
}
// Initialize the SDK and pass the request as well
await Personalize.init(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_PROJECT_UID, {
request,
});
return fetch(request);
}
Pass the variant parameter in the URL
Now add the following code to the [proxy].edge.js file to pass the variant parameter in the URL:
import Personalize from '@contentstack/personalize-edge-sdk';
export default async function handler(request, context) {
const parsedUrl = new URL(request.url);
// reset the SDK to remove any existing context
Personalize.reset();
if (context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL) {
Personalize.setEdgeApiUrl(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL);
}
await Personalize.init(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_PROJECT_UID, {
request,
});
// get the variant parameter from the SDK
const variantParam = Personalize.getVariantParam();
// set the variant parameter as a query param in the URL
parsedUrl.searchParams.set(Personalize.VARIANT_QUERY_PARAM, variantParam);
// rewrite the request with the modified URL
const modifiedRequest = new Request(parsedUrl.toString(), request);
const response = await fetch(modifiedRequest);
return response;
}
Add Response Context
Now add the following code to the [proxy].edge.js file to add the response context:
import Personalize from '@contentstack/personalize-edge-sdk';
export default async function handler(request, context) {
const parsedUrl = new URL(request.url);
Personalize.reset();
if (context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL) {
Personalize.setEdgeApiUrl(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL);
}
await Personalize.init(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_PROJECT_UID, {
request,
});
const variantParam = Personalize.getVariantParam();
parsedUrl.searchParams.set(Personalize.VARIANT_QUERY_PARAM, variantParam);
const modifiedRequest = new Request(parsedUrl.toString(), request);
const response = await fetch(modifiedRequest);
const modifiedResponse = new Response(response.body, response);
// add cookies to the response
await Personalize.addStateToResponse(modifiedResponse);
// ensure that the response is not cached on the browser
modifiedResponse.headers.set('cache-control', 'no-store');
return modifiedResponse;
}
Exclude Asset Calls
We do not need to invoke the proxy logic for Next.js asset calls. By adding a quick check at the start of our edge function, we’re able to exclude them and only include page requests for processing in the edge function, achieving better performance.
import Personalize from '@contentstack/personalize-edge-sdk';
export default async function handler(request, context) {
const parsedUrl = new URL(request.url);
const pathname = parsedUrl.pathname;
// exclude Next.js asset calls so that only page requests are processed
if (['_next', 'favicon.ico'].some((path) => pathname.includes(path))) {
return fetch(request);
}
// reset the SDK to remove any existing context
Personalize.reset();
// set a custom edge API URL
if (context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL) {
Personalize.setEdgeApiUrl(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_EDGE_API_URL);
}
// Initialize the SDK and pass the request as well
await Personalize.init(context.env.NEXT_PUBLIC_CONTENTSTACK_PERSONALIZE_PROJECT_UID, {
request,
});
const variantParam = Personalize.getVariantParam();
parsedUrl.searchParams.set(Personalize.VARIANT_QUERY_PARAM, variantParam);
const modifiedRequest = new Request(parsedUrl.toString(), request);
const response = await fetch(modifiedRequest);
const modifiedResponse = new Response(response.body, response);
// add cookies to the response
await Personalize.addStateToResponse(modifiedResponse);
// ensure that the response is not cached on the browser
modifiedResponse.headers.set('cache-control', 'no-store');
return modifiedResponse;
}
Fetch Variant Content from the Origin
The final step is to fetch the variant content using the variant parameter passed in the URL. This step needs to be performed for all the pages that need Personalize enabled.
Move to Server Side Rendering (SSR) + Cache Headers
Launch uses cache headers to cache content with SSR, hence we need to change our Next.js rendering mode to SSR. This is because we want to execute backend logic to render uncached requests, so that the right variants can be rendered for each unique request.
Please follow the Launch documentation steps to move to SSR and cache headers here. Essentially, this step involves force rendering the page on the server side and setting the appropriate cache headers.
The cache headers recommended in the above documentation can be tweaked for our personalized setup. We need to replace stale-while-revalidate with must-revalidate in the example code, to ensure that the personalized page is available immediately as the user navigates the website.
Fetch Personalized Variants from the CMS
Here, we modify the Homepage (/app/page.ts) to fetch Personalized variants from the CMS. Similarly, add the code to the other pages of your website source code.
import Personalize from '@contentstack/personalize-edge-sdk';
import contentstack from '@contentstack/delivery-sdk';
const Page = async ({
searchParams,
}: {
searchParams: Record<string, string>;
}) => {
// extract the variant parameter from the URL
let variantParam = decodeURIComponent(
searchParams[Personalize.VARIANT_QUERY_PARAM]
);
// initialize the CMS Delivery SDK
const stack = contentstack.stack({
apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY,
deliveryToken: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN,
environment: process.env.NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT,
host: process.env.CONTENTSTACK_DELIVERY_API_HOST,
});
// the call to fetch a specific entry
const entryCall = stack
.contentType(process.env.NEXT_PUBLIC_CONTENTSTACK_HOMEPAGE_CONTENTTYPE_UID)
.entry(process.env.NEXT_PUBLIC_CONTENTSTACK_HOMEPAGE_ENTRY_UID);
let entry;
if (variantParam) {
// convert the variant parameter to variant aliases
const variantAlias = Personalize.variantParamToVariantAliases(variantParam).join(',');
// pass the variant aliases when fetching the entry
entry = await entryCall.variants(variantAlias).fetch();
} else {
// fetch the entry without the variant aliases
entry = await entryCall.fetch();
}
// render the page with the entry
};
Set Up Attributes and Trigger Events
Setting attributes and triggering events can be done in the following ways:
Integrating with a CDP
Using Google Tag Manager
Using the SDK in the code (JavaScript Personalize Edge SDK)
Set Attributes
The following snippet shows how to set an attribute submitted via a form for a user’s age:
'use client';
import { useContext } from 'react';
import { PersonalizeContext } from '../context/PersonalizeContext';
const Page = () => {
const Personalize = useContext(PersonalizeContext);
const onFormSubmit = async (form) => {
await Personalize.set({ age: form.age });
}
// render the component
}
Trigger Events
Impressions
To let Personalize know that a particular experience is being shown to the user, you can trigger an impression event. The following snippet shows how to trigger impressions:
'use client';
import { useContext, useEffect } from 'react';
import { PersonalizeContext } from '../context/PersonalizeContext';
const Page = () => {
const Personalize = useContext(PersonalizeContext);
useEffect(async () => {
await Personalize.triggerImpression(MY_EXPERIENCE_SHORT_UID);
}, []);
// render the component
}
Conversions
Any action performed by the user can be a conversion event if it leads to a positive outcome. To let Personalize know that an action has been performed, trigger the event as follows:
'use client';
import { useContext } from 'react';
import { PersonalizeContext } from '../context/PersonalizeContext';
const Page = () => {
const Personalize = useContext(PersonalizeContext);
const onClickLearnMore = async (e) => {
e.preventDefault();
await Personalize.triggerEvent('learnMoreClicked'); // here 'learnMoreClicked' is the eventKey
}
// render the component
}
Reference Project
You can refer to the following project for a reference implementation: Next.js example GitHub repository and find it hosted here: https://personalize-demo.contentstackapps.com