Personalizing website content with Uniform and Netlify Edge Functions: a tutorial

Before starting this tutorial, learn these two key terms:
  • Personalization, which is content adapted to visitors aimed at enhancing their experience and fostering conversions. You can personalize various criteria, e.g.:
    • The location
    • The date and time
    • A previous activity
    • The current activity
    • The information disclosed by the visitor
    • The information inferred from the visitor's behavior
For example, if a visitor clicks a personalized page but soon bounces off, you could enhance that individual’s next visit based on that behavior.
  • Edge-side personalization, which enables personalization to run on a content delivery network (CDN), whose servers cache content in a location closest to visitors.
This tutorial shows you how to personalize website content with Uniform and Netlify Edge Functions.The source code, written in Stackblitz, is on GitHub.

Understanding the prerequisites

To take this tutorial, you must have a working knowledge of—
In addition, you must have a Uniformaccount, which you can sign up for free; a Uniform project; and a Uniform API key. You also need a Netlify account.

Setting up a Uniform project

First, create a project on the Uniform dashboard. Follow the steps below:
  1. Log in to the Uniform dashboard at https://uniform.app. This page is then displayed:
    Uniform dashboard
  2. Click the red(+) icon to create a project called Website Content PersonalizationClick to copy. The project dashboard is then displayed.
    Project dashboard
  3. Add an API key for your web app to read data and settings from your Uniform project. Start by clicking the Security tab on the Uniform dashboard and then click API Keys.
    API keys
  4. Click the red(+)icon to add an API key and name it Website Content PersonalizationClick to copy Key. Set the following permissions:
    1. Uniform Canvas > Compositions > Read Draft
    2. Uniform Canvas > Compositions > Read Published
    3. Uniform Context > Read Drafts
    4. Uniform Context > Manifest > Read
  5. Click Create API Key.
  6. Copy the values of the API key and project ID and set them aside for use later.
Note: The API key is displayed only once. If you don’t copy it now, you’ll have to create a new one later.
Creation of an API key

Building a web app

Now build a Next.js project:
  1. Open a terminal and run this command line to scaffold a Next.js app: npx create-next-app website-content-personalizationClick to copy
  2. Go to your project directory and start the development server on localhost:3000 with this command: cd website-content-personalization && npm run devClick to copy
  3. Rename the styles/Home.module.cssClick to copy file to styles/pages.cssClick to copy to make all the styles globally available and streamline the file’s content to adopt only the specified styles.
    1.container {
    2  padding: 0 2rem;
    3}
    4.main {
    5  min-height: 100vh;
    6  padding: 4rem 0;
    7  flex: 1;
    8  display: flex;
    9  flex-direction: column;
    10  justify-content: center;
    11  align-items: center;
    12}
    13.title {
    14  margin: 0;
    15  line-height: 1.15;
    16  font-size: 4rem;
    17}
    18.title,
    19.description {
    20  text-align: center;
    21}
    22.description {
    23  margin: 4rem 0;
    24  line-height: 1.5;
    25  font-size: 1.5rem;
    26}
    1@media (max-width: 600px) {
    2  .grid {
    3    width: 100%;
    4    flex-direction: column;
    5  }
    6}
    7@media (prefers-color-scheme: dark) {
    8  .card,
    9  .footer {
    10    border-color: #222;
    11  }
    12  .code {
    13    background: #111;
    14  }
    15  .logo img {
    16    filter: invert(1);
    17  }
    18}
  4. Import the styles into the `pages/_app.js` file with this code:
    1import '../styles/globals.css'
    2  import '../styles/pages.css'
    1  function MyApp({ Component, pageProps }) {
    2    return <Component {...pageProps} />
    3  }
    4  export default MyApp
  5. Remove the import of Home.module.cssClick to copy from pages/index.jsClick to copy. Also, create a .envClick to copy file in your project’s root directory for your environmental variables. Store in .envClick to copy the value of Uniform’s API key and project ID, which you copied in step 6 of the previous section.
    1UNIFORM_API_KEY=[!!!API KEY!!!]
    2  UNIFORM_PROJECT_ID=[!!!PROJECT ID!!!]
  6. Create an srcClick to copy folder in the project’s root directory and then a componentsClick to copy folder under srcClick to copy.
  7. Create a Body.jsxClick to copy component with the code below:
    1export default function Body(){
    2  return(
    3      <div>
    4          <h1 className="title">Website Content</h1>
    5          <p className="description">
    6            This is a website content personalization tutorial
    7          </p>
    8      </div>
    9    )
    10}

