Personalized content API

November 11, 2022
Note: Marketing Factory is renamed to jExperience in version 1.11 and Apache Unomi is renamed to jCustomer. The 1.10 documentation has been updated to reflect the product name change.

When integrating with certain applications, you might want to use jExperience’s personalization functionalities without serving full-page HTML content generated by Jahia. For example, you might want to retrieve a list of images that are stored in Jahia, and make sure that personalization or even test optimizations, such as A/B testing, are applied to the elements of the list. In this case, the list of images is stored in Jahia’s CMS and the personalization configuration is stored within the data that is managed by jExperience’s Jahia modules.

If we reduce the example from a list to a single variant element, this is actually very close to what an ad-server might look like, where you want to retrieve a single image for a specific user profile. This image will be delivered based on the criteria (such as segmentation, profile properties, location) that have been set up for personalization. We are in this scenario only interested in this (or these) resource, and not on a full-blown HTML page.

This is now possible in jExperience through the content personalization rendering API (aka personalization API). This API is primarily a content rendering API, and will not cover setting up or modifying existing personalization or test optimization components. These must simply be setup using jExperience’s UI. This should be acceptable in most use cases, where it is acceptable to use a back-end UI to edit/setup things and then use an API to access the result of the applied personalization.

Jahia provides two versions of this API: REST and GraphQL. We recommend using the GraphQL version as the REST version might be phased out in future versions.

Content personalization GraphQL API

Let's look at an example of how to use this.

Steps:

  1. Add a personalization or optimization to your content using the Jahia or Content & Media Manager UI.
  2. Open Jahia GraphQL Core Provider : graphiql in dev Tools.
  3. Try to use the following query if you have set up a personalization.
    {
      jcr {
        nodePerson: nodeByPath(path: "PATH_TO_YOUR_PERSONALIZED_NODE") {
          uuid
          name
          marketingFactory {
            personalizedVariant {
              name
              uuid
              path
              property(name: "text", language: "en") {
                value
              }
            }
          }
        }
      }
    }
    
  4. If you setup an optimization you can use the following query.
    {
      jcr {
        nodeOpti: nodeByPath(path: "PATH_TO_YOUR_OPTIMIZED_NODE") {
          uuid
          name
          marketingFactory {
            optimizedVariant {
              name
              path
              uuid
              property(name: "text", language: "en") {
                value
              }
            }
          }
        }
      }
    }

In the above example you need the path to the personalized or optimized nodes. This is usually something in the form of "/sites/content/PATH" based on what you have setup in the Content & Media Manager module or in your site contents. The nodes you retrieve using these queries are the nodes that match the personalization or optimizations that were setup in jExperience's Personalization and Optimization tests UI. 

You can learn more about the GraphQL API by using the GraphiQL Docs browser and looking at the "marketingFactory" field of the JCRNode type. You can also learn more about Jahia's GraphQL API here.

Content personalization rendering REST API

This rendering API output JSON structures, much in a similar way that Jahia’s REST API does, but it is a bit different in format and especially in functionality. It is also customizable since it uses Jahia views to perform its rendering, making it possible to adjust the final result output using custom modules (for example to customize the JSON output of a specific Jahia component).

Before going into the specific format of the JSON structures, let’s look at the URL format:

http://server:port/CONTEXT/PATH_TO_CONTENT.mf.json?QUERY_PARAMETERS

where:

  • CONTEXT
    Is the context in which the Jahia application is deployed (by default the ROOT context is used in which case CONTEXT should be empty)
  • PATH_TO_CONTENT
    Is the typical path to a content object (without any extension), for example: /sites/digitall/home/texts
  • QUERY_PARAMETERS
    Are parameters described in the following table.
Name Value type Default value Optional Description
hidden boolean false Yes If activated, the JSON output will contain content properties marked as hidden.
pretty boolean false Yes If activated, the JSON output will be indented to make it easier to read. This is useful for debugging but shouldn’t be used in production.
depth integer 1 Yes This parameter controls the depth of the tree that is generated in the JSON output. Usually a depth of 2 is suitable when rendering a list of elements to avoid having to request the elements in separate HTTP requests.
wemSessionId string null Yes This parameter contains a valid jCustomer session ID (usually obtained from a previous request). If not specified, it will first look in the server session for an object named "wemSessionId" and if it cannot be found, a new sessionId is generated by jCustomer. In all cases, the jCustomer session ID will be part of the JSON result.
wemProfileId string null Yes This parameter contains a valid jCustomer profile ID. If not specified, the API will try to find the value either in the context-profile-id cookie, or in a server-side session object named "wemProfileId" and setup by a previous request.
wemPersonaId string null Yes This parameter contains a valid jCustomer Persona ID. If specified and valid, the persona ID is used to perform personalization and test optimizations against the persona instead of the current profile. The list of personas may be retrieved using jCustomer’s Persona API.

