Develop the Products Feature Part 3: Product Detail Page - BloomReach Experience - Open Source CMS

This article covers a Hippo CMS version 10. There's an updated version available that covers our most recent release.

Develop the Products Feature Part 3: Product Detail Page

Previous Step

Develop the Products Feature Part 2: Products Overview Page

After implementing the Products Overview page and its business logic and template, you will do the same for the Product Detail page.

Page Configuration

Open the Hippo Console in your browser:  http://localhost:8080/cms/console/. Log in as user 'admin' with password 'admin'.

Browse to the node  /hst:hst/hst:configurations/gogreen/hst:pages. This is where the page configurations are stored.

Add a new node called  productpage of type  hst:component.

Add a property  hst:referencecomponent to the  productpage node and enter the value  hst:abstractpages/twocolumns.

/hst:hst/hst:configurations/gogreen/hst:pages
    + productpage [hst:component]
        - hst:referencecomponent = hst:abstractpages/twocolumns

You have now defined a new page configuration  productpage that extends the abstract  twocolumns page configuration you defined earlier. You will define what goes in the content area later.

URL Configuration

In the Hippo Console browser to the node  /hst:hst/hst:configurations/gogreen/hst:sitemap/products. This is the URL you configured for the Products Overview page in the previous step.

Add a new child node called _any_.html of type  hst:sitemapitem to the products node. The string " _any_" represents a wildcard comparable to ** in e.g. Unix. It allows you to define a URL pattern so you don't have to define a separate URL for each product.

Add a property  hst:componentconfigurationid to the _any_.html node and enter the value hst:pages/productpage.

Add a property  hst:relativecontentpath to the  _any_.html node and enter the value  ${parent}/${1}. This combines the relative content path mapped to the parent URL ( products) with the value matched by the wildcard ( _any_).

/hst:hst/hst:configurations/gogreen/hst:sitemap/products
    + _any_.html [hst:sitemapitem]
        - hst:componentconfigurationid = hst:pages/productpage
        - hst:relativecontentpath = ${parent}/${1}

You have now effectively created URLs for all product documents in the products folder, even those that don't exist yet. They are also mapped to the productpage page configuration.

At this point if you point your browser at the Products Overview page and click on any of the products listed you will see they link to a specific URL and the page on that URL shows the common page elements. The main content area is still empty, you will get to that in a minute.

https://documentation.bloomreach.com/binaries/ninecolumn/content/gallery/connect/trails/build-a-website/developer-trail-10.0/sprint-2/products-detail/products-detail-empty-page.png

Product Detail Business Logic

Once again you can rely on one of the provided Hippo Java components to implement the business logic for the Product Detail component. The Content Component retrieves a single document from the content repository and makes it available as a rendering attribute. It also implements exception handling in case the document doesn't exist.

All you need to know is

  • The Java class name is  org.onehippo.cms7.essentials.components.EssentialsContentComponent.
  • The document is made available through the rendering attribute document.

Product Detail Template

Open the file products-detail.html found in the web design. Locate the element  <div class="col-md-9 col-sm-9">. This contains the HTML markup for the Product Detail page.

Also locate the CSS and Javascript references that are specific to this page. They are marked with the following comments:

<!-- start page specific css -->

<!-- end page specific css -->

and:

<!-- start page specific js -->

<!-- end page specific js -->

Create the file  bootstrap-webfiles/src/main/resources/site/freemarker/gogreen/productpage-main.ftl in your project. This will be the Freemarker template that renders the contents of the product document.

Add the following line to productpage-main.ftl:

<#include "../include/imports.ftl">

You will implement the actual template in a minute.

Open the Hippo Console in your browser and browse to the node  /hst:hst/hst:configurations/gogreen/hst:templates. This is where the templates are configured.

Add a new node called  productpage-main of type  hst:template.

Add a property  hst:renderpath to the  productpage-main node and enter the value webfile:/freemarker/gogreen/productpage-main.ftl.

/hst:hst/hst:configurations/gogreen/hst:templates
    + productpage-main [hst:template]
        - hst:renderpath = webfile:/freemarker/gogreen/productpage-main.ftl

In the Hippo Console browse to the node  /hst:hst/hst:configurations/gogreen/hst:pages/productpage.

