Introduction to Page Model JSON API - BloomReach Experience - Open Source CMS
04-03-2019

Introduction to Page Model JSON API

Introduction

Page Model JSON API was designed to address the two plus factors:

  1. Intuitive, built-in model contribution and aggregation JSON API
  2. Seamless integration with WCMS delivery tier and channel management features

A built-in JSON API should represent a generic resource representation, while the integration with delivery tier framework should represent a dynamic page model, comprised of component representations and their content and domain-specific models, based on flexible page compositions through Channel Manager.

Also, it should be straightforward for developers to contribute any kind of model objects to the aggregated page representation in component implementations, without having to write boilerplate code by themselves.

Page Model JSON API provides an intuitive REST API endpoint. For example, for a channel served at http://localhost:8080/site/, its Page Model JSON API is available through http://localhost:8080/site/resourceapi/. (The submount path, resourceapi, depends on the @hst:pagemodelapi property value. See Configure SPA++ for detail). The JSON API resources represent intuitive models containing all the components, content items, menus, domain specific models, etc. in a generic and effective way. SPAs can consume the API to implement the delivery tier and fully integrate with channel management features as a result.

Developers can also use standard HST APIs such as HstRequest#setModel(String, Object) to participate in contributing content items or domain-specific models to the aggregated page model representation easily. See Model Contribution API for details.

Page Model JSON API

As explained in Configure SPA++, once you configure the Page Model JSON API, it becomes available to SPAs automatically at the child mount path (e.g, /resourceapi), configured by the @hst:pagemodelapi property. Internally, HST Container automatically augments the mount with a child mount, the name of which is set to the @hst:pagemodelapi property value, for Page Model JSON API. This auto-augmented child mount invokes the PageModelPipeline, which is fairly similar to the default website pipeline, except that it does not invoke the rendering phases (i.e., rendering FreeMarker/JSP templates). Instead it processes contributed model collection, page model aggregation and JSON serialization phases.

This makes it easy for SPAs to consume all the models included and aggregated for the page. For example, if the initial page, loading an SPA, is from http://localhost:8080/site/myapp/ or http://localhost:8080/site/myapp/news/, then developers can easily figure out the API endpoint at http://localhost:8080/site/myapp/resourceapi/ or http://localhost:8080/site/myapp/resourceapi/news/. As it uses the built-in HST Mount configurations, it is also easy to create links in the server-side code by using the standard org.hippoecm.hst.core.linking.HstLinkCreator API.

Page Model JSON API follows the core principles of HATEOAS. An SPA enters a REST application through a simple HST-2 navigational URL. An aggregated page model is returned for the SPA to discover all the data within the resource representations to construct the page. Example payloads look like the following:

GET /site/resourceapi/ HTTP/1.1
Host: localhost:8080
Accept: application/json
...

The JSON response is always of the following structure:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
API-Version: 0.9
Content-Type: application/json;charset=UTF-8
Content-Length: ...
...
{
  "id": "r19",
  "_links": {
    "self": {
      "href": "http://localhost:8080/site/resourceapi"
    },
    "site": {
      "href": "http://localhost:8080/site/"
    }
  },
  "page": { ... },
  "content": { ... }
}

The JSON response contains an HTTP body which represents an Aggregated Page Model, described in the next section. On a high level, an Aggregated Page Model contains its identifier, follow-up links, page representation containing components and domain specific models, and content models.

Aggregated Page Model

Aggregated Page Model, which establishes the root level structure of Page Model JSON API responses, can be depicted in a domain model like the following:

Each domain object is described below.

AggregatedPageModel

This defines the root level object in Page Model JSON API responses.

 Field  Type  Mandatory   Description
 id  String  Yes The reference namespace ID of the root page component.
 _meta  Map<String,JSON>  No Metadata map, containing pairs of String key and JSON value.
 _links  Map<String,LinkModel>  No Follow-up links, containing pairs of link name key and LinkModel value.
 page  JSON  Yes