Here is an example of such an URL :

http://localhost:8080/sites/digitall/home/texts.mf.json?hidden=true&pretty=true&depth=5

and here in an excerpt of the result (edited for readability):

{
  "path": "/sites/digitall/home/texts",
  "parentPath": "/sites/digitall/home",
  "identifier": "42e3db2a-6394-4f8d-9c0e-7d8ade34ca4d",
  "index": 1,
  "depth": "5",
  "nodename": "texts",
  "properties": {
    "j:nodename": "texts",
    "j:fullpath": "/sites/digitall/home/texts",
    "jcr:createdBy": "root",
    
  },
  "types": {
    "primaryNodeType": "jnt:contentList",
    
  },
  "title": "texts",
  "classes": "selectable",
  "hasChildren": true,
  "childNodes": [
    {
      "path": "/sites/digitall/home/texts/rich-text",
      "parentPath": "/sites/digitall/home/texts",
      "identifier": "9769d43a-bc76-4681-90e1-633b863a9562",
      "index": 1,
      "depth": "4",
      "nodename": "rich-text",
      "properties": {
        "j:nodename": "rich-text",
        "j:fullpath": "/sites/digitall/home/texts/rich-text",
        "jcr:createdBy": "root",
        "text": "<p><strong>Bold Text<\/strong><\/p>\n\n<p>&nbsp;<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        
      },
      "title": "Bold Text",
      "classes": "selectable",
      "hasChildren": false
    },
    {
      "path": "/sites/digitall/home/texts/experience-rich-text-2/rich-text-1",
      "parentPath": "/sites/digitall/home/texts/experience-rich-text-2",
      "identifier": "5ab49763-18b3-4613-973f-c056bf8a3e8b",
      "index": 1,
      "depth": "3",
      "nodename": "rich-text-1",
      "properties": {
        "j:nodename": "rich-text-1",
        "j:fullpath": "/sites/digitall/home/texts/experience-rich-text-2/rich-text-1",
        "jcr:createdBy": "root",
        "wem:tab": "",
        "wem:jsonFilter": "",
        
        "text": "<p>fallback<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        "mixinTypes": ["wemmix:editItem"],
        "parentPrimaryNodeType": "wemnt:personalizedContent",
        "parentMixinTypes": []
      },
      "title": "fallback",
      "classes": "selectable",
      "hasChildren": false
    },
    {
      "path": "/sites/digitall/home/texts/optimization-variant-a/variant-c",
      "parentPath": "/sites/digitall/home/texts/optimization-variant-a",
      "identifier": "3ea48051-6584-4493-9a21-edf72d98b495",
      "index": 1,
      "depth": "3",
      "nodename": "variant-c",
      "properties": {
        "j:nodename": "variant-c",
        "j:fullpath": "/sites/digitall/home/texts/optimization-variant-a/variant-c",
        
        "text": "<p>variant-c<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        "mixinTypes": ["wemmix:editItem"],
        "parentPrimaryNodeType": "wemnt:optimizationTest",
        "parentMixinTypes": []
      },
      "title": "variant-c",
      "classes": "selectable",
      "hasChildren": false
    }
  ],
  "wemSessionId": "1fc2bcb8-99fe-47d1-8432-213ff495d6ff",
  "wemProfileId": "0e4768b6-b129-45a7-a312-19544d112b7b",
  "wemPersonaId": ""
}

In this example, there are multiple things to note:

  • A list of objects is rendered, so this is why you see a root object that has child nodes.
  • The first child is a regular rich text object with a text value.
  • The second child is a personalized rich-text object. As you can see it has a "wemmix:editItem" mixin type and a "wemnt:personalizedContent" parent node type that indicates that you have rendered a variant of a personalized content list. Note that in this output you cannot see the top personalization node (the "wemnt:personalizedContent" personalization node) or the other variants. Only the one that was rendered by jExperience for the current profile is part of the output. This way integrators don’t have to understand all the inner workings of the personalization technology, they can just set it up and get the expected result in JSON format.
  • The third child is an optimization test (aka A/B testing node) node. Again, a variant was selected server-side and only the selected node is part of the output. The top optimization test node and the other variants are not part of the output.

