Personalize the Jamstack with Prismic Slicemachine, Uniform, and Nuxt

tim-2020-small
Tim BenniksPosted on Aug 2, 2021
12 min read
Home/Blog/Personalize Jamstack with Prismic

Why Personalize your website?

The story between a user and the content on your website is important. If it becomes a love story, they have a great experience. If not, you will see the bounce rate go way up. You don't ask your date to get married the first time you see them right? You woo your partner, you go on a few dates and over time you are ready to pop the question (of give them the key to your apartment - or whatever floats your boat).
When users experience content that is just right for them, they love it and they will have much better conversion. You invest in your user and you build a bond of trust for the future. To make sure content fits the intent of the user, you need personalization. Content needs to adapt automatically based on the signals the user gives off while browsing.
But why don't we see personalization more often? As it turns out, traditional personalization is hard to implement and often significantly impacts page load speed. With Uniform we've made it simpler to personalise pages and there is very small overhead.
In this blogpost we will discuss a new way of personalization that is easy to use. It's API first and fit's perfectly with Prismic Slicemachine and Nuxt Jamstack websites.

TL:DR

Check out the Uniform Prismic Nuxt Example site on Github. It holds all the code explained in this post.

How to personalize the Uniform way

The philosophy behind personalization at Uniform is based on this simple paradigm:
Visitors to your site have an intent, something they want to accomplish. A reason for their visit.
Personalization aims to make it as easy as possible for the visitor to accomplish their intent. With Uniform, you can build up a real-time intent profile of every visitor and use this profile to make personalization decisions.
Uniform identifies a visitor's intent by signals that they send. Signals cause a change of the visitor's score in a specific intent. Signals can be behavior (visiting pages), a predefined cookie, events like adding something to the shopping cart or a query string from a marketing campaign.
The Uniform integration with a CMS allows the content editor to tag content with "intent tags" so it can later be used for personalization. These intent tags are configured within the Uniform Optimize dashboard and become available in the CMS through a custom Uniform field.
To make personalization work within the data model of the CMS, a wrapper "personalized" component is created and within this component all personalization variants of the component are referenced.
When a user visits a specific page that has behavior tracking on it, Uniform tracks this user and remembers the intent. Let's say the homepage has a personalized hero component. When the user returns back to the homepage, the hero component now shows the hero variant that is tagged against the intent of the user.
Uniform's Personalization component receives the variants for personalization, it checks the intent profile and its scoring and will then render the right variant for the user.
Everything happens on hydration and no API calls have to be made. We like to say: "Look ma, no origin!" Everything happens statically on the CDN edge.

How to make Uniform personalization work in Prismic

There are a few configurations needed to get Uniform working with Prismic.
  1. Integration field implementations in Prismic are relatively limited and opinionated to selecting things from lists with a specific JSON structure. In this post we will show you how to do that with Uniform "Intent Tags".
  2. Slices cannot be added into other slices in Prismic and therefore we can't create a "Personalized Wrapper slice" that holds the personalization variant slices for Uniform's personalization engine to choose from. We will do some data model magic to make this work as well.

The integration field