The page representation in JSON. This representation follows the same structure as the HST component configuration (ComponentWindowModel) for a page.

 content  Map<String,JSON>  No Content representation map, containing pairs of JSON Identifier key and content item representation in JSON.
Any HST Content Beans, contributed by each HstComponent, for documents, folders, gallery images and assets are included in this content field.
For details on how content items are serialized to JSON, see Model JSON Mapping Details.

Unlike other metadata fields, page field and content field represent dynamic page composition with components in a page and content item representations contributed by each component in a page.

Also, see Model Contribution API for details on how each component can contribute content items to content field.

LinkModel

This defines a generic container of the follow-up link objects for a linkable resource representation.

 Field  Type  Mandatory  Description
 href  String  Yes The URI of this link.
 type  String  No

The type of the link. e.g, internal, external or resource
internal means the link is in the same SPA page resources, and so SPA may retrieve the linked resource through AJAX (XHR) calls, without having to reload the whole SPA page. 
external links cannot be represented within the current SPA, meaning the link should be treated as a document request (normal URL) and should not be fetched as XHR request by the SPA.
resource links are mostly for binary resources  such as images and assets.

 rel  String  No

The name of the relationship that the linked resource has to the page from which it’s referenced.

 title  String  No The title of the link.

ComponentWindowModel (HST Component Configuration)

This defines either a page component (which is a composite representation of HST Component Configurations) or a single descendant HstComponent resource representation.

 Field  Type  Mandatory  Description
 id  String  Yes

The reference namespace ID of the component.

 _meta  Map<String,JSON>  No

Metadata map, containing pairs of String key and JSON value.

 _links  Map<String,LinkModel>  No

Follow-up links, containing pairs of link name key and LinkModel value.

 name  String  Yes

HST Component configuration node name.
 See HstComponent Configuration for details.

 componentClass  String  Yes

HST Component class name.
 See HstComponent Configuration for details.

 type  String  Yes

HST Component class type.
COMPONENT, CONTAINER_COMPONENT or CONTAINER_ITEM_COMPONENT.
COMPONENT means a normal HstComponent, neither Channel Editor container nor container item component.
See Channel Editor Containers for detail on container or container item components.

 label  String  No HST Component catalog item's label.
See Channel Editor Catalog for detail.
 components  Array<ComponentWindowModel>  No Array containing child copmonent representations, each of which contains data in the same structure of ComponentWindowModel, recursively.
 models  Map<String,JSON>  No Map of content, menu, domain-specific model representations, etc., containing string key and object value in JSON.
Any models, that are contributed by HstComponent  but not included in the top level content field, are included in this models field in a component representation.  

Unlike other metadata fields, components field and models field represent dynamically composed component representations and content item reference, menu model representations or other domain-specific model representations.

See Model JSON Mapping Details for details on how models are serialized into JSON. Also, see Model Contribution API for details on how each component can contribute menu, domain-specific models, etc. to models field.

Content vs. Models

As you may have noticed, there are two different kinds of model container fields. One is the top level content field and the other is models in each component representation. Basically both include the models contributed by each HstComponent.

The difference is, however, the top level content field contains only WCMS content data for documents, folders, gallery images and assets. The other models such as menu, HstURL, HstLink or other domain-specific models are included in the models field of each component representation. When an HstComponent contributes a WCMS content model (for documents, folder, gallery images or assets), the real data of the content model will be included only in the top level content field, while leaving a JSON Pointer reference as JSON String to the real JSON content data in the models field of the component representation. See examples in the JSON Response Examples section.

This approach gives the following advantages:

  • Even if an HST Content Bean object as a model is referred by multiple HstComponents in the same page, the object will not be serialized multiple times. Instead, the real content data will be serialized only once in the top level content field, while each component representation has just a JSON Pointer reference as JSON String to the real content item. This makes JSON serializations a lot more effective.
  • An HST Content Bean object could have reference properties to other HST Content Bean objects. In that case, instead of nesting all the referred content data inside a content item representation, the referenced object property will be replaced by a JSON Pointer reference as JSON String as well and the real content data of the referred HST Content Bean object will be separately serialized into the top level content field as a sibling. This makes the JSON structure a lot cleaner.
  • Page Model JSON API module can also avoid any issues caused by circular references between HST Content Bean objects as a result.