Proper session and profile tracking

Whenever interacting with jCustomer’s API, Jahia’s API or jExperience’s Personalization API, you must ensure that you track and transfer any personalization identifiers that you may have retrieved as part of a request’s response.

For example, if you perform a context retrieval call using jCustomer’s REST API such as :

http://UNOMI_HOST:UNOMI_PORT/context.json?sessionId=MY_SESSION_ID

You will likely retrieve a profileId as a result of this context retrieval. If you provided a new session ID in MY_SESSION_ID, the session will be created in jCustomer with this identifier and you must use that for all subsequent calls to any APIs. For example, if you use the personalization REST API to render content, you will need to do something like this:

http://localhost:8080/sites/digitall/home/texts.mf.json?hidden=true&pretty=true&depth=5&wemSessionId=MY_SESSION_ID&wemProfileId=MY_PROFILE_ID

where MY_PROFILE_ID is the profile ID that was returned as a result of the request to the context.json servlet.

If instead you are using the Personalization GraphQL API, you can specify the sessionId and profileId as arguments to the marketingFactory field as in the following example:

{
  jcr {
    nodePerson: nodeByPath(path: "PATH_TO_YOUR_PERSONALIZED_NODE") {
      uuid
      name
      marketingFactory(profileId: "MY_PROFILE_ID", sessionId: "MY_SESSION_ID" {
        personalization {
          name
          uuid
          path
          property(name: "text", language: "en") {
            value
          }
        }
      }
    }
  }
}

If you do not specify a profileId or sessionId for the GraphQL arguments, that's entirely possible if you are properly managing cookies or passing them as query string parameters to the GraphQL endpoint. However if you are running into problems with session or profile tracking not working properly because of some middleware layer, it might be a good idea to use the field arguments to ensure proper tracking.

It is also recommended that you use HTTP clients that can either handle cookies properly or you will have to manually manage the cookies for proper Jahia server session tracking (for example, the JSESSIONID cookie). As a general rule, check whether the HTTP client you use supports cookies out-of-the-box. If not, you must parse the returned cookies and send them back when performing requests to both Jahia and jCustomer. The following table provides a list of the most important cookies and their usage:

Name Origin Type Description
JSESSIONID Jahia string This cookie is used to track server-side sessions inside Jahia.
context-profile-id Unomi string Used by jCustomer to track the profile ID. If a wemProfileId parameter is specified, it will override the cookie value but it would be best that they always be in sync.

Jahia login session integration

jExperience also provides integration with Jahia’s login servlet. For proper session and profile tracking to occur, you must add a request parameter to the /cms/login servlet when submitting a Jahia login. Here is an example:

http://JAHIA_HOST:JAHIA_PORT/CONTEXT/cms/login?wemSessionId=MY_SESSION_ID

The body of the request is in typical multipart/form-data format (default type for HTTP POST Form submissions) that contains the following fields:

Name Origin Type Description
doLogin boolean Yes Must be set to "true" for the login to be performed
username string Yes The user unique name
password string Yes The user’s password
restMode boolean Yes Must be set to "true" for this use case
redirectActive boolean Yes Must be set to "false" for this use case
site string Yes The site against which to login, which is used mostly to redirect back to the site after the login. In the case of integration with jExperience, this is also used to know in which site to retrieve the jCustomer configuration.
Note: The required column relates to requirements for integration with jExperience. In more general uses cases the requirements are different. Please refer to the Jahia REST API documentation for more precise login servlet information.

Typical personalization flows

Introduction

The following sections describe typical personalization flows that application developers might want to integrate. Note that while the steps are quite detailed, most of them happen automatically when using the personalization API. For example, all interactions between Jahia and jCustomer are handled by jExperience. Other steps, such as the login or the session ID generation, must be explicitly integrated into the custom applications. Implicit operations are indicated with an [I] and explicit operations with an [E].

Conventions

"Get personalized JSON" or "Get test optimization JSON" are not really different requests, they are both requests to the personalization API we have described previously

"DX" refers to the jExperience Jahia modules, not the Jahia server by itself.

"Unomi" refers to the jCustomer Context Server.

Anonymous personalization

The following diagram describes the flow of a typical integration of personalization without authentication into a custom application:

Here is a more detail description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to reuse session IDs, although there could be some cases where it could be useful.
  2. [E] UYou can now request the personalized JSON content, passing in the session ID that was generated, and optionally any previously stored profile ID (for example, from previous requests) and an optional persona ID if you want to render the result for a given persona instead of the current profile.
  3. [I] The jExperience Jahia modules collect all the filters setup for each variant, as well as the personalization configuration for each personalization content node.
  4. [I] Jahia sends a request to jCustomer that contains the filters to execute against the context that corresponds to the current session profile and persona.
  5. [I] The jCustomer executes the filters and returns the context that contains the session and profile properties, along with the results of the filters execution.
  6. [I] The Jahia server uses the filter results to filter the content and returns the JSON with the filtered personalized content, as well as the session ID, the profile ID (warning it might change in the case of profile merging) and the persona ID if one was used.

Authenticated personalization

The following diagram describes the flow of a typical integration of personalization with authentication into a custom application:

Here is a more detailed description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to reuse session IDs, although there could be some cases where it could be useful.
  2. [E] This first context request is required because only the context servlet in jCustomer is allowed to create new sessions.
  3. [I] The context is returned as a response of the request, and it contains a JSON structure that contains the session ID, profile ID, profile and session properties
  4. [E] A login POST request is sent to Jahia with the required login credentials.
  5. [I] jExperience detects the login. If it is successful jExperience generates a login event and sends it to jCustomer. This event contains all the user properties except the password, to augment the existing jCustomer profile. Also this may trigger profile merging operations inside jCustomer if multiple profiles are referencing the same profile (this typically happens when a user logs in from different clients).
  6. [I] Jahia returns the result of login. If the login was successful it simply returns "OK" as a text response body.
  7. [E] The app issues a second context request to get the updated profile and session properties that were the result of the login request. This is necessary because the login event was send from server-to-server and we therefore don’t get access to the updated properties directly. Again the proper session ID must be passed to the context request.
  8. [I] The updated context is returned.
    Important note: The profile ID might have changed because of merging. Make sure you read the profile ID and compare it to values you might have and update them with the profile ID returned in this response.
  9. [E] You can now request the personalized JSON content, passing in the session ID that was generated, and optionally any previously stored profile ID (from previous requests for example) and an optional persona ID if you want to render the result for a given persona instead of the current profile.
  10. [I] The jExperience Jahia modules collect all the filters setup for each variant, as well as the personalization configuration for each personalization content node.
  11. [I] Jahia then sends a request to jCustomer that contains the filters to execute against the context that corresponds to the current session profile and persona.
  12. [I] jCustomer executes the filters and returns the context that contains the session and profile properties, along with the results of the filters execution.
  13. [I] The Jahia server returns the JSON with the personalized content, as well as the session ID, the profileId (warning it might change in the case of profile merging) and the personaId if one was used.

Anonymous test optimization

The following diagram describes the flow of a typical integration of anonymous test optimization into a custom application:

Here is a more detailed description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to reuse session IDs, although there could be some cases where it could be useful.
  2. [E] The application sends a request for the JSON structure resulting of the test optimization for the current profile, session and optional persona selected. Note that the only required parameter is the session ID.
  3. [I] Jahia then asks jCustomer for the current context, because it uses a session property to store the selected variant ID for the test optimization. If it is found, skip directly to step 5
  4. [I] jCustomer sends back the context corresponding to the request session, profile and persona.
  5. [I] If a variant was already selected, it is read from the session properties that were send in the context and you skip to step 8. If no variant was selected, Jahia selects a variant using the configured test optimization setup.
  6. [I] If no variant was selected, Jahia send a variant selection event to jCustomer to notify it and any rules related to variant selection are executed.
  7. [I] If no variant was selected, Jahia sends a request to save the selected variant ID in the session.
    Note: This still will probably disappear in future versions of jExperience, and directly integrated into a rule in the jCustomer server.
  8. [I] The JSON content corresponding to the test optimization for the current session, profile and persona is returned, along with the identifiers for the session,profile and persona.