First things first, request Prismic support to enable integration fields for your Prismic repo. Once this is out of the way we can get started. To be able to use a Prismic Integration field it needs a result set with a specific data model. This page shows a lot more info.
1{
2  "results_size": 144,
3  "results": [
4    {
5      "id": "my_item_id",
6      "title": "Item Title",
7      "description" : "Description of the item.",
8      "image_url" : "http://...",
9      "last_update" : 1509364426938,
10      "blob": {
11        // ...Uniform Intent Tag information
12      }
13    },
14    // ...
15  ]
16}
To be able to get this data model it is advised to create your own endpoint that maps the Uniform intent data to this model. In this example we use a lambda function from Netlify. A simple version could look like this (add your own error handling and defensive coding as needed):
1const fetch = require('node-fetch')
2
3function mapIntents(intents) {
4  return intents.map((intent) => {
5    return {
6      id: intent.id,
7      title: intent.name,
8      description: intent.description,
9      image_url:
10   'https://pbs.twimg.com/profile_images/1235674864649830400/kd3pN6iU_400x400.jpg',
11      last_update: Date.now(),
12      blob: {
13        [intent.id]: {
14          str: intent.signals[0].str,
15        },
16      },
17    }
18  })
19}
20
21exports.handler = async (event, context) => {
22  const res = await fetch('https://uniform.app/api/v1/preview', {
23    method: 'get',
24    headers: {
25      'x-api-key': 'YOUR_UNIFORM_API_KEY',
26    },
27  })
28
29  const data = await res.json()
30
31  return {
32    statusCode: 200,
33    headers: {
34      'Content-Type': 'application/json',
35    },
36    body: JSON.stringify({
37      results_size: data.site.intents.length,
38      results: mapIntents(data.site.intents),
39    }),
40  }
41}
To make this work you need a Uniform API key. To learn how to set that up feel free to explore our documentation.
Now that the Netlify function is in place, let's set up the integration field in Prismic. Use your own Netlify URL for the endpoint. An access token is not needed here. As you control the code for the endpoint, you can store your Uniform API key in an environment variable in Netlify.

The slices setup

As mentioned above, slices cannot reference other slices as sub slices. The ideal scenario is having a personalization wrapper slice with sub slices that function as variants. These variants are tagged with a Uniform Intent Tag. Based on the signals a user gives (visiting pages, a query string, cookie, etc) the Uniform tracker will calculate a score against the intent of the user. The intent with the highest score will match tagged variant slice and the `<personalize>` component will show it.

The project