JSON Response Examples

Let's first take a look around a whole JSON response example from an AggregatedPageModel object below.

{
   "id":"r19",
   "_links":{
      "self":{
         "href":"/resourceapi"
      },
      "site":{
         "href":"http://localhost:8080/site/myapp"
      }
   },
   "page":{
      "id":"r19",
      "name":"homepage",
      "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent",
      "type":"COMPONENT",
      "components":[
         {
            "id":"r19_r1",
            "name":"main",
            "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent",
            "type":"COMPONENT",
            "components":[
               {
                  "id":"r19_r1_r1",
                  "name":"container",
                  "componentClass":"org.hippoecm.hst.pagecomposer.builtin.components.StandardContainerComponent",
                  "type":"CONTAINER_COMPONENT",
                  "label":"Homepage Main Container",
                  "components":[
                     {
                        "id":"r19_r1_r1_r1",
                        "name":"banner",
                        "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent",
                        "type":"CONTAINER_ITEM_COMPONENT",
                        "label":"Banner",
                        "models":{
                           "document":{
                              "$ref":"/content/u895fb1b6410d497298946b6a06d2b361"
                           }
                        },
                        "_meta":{
                           "paramsInfo":{
                              "document":"banners/banner1"
                           },
                           "params":{
                              "com.example.cms7.targeting.TargetingParameterUtil.hide":"off",
                              "document":"banners/banner1",
                              "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl"
                           }
                        },
                        "_links":{
                           "componentRendering":{
                              "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r1"
                           }
                        }
                     },
                     {
                        "id":"r19_r1_r1_r2",
                        "name":"banner1",
                        "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent",
                        "type":"CONTAINER_ITEM_COMPONENT",
                        "label":"Banner",
                        "models":{
                           "document":{
                              "$ref":"/content/u9a3f1f5c530243c49bec584e810ffa2f"
                           }
                        },
                        "_meta":{
                           "paramsInfo":{
                              "document":"banners/banner2"
                           },
                           "params":{
                              "com.example.cms7.targeting.TargetingParameterUtil.hide":"off",
                              "document":"banners/banner2",
                              "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl"
                           }
                        },
                        "_links":{
                           "componentRendering":{
                              "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r2"
                           }
                        }
                     }
                  ],
                  "_meta":{
                     "params":{

                     }
                  },
                  "_links":{
                     "componentRendering":{
                        "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1"
                     }
                  }
               }
            ],
            "_meta":{
               "params":{

               }
            },
            "_links":{
               "componentRendering":{
                  "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1"
               }
            }
         },
         {
            "id":"r19_r2",
            "name":"top-right",
            "componentClass":"org.hippoecm.hst.core.component.GenericHstComponent",
            "type":"COMPONENT",
            "_meta":{
               "params":{

               }
            },
            "_links":{
               "componentRendering":{
                  "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r2"
               }
            }
         },
         {
            "id":"r19_r3",
            "name":"menu",
            "componentClass":"com.example.cms.components.HapMenuComponent",
            "type":"COMPONENT",
            "models":{
               "menu":{
                  "name":"main",
                  "selectSiteMenuItem":{
                     "depth":0,
                     "repositoryBased":false,
                     "name":"home",
                     "expanded":true,
                     "selected":true,
                     "parameters":{
                        "css class":"home"
                     },
                     "childMenuItems":[

                     ],
                     "_links":{
                        "site":{
                           "href":"/site/myapp",
                           "type":"internal"
                        }
                     }
                  },
                  "siteMenuItems":[
                     {
                        "depth":0,
                        "repositoryBased":false,
                        "name":"home",
                        "expanded":true,
                        "selected":true,
                        "parameters":{
                           "css class":"home"
                        },
                        "childMenuItems":[

                        ],
                        "_links":{
                           "site":{
                              "href":"/site/myapp",
                              "type":"internal"
                           }
                        }
                     },
                     {
                        "depth":0,
                        "repositoryBased":false,
                        "name":"news",
                        "expanded":false,
                        "selected":false,
                        "parameters":{
                           "css class":""
                        },
                        "childMenuItems":[

                        ],
                        "_links":{
                           "site":{
                              "href":"/site/myapp/news",
                              "type":"internal"
                           }
                        }
                     }
                  ]
               }
            },
            "_meta":{
               "paramsInfo":{
                  "siteMenu":"main"
               },
               "params":{
                  "selectedMenu":"on",
                  "level":"1",
                  "menu":"main"
               }
            },
            "_links":{
               "componentRendering":{
                  "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r3"
               }
            }
         }
      ],
      "_meta":{
         "definitionId":"hst:pages/homepage",
         "params":{

         }
      },
      "_links":{
         "componentRendering":{
            "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19"
         }
      }
   },
   "content":{
      "u895fb1b6410d497298946b6a06d2b361":{
         "id":"895fb1b6-410d-4972-9894-6b6a06d2b361",
         "_links":{
            "site":{
               "href":"/site/myapp",
               "type":"internal"
            }
         },
         "name":"banner1",
         "displayName":"banner1",
         "content":{
            "name":"hap:content",
            "displayName":"hap:content",
            "value":"\n          \n\n          <p>Banner description</p>\n\n          \n          "
         },
         "title":"Sample banner",
         "image":{
            "$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c"
         },
         "address":false,
         "localeString":"en"
      },
      "ub89d576f680a4bbf9c272dced9da3d6c":{
         "id":"b89d576f-680a-4bbf-9c27-2dced9da3d6c",
         "_links":{
            "site":{
               "href":"/site/binaries/content/gallery/hap/banners/banner-1.png",
               "type":"resource"
            }
         },
         "name":"banner-1.png",
         "displayName":"banner-1.png",
         "fileName":"banner-1.png",
         "description":"Description for banner-1.png",
         "original":{
            "width":700,
            "height":250,
            "length":151972,
            "lastModified":1395331380000,
            "mimeType":"image/png",
            "_links":{
               "site":{
                  "href":"/site/binaries/content/gallery/hap/banners/banner-1.png",
                  "type":"resource"
               }
            }
         },
         "thumbnail":{
            "width":60,
            "height":21,
            "length":3125,
            "lastModified":1395331380000,
            "mimeType":"image/png",
            "_links":{
               "site":{
                  "href":"/site/binaries/thumbnail/content/gallery/hap/banners/banner-1.png",
                  "type":"resource"
               }
            }
         }
      },
      "u9a3f1f5c530243c49bec584e810ffa2f":{
         "id":"9a3f1f5c-5302-43c4-9bec-584e810ffa2f",
         "_links":{
            "site":{
               "href":"/site/myapp",
               "type":"internal"
            }
         },
         "name":"banner2",
         "displayName":"banner2",
         "content":{
            "name":"hap:content",
            "displayName":"hap:content",
            "value":"\n          \n\n          <p>Banner description</p>\n\n          \n          "
         },
         "title":"Sample banner 2",
         "image":{
            "$ref":"/content/udb5907cce507460eb54dde5d5c784e0a"
         },
         "address":false,
         "localeString":"en"
      },
      "udb5907cce507460eb54dde5d5c784e0a":{
         "id":"db5907cc-e507-460e-b54d-de5d5c784e0a",
         "_links":{
            "site":{
               "href":"/site/binaries/content/gallery/hap/banners/banner2.png",
               "type":"resource"
            }
         },
         "name":"banner2.png",
         "displayName":"banner2.png",
         "fileName":"banner2.png",
         "description":"Description for banner2.png",
         "original":{
            "width":700,
            "height":250,
            "length":103656,
            "lastModified":1395504540000,
            "mimeType":"image/png",
            "_links":{
               "site":{
                  "href":"/site/binaries/content/gallery/hap/banners/banner2.png",
                  "type":"resource"
               }
            }
         },
         "thumbnail":{
            "width":60,
            "height":21,
            "length":2213,
            "lastModified":1395504540000,
            "mimeType":"image/png",
            "_links":{
               "site":{
                  "href":"/site/binaries/thumbnail/content/gallery/hap/banners/banner2.png",
                  "type":"resource"
               }
            }
         }
      }
   }
}