Activating classification

Classification, an essential feature of personalization, considers the known facts about a website visitor at a point in time and then determines what content to serve. For example, if a visitor is from Germany, you might classify that individual as a resident there and, accordingly, recommend products that are available in the German market. 
Uniform offers components that handle classification, which you’ll add to your app. Follow the steps below to activate classification.
  1. Open a terminal window in your project’s root directory and run the following command to install the packages for React apps that use Uniform: npm install @uniformdev/context @uniformdev/context-reactClick to copy
  2. Install the Uniform CLI, the command-line interface through which you can interact with Uniform: npm install -D @uniformdev/cliClick to copy
  3. Edit the package.json Click to copyfile and modify the scriptsClick to copy object, as follows:
    1"scripts": {
    2    "dev": "npm run download:manifest && next dev",
    3    "build": "npm run download:manifest && next build",
    4    "download:manifest": "uniform context manifest download --output ./contextManifest.json",
    5    "start": "next start"
    6  },
    This step adds a script to the Next.js devClick to copy and buildClick to copy processes to download from Uniform the contextClick to copy manifest, which contains the instructions on how to classify visitors.
  4. Run this command to download the manifest and save it in the contextManifest.jsonClick to copy file: npm run download:manifestClick to copy
  5. Edit the pages/_app.jsClick to copy file to read like this:
    1import { UniformContext } from "@uniformdev/context-react";
    2import { Context, enableContextDevTools } from "@uniformdev/context";
    3import manifest from "../contextManifest.json";
    4import '../styles/globals.css'
    5import '../styles/pages.css'
    6const context = new Context({
    7  manifest,
    8  defaultConsent: true,
    9  plugins: [
    10    enableContextDevTools(),
    11  ],
    12});
    13function MyApp({ Component, pageProps }) {
    14  return (
    15    <UniformContext context={context}>
    16      <Component {...pageProps} />
    17    </UniformContext>
    18  )
    19}
    20export default MyApp
The highlighted code above does the following:
  • Imports the custom React contextClick to copy object.
  • Imports ContextClick to copy and enableContextDevToolsClick to copy, through which the browser’s developer tools can access the state details from Uniform’s contextClick to copy object.
  • Imports the manifest from the contextManifest.jsonClick to copy file you saved in a previous step.
  • Adds and assigns the manifest to the Uniform contextClick to copy, sets the default consent for classification on your web app to trueClick to copy, and sets the enableContextDevToolsClick to copy plugin.
  • Leverages the custom ReactClick to copy object for rendering the component.

Defining the web app’s layout on Uniform

You configure personalization in Uniform with a tool called Uniform Canvas through the components displayed in your web app’s frontend. 
First, create your applayout with Uniform Canvas, as follows:
  1. In your Uniform project, click Canvas and then Component Library.
    Component Library
  2. Click the red (+) icon to create a component library and type BodyClick to copy under Component Name, which then prefills the Public ID field with bodyClick to copy. Under Component Icon, select Menu BoxedClick to copy.
    Add Component Type
  3. Click the red (+) icon under the Parameters tab to create a parameter for the component with the following properties and then click OK:
    1. Parameter Name: Content IDClick to copy
    2. Public ID: contentIdClick to copy
    3. Type: TextClick to copy
    4. Required: TrueClick to copy
  4. Click Save and close.
Body component
Next, create a PageClick to copy component, a composition component that embeds the BodyClick to copy component. Follow the steps below.
  1. Click the red(+)icon as displayed in the above image.
  2. Enter the following
    • Component Name: PageClick to copy
    • Public ID: pageClick to copy
    • Component Icon: file-documentClick to copy
    • Composition Component: TrueClick to copy
  3. Click the Slots tab. A slot is a spot with which you personalize a webpage. Based on the slot’s configuration, Uniform determines which component to place in the slot and what facts are known about the visitor as a result of that individual’s activities on the webpage.
    Setup for slots
  4. Click the red (+) button and enter the following values in the form that is displayed:
    1. Slot Name: bodySlotClick to copy
    2. Public ID: bodySlotClick to copy
    3. Minimum: 1Click to copy
    4. Maximum: 1Click to copy
    5. Allowed Components: Select Specify allowed components, followed by Body and Personalization.
      Slot