Add a new node called  main of type  hst:component.

Add a new child node called  left of type  hst:component to the  main node.

Add a property  hst:componentclassname to the  left node and enter the fully qualified Java class name of the Content Component:  org.onehippo.cms7.essentials.components.EssentialsContentComponent.

Add a property  hst:template to the  left node and enter the value  productpage-main.

/hst:hst/hst:configurations/gogreen/hst:pages/productpage
    + main [hst:component]
        + left [hst:component]
            - hst:componentclassname = org.onehippo.cms7.essentials.components.EssentialsContentComponent
            - hst:template = productpage-main

Go back to the template  productpage-main.ftl.

Use Freemarker syntax to make it dynamically render the contents of the product document conforming to the web design.

Some hints:

  • You can look at  bootstrap-webfiles/src/main/resources/site/freemarker/hstdefault/newspage-main.ftl for inspiration.
  • The product document is available through the variable  document.
  • Product ratings are out of scope in this sprint so you can comment out the related markup ( <#-- -->).
  • Remember that the product document type allows multiple images to be selected!
  • Don't forget to add head contributions for the CSS and Javascript files that are loaded specifically for this page!

You will end up with something like this:

<#include "../include/imports.ftl">
<#if document??>
  <@hst.link var="link" hippobean=document/>
  <div class="blog-post">
    <div class="blog-post-type">
      <i class="icon-shop"> </i>
    </div>
    <div class="blog-span">
      <h2>${document.title?html}</h2>
      <@hst.cmseditlink hippobean=document/>
      <#if document.images??>
        <div class="blog-post-featured-img img-overlay">
          <div class="cycle-slideshow frame1" data-cycle-slides="> .slider-img" data-cycle-swipe="true"
            data-cycle-prev=".cycle-prev" data-cycle-next=".cycle-next" data-cycle-overlay-fx-out="slideUp"
            data-cycle-overlay-fx-in="slideDown" data-cycle-timeout="0">
            <div class="fa fa-chevron-right cycle-next"></div>
            <div class="fa fa-chevron-left cycle-prev"></div>
            <div class="cycle-pager"></div>
            <#list document.images as item>
              <@hst.link var="img" hippobean=item/>
              <div class="slider-img">
                <img src="${img}"
                  alt="${document.title?html}">
                <div class="item-img-overlay">
                  <a class="portfolio-zoom icon-zoom"
                    href="${img}"
                    data-rel="prettyPhoto[portfolio]" title="${document.title?html}"></a>
                </div>
              </div>
            </#list>
          </div>
        </div>
      </#if>
      <div class="blog-post-body">
        <p>${document.introduction?html}</p>
        <@hst.html hippohtml=document.description/>
      </div>
      <div class="blog-post-details">
        <div class="blog-post-details-item blog-post-details-item-left">
          <img src="<@hst.webfile path="/images/icon-banknote.png"/>" class="icon">
          <span class="price"><@fmt.formatNumber value="${document.price}" type="currency" /></span>
        </div>
        <#--
        <div class="blog-post-details-item blog-post-details-item-left rating-info">
          <span id="document-rating" data-score="3.5"></span>
        </div>
        -->
      </div>
    </div>
  </div>

  <@hst.headContribution category="htmlHead">
    <link rel="stylesheet" href="<@hst.webfile path="/css/prettyPhoto.css"/>" />
  </@hst.headContribution>
  <@hst.headContribution category="htmlHead">
    <link rel="stylesheet" href="<@hst.webfile path="/css/jquery.raty.css"/>" />
   </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery-2.1.0.min.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.raty.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.cycle.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.prettyPhoto.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript">
      jQuery(document).ready(function($) {
        /* Portfolio PrettyPhoto */
        $("a[data-rel^='prettyPhoto']").prettyPhoto({
            animation_speed: 'fast', /* fast/slow/normal */
            slideshow: 5000, /* false OR interval time in ms */
            autoplay_slideshow: false, /* true/false */
            opacity: 0.80  /* Value between 0 and 1 */
        });
      });
  
      $(
        '#comments .comment-rating > div, #document-rating')
        .raty(
          {
            score : function() {
              return $(this).attr('data-score');
            },
            readOnly : true,
            half : true,
            starType : 'i'
          });
      $('#rating-field')
        .raty(
          {
            targetText : 0,
            target : '#rating',
            targetType : 'score',
            targetKeep : true,
            starType : 'i'
          });
    </script>
  </@hst.headContribution>

</#if>

Open the site in your browser and browse to a product. The Product Detail page will now render all the contents of the product document conform to the web design.

https://documentation.bloomreach.com/binaries/ninecolumn/content/gallery/connect/trails/build-a-website/developer-trail-10.0/sprint-2/products-detail/product-detail-gallery.png

Next Step

Add Featured Products to the Home Page

Full Source Code

productpage-main.ftl

<#include "../include/imports.ftl">
<#if document??>
  <@hst.link var="link" hippobean=document/>
  <div class="blog-post">
    <div class="blog-post-type">
      <i class="icon-shop"> </i>
    </div>
    <div class="blog-span">
      <h2>${document.title?html}</h2>
      <@hst.cmseditlink hippobean=document/>
      <#if document.images??>
        <div class="blog-post-featured-img img-overlay">
          <div class="cycle-slideshow frame1" data-cycle-slides="> .slider-img" data-cycle-swipe="true"
            data-cycle-prev=".cycle-prev" data-cycle-next=".cycle-next" data-cycle-overlay-fx-out="slideUp"
            data-cycle-overlay-fx-in="slideDown" data-cycle-timeout="0">
            <div class="fa fa-chevron-right cycle-next"></div>
            <div class="fa fa-chevron-left cycle-prev"></div>
            <div class="cycle-pager"></div>
            <#list document.images as item>
              <@hst.link var="img" hippobean=item/>
              <div class="slider-img">
                <img src="${img}"
                  alt="${document.title?html}">
                <div class="item-img-overlay">
                  <a class="portfolio-zoom icon-zoom"
                    href="${img}"
                    data-rel="prettyPhoto[portfolio]" title="${document.title?html}"></a>
                </div>
              </div>
            </#list>
          </div>
        </div>
      </#if>
      <div class="blog-post-body">
        <p>${document.introduction?html}</p>
        <@hst.html hippohtml=document.description/>
      </div>
      <div class="blog-post-details">
        <div class="blog-post-details-item blog-post-details-item-left">
          <img src="<@hst.webfile path="/images/icon-banknote.png"/>" class="icon">
          <span class="price"><@fmt.formatNumber value="${document.price}" type="currency" /></span>
        </div>
        <#--
        <div class="blog-post-details-item blog-post-details-item-left rating-info">
          <span id="document-rating" data-score="3.5"></span>
        </div>
        -->
      </div>
    </div>
  </div>

  <@hst.headContribution category="htmlHead">
    <link rel="stylesheet" href="<@hst.webfile path="/css/prettyPhoto.css"/>" />
  </@hst.headContribution>
  <@hst.headContribution category="htmlHead">
    <link rel="stylesheet" href="<@hst.webfile path="/css/jquery.raty.css"/>" />
   </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery-2.1.0.min.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.raty.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.cycle.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript" src="<@hst.webfile path="/js/jquery.prettyPhoto.js"/>"></script>
  </@hst.headContribution>
  <@hst.headContribution category="htmlBodyEnd">
    <script type="text/javascript">
      jQuery(document).ready(function($) {
        /* Portfolio PrettyPhoto */
        $("a[data-rel^='prettyPhoto']").prettyPhoto({
            animation_speed: 'fast', /* fast/slow/normal */
            slideshow: 5000, /* false OR interval time in ms */
            autoplay_slideshow: false, /* true/false */
            opacity: 0.80  /* Value between 0 and 1 */
        });
      });
  
      $(
        '#comments .comment-rating > div, #document-rating')
        .raty(
          {
            score : function() {
              return $(this).attr('data-score');
            },
            readOnly : true,
            half : true,
            starType : 'i'
          });
      $('#rating-field')
        .raty(
          {
            targetText : 0,
            target : '#rating',
            targetType : 'score',
            targetKeep : true,
            starType : 'i'
          });
    </script>
  </@hst.headContribution>

</#if>