Note that the page field always has a components JSON Array and each item in the components JSON Array may contain its child components JSON Array recursively. This makes sense because an HST page consists of a hiearchical collection of descendant components. See HST Component Configuration for detail.

If a component contributes a document, then the document content item model will be included as a reference in models field inside the component representation like the following fragment shown above:

{
  "id":"r19_r1_r1_r1",
  "name":"banner",
  "componentClass":"org.onehippo.cms7.essentials.components.EssentialsDocumentComponent",
  "type":"CONTAINER_ITEM_COMPONENT",
  "label":"Banner",
  "models":{
    "document":{
      "$ref":"/content/u895fb1b6410d497298946b6a06d2b361"
     }
   },
   "_meta":{
     "paramsInfo":{
        "document":"banners/banner1"
      },
      "params":{
        "com.example.cms7.targeting.TargetingParameterUtil.hide":"off",
        "document":"banners/banner1",
        "org.hippoecm.hst.core.component.template":"webfile:/freemarker/hstdefault/essentials-banner.ftl"
      }
    },
    "_links":{
      "componentRendering":{
      "href":"/site/myapp/resourceapi?_hn:type=component-rendering&_hn:ref=r19_r1_r1_r1"
    }
  }
}

Note that the real content representation of the contributed document content item model is not included directly inside the models field. Instead it contains only a JSON Pointer referece as JSON String ("$ref":"/content/u895fb1b6410d497298946b6a06d2b361"), by which you can find the real content item representation under the content top level field by the key. Also, a component representation may include metadata, links and other model representations such as menu.