5. Click OK and then Save and close.
Now create a composition that represents your index page:
  1. Click Compositions and then the red (+) button to create a composition.
    Compositions
  2. In the Add a composition form that is displayed, select the Page component under Select a composition type and type Index PageClick to copy under Name
    Add a composition
  3. Click the Create button and type /Click to copy in the Slug field.
    slug
  4. Click the green(+) button and select the Body component.
    Body
  5. Type indexClick to copy under Content ID and click Save.

Publishing the web app on Netlify

You’ve now created a simple web app with a webpage that displays text. Deploy the app to Netlify by doing the following:
  1. Login to your Netlify account. This page is then displayed:
    Netlify account
  2. Import the source from GitHub by first clicking Import from Git.
    Source import from GitHub
  3. Select the repository and branch to be deployed. In your case, you have only the mainClick to copy branch, and Netlify is smart enough to detect the project.
    Deploy site
  4. Click Deploy site to start the build and deploy process. When the process is complete, your site will be up and running.

Integrating the Uniform project with Netlify

Now that your web app is running on Netlify, which supports edge-side personalization, connect your Uniform project to Netlify to harness the features. Perform the integration as follows:

Create a Netlify access token.

  1. Navigate to Netlify’s personal access tokens folder.
  2. Click New Access Token, type a description for the token, and then click Generate token
  3. Copy the token and paste it somewhere. You won’t be able access it after clicking away.

Integrate with Uniform

  1. Click the Integrations tab in the Uniform project.
    Netlify integration
  2. In the Integration search field, type NetlifyClick to copy, press Enter, and then click the Netlify option.
    Netlify integration notification
  3. Click Add to project, paste the access token that you copied in step 3 of the previous subsection, and click Continue. Select your Netlify account under Select Account and, under Select Site, the site that you deployed earlier.
    Netlify settings
    Note: You can use the web-hook feature by selecting a build hook, but that’s outside the scope of this tutorial.
  4. Click Save.
Now that you’ve integrated Netlify with your Uniform project, configure personalization based on Netlify’sgeodata available at the edge. Do so through quirks, which are key-value pairs offered programmatically—in your case, through Netlify Edge.
To view the Netlify quirks, click the Personalization tab in your Uniform project and then Quirks.
Quirks

Configuring personalization 

Now configure personalization in your Uniform project based on a visitor’s location. The steps below personalize for two locations, Germany and Poland.

Add signals

As its name implies, a signal is an occurrence you want to track and respond to. For example, a signal could indicate that a visitor is in a particular location, based on which Uniform then displays content specific to that location. 
To create two signals, one for Germany and the other for Poland, do the following:
  1. Click the Personalization tab and then Signals.
    Signal creation
  2. Click the red (+) icon to add a signal.
  3. Click Quirks on the left navigation and type two signal names: Is In PolandClick to copy and Is In GermanyClick to copy.
    Signal name
  4. Select Country Code under Quirk Name and equals under Comparison. Under Match, type PLClick to copy for Poland and DEClick to copy for Germany.
    Signal match
  5. Click Save and Close.
    Signals
  6. Click Publish to make the signals available.

Personalize a component

Next, add a personalization component to the slot you created in your composition component:
  1. Navigate to Canvas > Compositions.
  2. Click the Index PageClick to copy composition.
  3. Click the BodyClick to copy component and then click Personalize this.
    Personalization
  4. Click the Personalization component and type Website-content-personalizationClick to copy under Analytics tracking name. Set the number of variations to 1.
  5. Click the green(+) icon between the PersonalizationClick to copy and BodyClick to copy components and then select the BodyClick to copy component. Repeat to add another BodyClick to copy component.
    Content ID
  6. Under Content IDs for the two new components, type isInPolandClick to copy and isInGermanyClick to copy, as appropriate.
  7. Select the BodyClick to copy component with the isInPolandClick to copy content ID and click Add CriteriaClick to copy. Add Is in Poland under Personalize This > Set Signal and set the dimension to 50.
    Composition
  8. Choose Save and Publish from the drop-down menu of the Save button.