Let's create a hypothetical project where people either visit a "developers" or a "marketers" section of the website. When the visit one of these sections and goes back home, the homepage hero will show a personalized variant specific to developers or marketers.
The above setup is not possible so we need to take an alternative route. We will create the following:
  1. A page content type with two slices
    1. Personalized Hero
    2. Hero (A normal hero for non-personalized content)
  2. A hero content type (this type is used to reference from the personalized hero slice.

The Page content type with slices

The page content type with two slices.
The personalized hero slice has a reference field in the repeatable zone that only accepts "hero" items.
The Hero slice is just a simple slice with a title, description and text for non-personalized use.

The Hero content type

The hero content type becomes the personalization variation which is tagged with a Uniform Intent Tag that comes from the Integration field.

Content editing for personalization

Now that we have created the data model, let's create some content that can actually be personalized. Before we can add the Personalization wrapper slice to the page we first need hero content items with Intent Tags attached to be able to fill it.
Let's create a Hero content item.
Create a hero for Marketer, Developer and one without an Intent Tag (this will serve as the non-personalized variant.
Now that the Hero content items have been created, add a home page and put a personalized hero slice on it.
The personalization part is now done. The Uniform <personalize>Click to copy component can choose the right hero variant based on the signals from the user and the Intent Tags attached to the hero's.
But, before it can do this, we need to create pages for "Developers" and "Marketers" so signals can be send to the Uniform Tracker.
Create a new page type for marketers and now use the Hero "slice" we created before.
As you can see this Hero "slice" also has a Uniform Intent Tag. We have done this so that when this component shows on the page, we check what Intent it has been tagged with and we send a tracking event to Uniform with that intent tag.
1// this.$uniformOptimize comes from Nuxt context
2// this.data.intentTag comes from the Prismic Slice data
3this.$uniformOptimize.trackBehavior(this.data.intentTag)
This means that these Hero slices automatically send tracking events to Uniform when the mount.

This is what it looks like

How it connects in the codebase

To use with Nuxt install the Uniform Optimize Nuxt module.
1// Nuxt.config.js
2// For module options see the docs
3module.exports = {
4  modules: ['@uniformdev/optimize-nuxt']
5}
In your _uid.vue we use a normal `<slice-zone>` but with an extra fetchLinks param for the slice reference fields.
1<template>
2  <main>
3    <slice-zone
4	type="page"
5	:uid="$route.params.uid" 
6	:params="{
7        fetchLinks: links,
8      }"
9    />
10  </main>
11</template>
12
13<script>
14import SliceZone from 'vue-slicezone'
15
16export default {
17  components: {
18    SliceZone,
19  },
20  data() {
21    return {
22      links: `hero.title,
23        hero.description,
24        hero.image,
25        hero.intent_tag`,
26    }
27  },
28	
29  // Uniform: On page reload or query string change
30  // re-evaluate signals for tracking.
31  // This will make sure the intent score is up to date.
32  watch: {
33    '$route.query'() {
34      this.reevaluateSignals()
35    },
36  },
37  mounted() {   
38    // Uniform: On mounted and when the browser has initialized 
39    // re-evaluate signals for tracking.
40    // This will make sure the intent score is up to date.
41    this.$nextTick(() => {
42      this.reevaluateSignals()
43    })
44  },
45  methods: {
46    reevaluateSignals() {
47      if (!this.$uniformOptimizeContext.trackerInitializing) {
48        this.$uniformOptimizeContext.tracker.reevaluateSignals()
49      }
50    },
51  },
52}
53</script>
The Personalized Hero Slice
1<template>
2  <section class="component personalized-hero">
1  <!-- Personalize Vue component needs variant info from Prismic -->    
2    <personalize :variations="variants">
1    <!--
2	In its default slot it gives the correct variant(s) data to
3        show for personalization
4    -->
5      <template #default="{ variations, personalized }">
6        <Hero :data="variations[0]" :tracking="false" />
7        personalized: {{ personalized }}
8      </template>
9    </personalize>
10  </section>
11</template>
1<script>
2import { Personalize } from '@uniformdev/optimize-tracker-vue'
3import Hero from '../../components/Hero.vue'
4export default {
5  components: {
6    Personalize,
1    // The hero component code is shared
2    // between the hero slice and the hero content type.
3    Hero,
4  },
5  props: {
6    slice: { type: Object, required: true },
7  },
8  computed: {
9    // Create a variants object that the Personalize component understands.
10    variants() {
11      return this.slice.items.map((hero) => {
12        return {
13          ...hero.hero_options.data,
14          intentTag: this.mapIntentTagData(hero.hero_options.data.intent_tag),
15        }
16      })
17    },
18  },
19  methods: {
20    // Map Prismic Integration Field data to correct format for 
21    // the personalize component.
22    mapIntentTagData(intentField) {
23      const result = { intents: {} }
24      if (!intentField) {
25        return {}
26      }
27      result.intents = {
28        ...intentField,
29      }
30      return result
31    },
32  },
33}
34</script>
The Hero vue component
1<template>
2  <section class="hero">
3    <!-- In real life: use Prismic DOM and html serializer library for this -->
4    <h1>{{ data.title[0].text }}</h1>
5    <p>{{ data.description[0].text }}</p>
6    <img :src="data.image.url" width="200" />
7  </section>
8</template>
9<script>
10export default {
11  name: 'Hero',
12  props: {
13    data: { type: Object, required: true },
14    
15    // By default the trackign prop is set to true.
16    // This means it sends a behaviour signal to the
17    // Uniform tracker when it mounts.
18    // See mounted() lifecycle hook below.
19    tracking: { type: Boolean, required: false, default: true },
20  },
21  mounted() {
1    // Set tracking to false when the component should not send signal info.
2    // This happens when it shows as a personalization variant.
3    // See the Personalized Hero Slice code above.
4    if (this.tracking) {
5      this.$uniformOptimize.trackBehavior(this.data.intentTag)
6    }
7  },
8}
9</script>

Concluding

We had to jump through hoops a little for personalization to work within the Prismic way of working but in the end this approach is very usable!
Feel free to check out the Uniform Prismic Nuxt Example site on Github. It holds all the code explained in this post.
Happy hacking!