Content item model represetnations are always included in the content top level field for effectiveness (e.g, to avoid multiple serializations for the same content item) like the following:

"content": {
  "u895fb1b6410d497298946b6a06d2b361":{
    "id":"895fb1b6-410d-4972-9894-6b6a06d2b361",
    "_links":{
      "site":{
         "href":"/site/myapp",
         "type":"internal"
      }
    },
    "name":"banner1",
    "displayName":"banner1",
    "content":{
      "name":"hap:content",
      "displayName":"hap:content",
      "value":"\n          \n\n          <p>Banner description</p>\n\n          \n          "
    },
    "title":"Sample banner",
    "image":{
      "$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c"
    },
    "address":false,
    "localeString":"en"
  },
  "ub89d576f680a4bbf9c272dced9da3d6c":{
    "id":"b89d576f-680a-4bbf-9c27-2dced9da3d6c",
    "_links":{
      "site":{
        "href":"/site/binaries/content/gallery/hap/banners/banner-1.png",
        "type":"resource"
      }
    },
    "name":"banner-1.png",
    "displayName":"banner-1.png",
    "fileName":"banner-1.png",
    "description":"Description for banner-1.png",
    "original":{
      "width":700,
      "height":250,
      "length":151972,
      "lastModified":1395331380000,
      "mimeType":"image/png",
      "_links":{
        "site":{
          "href":"/site/binaries/content/gallery/hap/banners/banner-1.png",
          "type":"resource"
        }
      }
    },
    "thumbnail":{
      "width":60,
      "height":21,
      "length":3125,
      "lastModified":1395331380000,
      "mimeType":"image/png",
      "_links":{
        "site":{
          "href":"/site/binaries/thumbnail/content/gallery/hap/banners/banner-1.png",
          "type":"resource"
        }
      }
    }
  },
  // ...
}