Activate personalization in your web app

Now update your web app with the personalization parameters you configured in the previous section. Uniform runs the related instructions and determines which components and what content are appropriate.
  1. Run this command in the project directory to add references to the packages for the React apps that use Uniform Canvas: npm install @uniformdev/canvas @uniformdev/canvas-reactClick to copy
  2. Create a folder called contentClick to copy in your project directory and then, in that folder, create a file called content.jsonClick to copy with this code:
    1[
    2    {
    3        "id": "index",
    4        "url": "/",
    5        "fields": {
    6          "title": "Home",
    7          "description": "This is the home page"
    8        }
    9      },
    10      {
    11        "id": "isInPoland",
    12        "fields": {
    13          "title": "Poland",
    14          "description": "I am in Poland"
    15        }
    16      },
    17      {
    18        "id": "isInGermany",
    19        "fields": {
    20          "title": "Germany",
    21          "description": "I am in Germany"
    22        }
    23      }
    24
    Note: Verify that the idClick to copy property matches the value of content IdClick to copy of the Uniform components.
    Body parameters
  3. Create a libClick to copy folder in the project’s root directory and, under libClick to copy, create a file called enhancer.jsClick to copy with the code below: import { enhance, EnhancerBuilder } from "@uniformdev/canvas";Click to copy
    import content from "../content/content.json";Click to copy
    1const dataEnhancer = async ({ component }) => {
    2  const contentId = component?.parameters?.contentId?.value;
    3  if (contentId) {
    4    const topic = content.find((e) => e.id == contentId);
    5    if (topic) {
    6      return { ...topic.fields };
    7    }
    8  }
    9};
    1export default async function doEnhance(composition) {
    2  const enhancedComposition = { ...composition };
    3  const enhancers = new EnhancerBuilder().data("fields", dataEnhancer);
    4  await enhance({
    5    composition: enhancedComposition,
    6    enhancers,
    7  });
    1  return enhancedComposition;
    2}
    The code above does the following:
    1. Imports the enhanceClick to copy method and the EnhancerBuilderClick to copy object from Uniform Canvas.
    2. Imports the content in the content.jsonClick to copy file.
    3. Checks the value of content IdClick to copy in Uniform Canvas against the value of idClick to copy in the content.jsonClick to copy file and returns the fields property.
    4. Enhances the data and makes it available to the front end.
  4. Create a file called lib/resolveRenderer.jsClick to copy with the code below: import Body from "../src/components/Body";Click to copy
    1function UnknownComponent(component) {
    2  return <div>[unknown component: {component.type}]</div>;
    3}
    1export default function resolveRenderer({ type }) {
    2  if (type == "body") {
    3    return Body;
    4  }
    5  return UnknownComponent;
    6}
    The code above maps the component BodyClick to copy with the public ID of bodyClick to copy in your Uniform project and your web app’s component BodyClick to copy.
  5. Edit the src/components/Body.jsxClick to copy file to read like this: import React, { useEffect, useState } from 'react'Click to copy
    1export default function Body(props){
    2  const { fields } = props;
    3  const [title, setTitle] = useState();
    4  const [description, setDescription] = useState();
    1  useEffect(() => {
    2    setTitle(fields.title);
    3    setDescription(fields.description);
    4  }, []);
    1    return(
    2        <div>
    3            <h1 className="title">{title}</h1>
    4            <p className="description">{description}</p>
    5        </div>
    6    )
    7}
    The code above displays the titleClick to copy and descriptionClick to copy properties from the content.jsonClick to copy file.
  6. Create a file called LayoutCanvas.jsxClick to copy under src/componentsClick to copy with the code below:
    1import Head from "next/head";
    2import { Slot } from "@uniformdev/canvas-react";
    1export default function LayoutCanvas({ title }) {
    2  return (
    3    <div className="container">
    4      <Head>
    5        <title>{title}</title>
    6        <link rel="icon" href="/favicon.ico" />
    7      </Head>
    8      <Slot name="bodySlot" />
    9    </div>
    10  );
    11
    The code above adds a layout for Uniform Canvas in your Uniform project. Be sure to specify the correct slot name in the SlotClick to copy component.
  7. Edit the pages/index.jsClick to copy file to read like this:
    1import { CanvasClient } from "@uniformdev/canvas";
    2import { Composition } from "@uniformdev/canvas-react";
    import LayoutCanvas from "../src/components/LayoutCanvas";Click to copy
    1import content from "../content/content.json";
    2import doEnhance from "../lib/enhancer";
    3import resolveRenderer from "../lib/resolveRenderer";
    1async function getComposition(slug) {
    2  const client = new CanvasClient({
    3    apiKey: process.env.UNIFORM_API_KEY,
    4    projectId: process.env.UNIFORM_PROJECT_ID,
    5  });
    6  const { composition } = await client.getCompositionBySlug({
    7    slug,
    8  });
    9  return composition;
    10}
    1export async function getStaticProps() {
    2  const slug = "/";
    3  const topic = content.find((e) => e.url == slug);
    const composition = await getComposition(slug);Click to copy
    1  await doEnhance(composition);
    2  return {
    3    props: {
    4      composition,
    5      fields: topic.fields,
    6    },
    7  };
    8}
    1export default function Home({ composition, fields }) {
    2  return (
    3    <Composition data={composition} resolveRenderer={resolveRenderer}>
    4        <LayoutCanvas composition={composition} fields={fields} />
    5    </Composition>
    6  );
    7}
    The code above does the following:
    1. Retrieves the composition from Uniform with the API key and project ID and then returns the composition.
    2. Finds the content.jsonClick to copy file’s url value that matches the slug
      for the topic. 
    3. Retrieves and enhances the composition before returning a propsClick to copy object of the composition and its fields.
    4. Adds to your app the Uniform CompositionClick to copy component, which handles composition tasks and makes use of the Uniform Canvas-aware LayoutCanvasClick to copy component.

Configuring and enabling edge-side personalization on Netlify

  1. Activate edge-side personalization in your app by running the command below to add the package: npm i @uniformdev/context-nextClick to copy
  2. Edit the pages/_app.jsClick to copy file to make it read like this:
    1import { UniformContext } from "@uniformdev/context-react";
    2import { Context, enableContextDevTools } from "@uniformdev/context";
    3import manifest from "../contextManifest.json";
    4import { NextCookieTransitionDataStore } from "@uniformdev/context-next";
    5import '../styles/globals.css'
    6import '../styles/pages.css'
    7const context = new Context({
    8  manifest,
    9  defaultConsent: true,
    10  transitionStore: new NextCookieTransitionDataStore({}),
    11  plugins: [
    12    enableContextDevTools(),
    13  ],
    14});
    15function MyApp({ Component, pageProps }) {
    16  return (
    17    <UniformContext context={context} outputType="edge">
    18      <Component {...pageProps} />
    19    </UniformContext>
    20  )
    21}
    22export default MyApp
    The code above shows the new additions for activating edge-side personalization in your app.
Next, connect your app to Netlify with the Netlify CLI:
  1. Go to your project directory on a terminal and run this command to authenticate against Netlify: netlify loginClick to copy
  2. Type the command below to link your project to your Netlify site name, which you can obtain by clicking Site Overview > Site Details:
    1netlify link --name [!!! YOUR NETLIFY SITE NAME !!!
    Netlify site name
  3. Add the environment variable NPM_TOKEN Click to copyto your machine. Note: You can obtain the value of this environment variable only by contacting Uniform. Do not save NPM_TOKENClick to copy in your envClick to copy file because that file only defines the variables within your app whereas NPM_TOKENClick to copy is for npm to obtain private packages.
  4. Create a file called .npmrcClick to copy in your project’s root directory with the code below, which specifies the NPM_TOKENClick to copy value you obtained from Uniform.
    1//registry.npmjs.org/:_authToken=${NPM_TOKEN}
    2  engine-strict = true
  5. Add two packages, context-edge-netlifyClick to copy and cpxClick to copy, with this command:
    1 npm i @uniformdev/context-edge-netlify
    2  npm i -D cpx
    @uniformdev/context-edge-netlifyClick to copy contains components for executing personalization instructions in Netlify Edge Functions. cpxClick to copy copies files across environments (Windows and non-Windows).
  6. Add this command to the package.jsonClick to copy file:
    1{
    2  ...
    3  "scripts": {
    4    "copy:deno": "cpx node_modules/@uniformdev/context-edge-netlify/dist/index.deno.js lib/uniform",
    5    ...
    The script above copies the file generated during the build process to the destination where it will be picked up at buildtime.
  7. Run this command: npm run copy:denoClick to copy
  8. Add a file called /netlify/edge-functions/context-middleware.jsClick to copy with this code:
    1import {
    2  createEdgeContext,
    3  createUniformEdgeHandler,
    4  buildNetlifyQuirks
    5} from "../../lib/uniform/index.deno.js";
    6import manifest from "../../contextManifest.json" assert { type: "json" };
    const IGNORED_PATHS = /\/.*\.(ico|png|jpg|jpeg|svg|css|js|json)(?:\?.*|$)$/g;Click to copy
    1export default async (request, netlifyContext) => {
    2  if (
    3    request.method.toUpperCase() !== "GET" ||
    4    request.url.match(IGNORED_PATHS)
    5  ) {
    6    return await netlifyContext.next({ sendConditionalRequest: true });
    7  }
    1  const context = createEdgeContext({
    2    manifest,
    3    request,
    4  });
    const originResponse = await netlifyContext.next();Click to copy const handler = createUniformEdgeHandler();Click to copy
    1  const { processed, response } = await handler({
    2    context,
    3    request,
    4    response: originResponse,
    5    quirks: buildNetlifyQuirks(netlifyContext)
    6  });
    1  if (!processed) {
    2    return response;
    3  }
    1  return new Response(response.body, {
    2    ...response,
    3    headers: {
    4      ...response.headers,
    5      "Cache-Control": "no-store, must-revalidate",
    6      Expires: "0",
    7    },
    8  });
    1};
    2
    The code above does the following:
    1. Imports the createEdgeContextClick to copy, createUniformEdgeHandlerClick to copy, and buildNetlifyQuirksClick to copy functions from the file you copied in the previous step.
    2. Imports the manifest from contextManifest.jsonClick to copy .
    3. Checks the requestClick to copy method and, if it is not a GETClick to copy function or if it matches the paths to be ignored, does not execute that method.
    4. Creates EdgeClick to copy ContextClick to copy with manifestClick to copy and requestClick to copy.
    5. Creates a handler that makes a request to the origin and executes personalization instructions.
    6. Passes the objects into your handler, one of which is your quirks built from the Netlify context.
    7. Returns a response.
  9. Create a file called netlify.tomlClick to copy with the code below:
    1[[plugins]]
    2package = "@netlify/plugin-nextjs"
    1[[edge_functions]]
    2path = "/*"
    3function = "context-middleware"
    Once you’ve run the command for building your app on the Netlify CLI, Netlify executes the process with the plugin specified in this file, which is Next.jsClick to copy in your case. Netlify routes all requests to your Edge Function context-middleware.Click to copy
  10. Confirm if your Edge Function works locally with this command: netlify devClick to copy
  11. Run this command to deploy the app to Netlify: netlify deploy --build --dir .next --prodClick to copy
After deployment is complete, click the site URL to view the app. If I am in Poland at this time, the content for Poland is displayed. 
Personalized content for Poland
To view the signals and quirks, install the Uniform context extension tools from Chrome
Personalized content for Poland (2)
The image above shows that the is FromPolandClick to copy signal has been triggered.
If I am in Germany at this time, running the web app on Netlify shows the personalized content for Germany.
Personalized content for Germany (1)
And the dimension on Uniform’s contextClick to copy extension confirms that.
Personalized content for Germany (2)

Summing it up

This tutorial describes how to do the following:
  1. Build a Uniform project, set it up with a Next.js web app, and deploy the app on Netlify. 
  2. Create components and compositions to personalize webpages on Uniform.
  3. Integrate Netlify Edge Functions for personalization into a project.
Here are a few resources from Uniform’s documentation you might find helpful:
Guides/Personalizing website content with Uniform and Netlify Edge Functions: a tutorial