Each content item representation also has links and other fields (such as title, content and address) which are extracted from its mapped HST Content Bean class.

Also note that the referenced content item, the image field in the above example, contains only a JSON Pointer reference as JSON String as well ("$ref":"/content/ub89d576f680a4bbf9c272dced9da3d6c"), so you can find the image content item representation separately.

See Model JSON Mapping Details for more details on how built-in or custom models are serialized into JSON.

Maximum Content Item Reference Depth Level

As explained above, if an HST Content Bean object is referred by an HstComponent, the content item will be included only once in the top level content field. In this case, the reference depth level is 1, meaning that the content item is in the first reference depth level from the HstComponent.

Now, suppose the specific content item also has a reference to another content item, which makes the second referenced content item be in the reference depth level 2 from the HstComponent. Any referenced content item in the second or any deeper level will not be included in the top level content field, by default, unless another HstComponent has a direct reference to it. So, the first content item representation will include only a JSON Pointer reference to the second content item. The maximum content item reference depth level is set to 1 in the system, by default.

How to Include up to N Level Content Items?

You can change the maximum content item reference depth level in two ways: (a) changing it per request, (b) changing the system default setting. (a) takes the precedence over (b).

In order to change it per request, add _maxreflevel request parameter like the following:

  • http://localhost:8080/site/myapp/resourceapi/news/?_maxreflevel=2

The response will include referred content items in up to depth level 2.

In order to change the default maximum reference depth level, add the following in the HST-2 Container Configuration file (e.g, ${catalina.base}/conf/hst.properties):

# The default maximum content item reference depth level
#pagemodelapi.v09.defaultMaxContentReferenceLevel = 1
pagemodelapi.v09.defaultMaxContentReferenceLevel = 3

The above configuration will change it from 1 to 3, which makes the response include content items in up to reference depth level 3.

Links: Internal vs. External

In the content section, the _links object may contain a site link which specifies a type. The type can be either "internal" or "external". "internal" means the page can be represented within the current SPA. This means the SPA can use an XHR request and do a partial page update instead of making a full page request. In the above example, we have:

"_links":{
      "site":{
         "href":"/site/myapp",
         "type":"internal"
      }
    },

It means that the link, /site/myapp, can be represented within the current SPA. The SPA may opt to make an XHR call, at /resourceapi/site/myapp for example, instead of making a full page request.

Rich Content Rewriting

The top level content field, containing pairs of JSON Identifier key and content item representation in JSON, are serialized versions of HST Content Beans. The HippoDocumentBean instances may contain rich text fields, which are represented by HippoHtmlBean objects. When HippoHtmlBean objects are serialized to a JSON, the HTML content of the HippoHtmlBean objects are rewritten, to make sure that images and links are accessible and usable from the SPA. For example when a WCMS document contains a link to another document in a rich text field, the link is rewritten to more accessible and usable markups. See the following example:

<a href="/news/news1.html" data-type="internal"/>

The SPA retrieves the href attribute which it can directly navigate to or it can do something smarter with it optionally: If the data-type is "internal", then the link can be used as an XHR call (typically something like /resourceapi/news/news1.html) which updates part of the pages, instead of making a new page request. If, however, the data-type is "external", then the SPA should always do a full page request (never an XHR call) because the linked page cannot be displayed within the current SPA. Typically this happens when:

  1. There is a hybrid setup with SPA pages and the link represents a normal server side rendering page.
  2. The link belongs to a document that is part of a different channel which is not part of the SPA.

Summary

The built-in Page Model JSON API provides an intuitive REST API endpoint for SPAs, ensuring seamless integration with WCMS. SPAs may consume the JSON API responses to construct pages, components, menus, etc. with referenced content item models or domain-specific models, contributed by each component and aggregated for the page, in their own SPA frameworks. Page Model JSON API provides a generic, extensible, and most effective aggregated model representation for easier SPA development support.

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?