Product Page

LemonStand provides you with the flexibility to build out the design for your product page in any way you like. Any page you create for your store can be used as a product page as long as you set the page's Action to shop:product. The shop:product action will prepare the specific product and associated parameters for use on your product page.

Below is an example of how the product page appears in the front-end of your store using the Happy Hour theme. A product page will generally include information about your product, such as images, attributes, product options and an "add to cart" button.


Let's begin by looking at the product page in the Happy Hour theme.

The first section to take note of is the URL field which contains the value /product/:urlName

The :urlName segment is used by the shop:product CMS action to load the correct product variable at page load. Therefore, it is a required segment when setting the URL name for your product page. For example, when we load our baseball cap product page using /product/cap, the /cap section (which refers to the product URL name) is processed by the shop:product action and the cap product info is then prepped for use on the page.

Product Page Structure

The next section we will look at is the content area of our product page. It contains the design framework for the page and also a call to render the shop:product partial.

{% if product %}
       <div id="product-page">
          {{ partial('shop-product') }}
       </div>  
{% else %}
       <h2>We are sorry, the requested product was not found.</h2>
{% endif %}

This code example shows an if statement that is checking if the product exists when the page loads. If the product exists, we instruct LemonStand to output the shop-product partial. We could also build out the view for our product directly on this page but using partials is the recommended approach. Partials are also required If we want to update the product page using AJAX. Finally, the else section will output a "product was not found" message if no product matching the product name segment in the URL is found.

The typical structure for a product partial is as follows:

{{ open_form({'class': 'custom', 'onsubmit': 'return false'}) }}
    {% if product %}
           <h2>{{ product.name }}</h2>
            {% if product.description %}
                <h3>Description</h3>
                {{ product.description|unescape }}
       {% endif %}
       <img src="{{ product.images.first.thumbnail(80, 80)|default('http://placehold.it/80x80') }}"/>
           {{ partial('shop-product-attributes') }}
           {{ partial('shop-product-options') }}
           {{ partial('add-to-cart-button') }}
   {% else %}
      <h2>We are sorry, the requested product was not found.</h2>
   {% endif %}
{{ close_form() }}

Product Description and Images

Lets's now have a look at the shop-product partial in the Zest theme. The shop-product partial is where the code for our various product page elements such as the product name, description, image, pricing, and "add to cart" button will reside.

Any product you create in LemonStand will require a name and you will want to output your product's name on the product page. To do so, use the following code: {{ product.name }}

LemonStand also provides you with the ability to enter a Long Description for each product you enter into your store. Though it is not required you may want to add a description for your product. To output the long description as part of your product page, use the following code snippet that will first check if you have entered a description and then output it to the page if one is found:

{% if product.description %}
    <h3>Description</h3>
    {{ product.description|unescape }}
{% endif %}

Let's now look at how the code for the product image is referenced in the shop-product partial:

<img src="{{ product.images.first.thumbnail(746, 'auto')|default('<a href=" http:="" placehold.it="" 460x300')"=""><a href="<a href=" <a="">http://placehold.it/460x300')</a>"><a href="http://placehold.it/460x300')">http://placehold.it/460x300')</a>"><a href="http://placehold.it/460x300')">http://placehold.it/460x300')</a> }}" alt="{{ product.name }}"/>

As you can see, we have set the image SRC attribute for our product by referencing product.images.first.thumbnail(746,'auto'). This snippet of code will load the first image associated with the product and then set the thumbnail size using the .thumbnail(746,'auto')function, where 746 will equal the desired width of the image and 'auto' is the image height. The 'auto' value forces LemonStand to calculate the image height proportionally, so that the original size ratio is saved. Then we declare a default image in case the product being loaded doesn't have one set. We do this using |default('. Finally, we set our image alt tag as the product name using {{ product.name }}.

Product Attributes

In the Zest theme, product attributes are rendered via the shop-product-attributes partial. Let's have a look at the code contained within this partial:

{% if product.productAttributes.count %}
  <table>
    {% for attribute in product.productAttributes %}
      <tr>
        <th>{{ attribute.name }}</th>
        <td>{{ attribute.value }}</td>
      </tr>
    {% endfor %}
  </table>
{% endif %}

What we are looking at is an if statement that first checks if the current product contains any attributes by using {% if product.productAttributes.count %}. If any attributes are found, we then create a table that will be used to style the items that are returned. We then iterate over each of the attributes found with a for loop {% for attribute in product.productAttributes %}. For each attribute that is found, we then output the {{ attribute.name }} and {{ attribute.value }} in table cells within the row.

Product Options

In the Zest theme, product options are rendered via the shop-product-options partial using the following code:

{% if product.options.count %}
  <div class="clearfix">
    {% for index, option in product.options %}
      <div class="option">
        <label class="title" for="{{ 'option-'~index }}">{{ option.name }}</label>
        <select id="{{ 'option-'~index }}" name="options[{{ option.id }}]" class="product-option" data-ajax-handler="shop:product" data-ajax-update="#product-page=shop-product">
          {% for key, value in option.values %}
            <option value='{{ key }}' {{ option_state(postedOptions[option.id], key) }}>{{ value }}</option>
          {% endfor %}
        </select>
      </div>
    {% endfor %}
  </div>
{% endif %}

To output product options on your product page, we first start with an if statement to check if the product loaded contains any option records. We do this using {% if product.options.count %}. If product options are found, we then create a for loop to iterate over the options available. Since a product can contain numerous sets of options, our for loop must ensure that we are outputting values specific to each option. We do this by referencing the index of the option in our for loop, using {% for index, option in product.options %}. We then create a label for our option by setting the for value in our label element using {{ 'option-'~index }}. This will produce a value of option-0 in the front endfor the first item in the index. Then we output the name for the label using {{ option.name }}.

Next, we build our select element. We output the ID in the same manner as the label using {{ 'option-'~index }}. We then set the name by referencing the options array and the key for the current option using the option ID. This is accomplished using options[{{ option.id }}]. We also declare an AJAX handler and AJAX update attribute as part of our select element that will handle the update of page elements when an option is selected from the drop-down. This is done using data-ajax-handler="shop:product", which instructs the shop:product CMS object to handle the request, and data-ajax-update="#product-page=shop-product",which points to #product-page, which is the CSS selector for the element that contains the content to be updated and the shop-product partial that renders the content for updating.

To create the options for our select element, we create another for loop that will loop through the option.values array. This is done using {% for key, value in option.values %}. For the output of the option elements we set the value equal to the current key for option.values using {{ key }}. We then enter {{ option_state(postedOptions[option.id], key) }} for LemonStand to properly set the selected attribute for our option element on postback.

Add to Cart Button

No product page would be complete without the ability to add the product to the cart. Let's look at how this is accomplished in the partial-shop-product partial in the Zest theme.

<div class="row add-to-cart-block">
  <div class="four columns">
    <input type="text" value="{{ quantity|default("1") }}" name="quantity"/>
  </div>
  <div class="row add-to-cart-block">
    <input type="hidden" name="productId" value="{{ product.id }}"/>
    <a href="#" data-ajax-handler="shop:onAddToCart" data-ajax-update="#mini-cart=shop-minicart, #product-page=shop-product">Add to Cart</a>
  </div>
</div>

First we create our INPUT field for entering the number of items we want to add to the cart. We then set the value using {{ quantity|default("1") }}. This outputs a default value of 1 for quantity at page load. Then we create our "Add to Cart" button which contains two AJAX attributes - data-ajax-handler and data-ajax-update. The data-ajax-handler will refer to the CMS AJAX handler that will process the request on the server, in this case it would be the shop:onAddToCart AJAX handler. The data-ajax-update attribute refers to the page element that will be updated after the server processes the request. This is broken up into two sections. The CSS selector for the element that wraps the content that you want to be updated and the partial that renders the actual content to be updated. In this case we are updating the shop-minicart and the shop-product partial.

Adding Multiple Products to the Cart

The product page supports adding multiple products to the cart in one request. The fields used to do this are as follows:

  1. productIds
  2. quantities
  3. options
  4. extras
  5. customFields
  6. product_billing_plan - The product billing plan field can be passed as both an Array or as a single value. An array specifies plans for each product specifically, while a single value (e.g. product_billing_plan = 1) will assign the billing plan to the entire cart. The value should match an existing plan's ID.

All of the above fields must be arrays. General use looks like this:

data-ajax-handler: "shop:onAddToCart"
productIds[array-key]: {{ product.id }}
quantities[array-key]: 2
options[array-key][{{ option.id }}]: {{ option.id }}
extras[array-key][{{ extra.id }}]: true/false
customFields[array-key][field_name]: value
product_billing_plan[array-key]: 1
product_billing_plan: 1

Example:

<p>Batch Add Form</p>
{% for product in products %}
    <h3><a href="{{ page_url }}">{{ product.name }}</a></h3>
    <a href="{{ page_url }}"><img src="{{ product.images.first.thumbnail(25, 25)|default('http://placehold.it/365x365') }}" width="25" height="25" alt="{{ product.images.first.description }}" title="{{ product.images.first.title }}"/></a>
    {% for product in products %}
    <input name="productIds[{{ product.sku }}]" value="{{ product.id }}" type="hidden" />
    <input name="quantities[{{ product.sku }}]" value="1" type="text" />
    <label for="customFields[{{ product.sku }}][label]">Custom Product Label</label>
    <input name="customFields[{{ product.sku }}][label]" type="text" />
    {% if product.options.count %}
    <h5>Options</h5>
        {% for index, option in product.options %}
            <label class="title" for="{{ 'option-'~index }}">{{ option.name }}</label>
            <select id="{{ 'option-'~index }}" name="options[{{ product.sku }}][{{ option.id }}]" class="product-option">
              {% for key, value in option.values %}
                <option value='{{ key }}' {{ option_state(postedOptions[option.id], key) }}>{{ value }}</option>
              {% endfor %}
            </select>
        {% endfor %}
    {% endif %}
    {% if product.extras.count %}
    <h5>Extras</h5>
        {% for index, extra in product.extras %}
            <label class="title" for="{{ 'extra-'~index }}">{{ extra.name }} ({{ extra.price|currency }})</label>
            {% if extra.enabled %}
                <input type="checkbox" id="{{ 'extra-'~index }}" {{ checkbox_state(postedExtras[extra.id], extra.id) }} name="extras[{{ product.sku }}][{{ extra.id }}]">
            {% else %}
                <input type="checkbox" disabled="disabled">
            {% endif %}
        {% endfor %}
    {% endif %}
{% endfor %}
<a href="#" data-ajax-handler="shop:onAddToCart">Add to Cart</a>
{{ close_form() }}

Product Upsells and Cross-sells

Product Upsells can be used to upsell related products. To learn how to add product upsells, check out the documentation here.

In the Zest theme for example, product upsells can be rendered in the product page by embedding the following snippet at the bottom of the shop-product partial. They can also be rendered on other pages, such as the checkout page.

Two pieces of data are required to add an Upsell to the cart.

1. upsellId - The ID of the upsell product.

2. productId - The ID of the upsell's parent product.

Here's an example that will add the upsell product directly to the customer's cart:

{% if product.upsells.isEmpty() == false %}
    <table class="product-list full-width">
        <tr>
        <th>Buy this, as well!</th>
        <th class="narrow">Original Price</th>
        <th class="narrow">Sale Price</th>
        <th class="narrow"></th>
        </tr>
        {% for upsell in product.upsells %}
        <tr>
        <td>
          <a class="hide-for-small" href="/product/{{ upsell.url_name }}"><img src="{{ upsell.images.first.thumbnail(80, 80)|default('http://placehold.it/80x80') }}"/></a>
          <div class="short-description">
            <h3><a href="/product/{{ upsell.url_name }}">{{ upsell.name }}</a></h3>
            {% set description = upsell.optionsString() %}
            {% if description %}<p>{{ description|unescape }}</p>{% endif %}
          </div>
        </td>
        <td class="narrow"><i>{{ upsell.base_price|currency }}</i></td>
        <td class="narrow"><i>{{ upsell.price|currency }}</i> </td>
        <td class="narrow">
            <a class="button"
                href="#"
                data-ajax-handler="shop:onAddToCart"
                data-ajax-update="#mini-cart=shop-minicart, #product-page=shop-product"
                data-ajax-extra-fields="upsellId='{{ upsell.id }}', productId='{{ product.id }}'"
                >Buy Now</a></td>
        </tr>
        {% endfor %}
    </table>
{% endif %}

Product Cross-sells work more like related products. The above code for upsells will add the product directly to your cart, however Cross-sells will link to the specific product page. In the Zest theme, adding cross-sells to a product will add related products below the product itself. You can also use the cross-sell variables if you would like to embed cross-sells elsewhere in your LemonStand store.

Volumetric Pricing

Products with volumetric pricing should display this info to the customer. To do this use the product variable product.priceTiers. Take a look at the product variable page for more info on price tiers. Here is an example of displaying volumetric pricing.

<table class="table product-list">
                        <thead>
                            <tr>
                                <th class="price-tier-quantity">Quantity</th>
                                <th class="price-tier-price">Price</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for priceTier in product.priceTiers %}
                            <tr>
                                <td class="price-tier-quantity">{{ priceTier.quantity }}</td>
                                <td class="price-tier-price">{{ priceTier.price|currency }}</td>
                            </tr>
                            {% endfor %}
                        </tbody>
</table>

Page Redirects

After a product is added to cart, you have the option of redirecting the customer to a different page—the cart page, for instance—by using the data-ajax-redirect attribute on the "Add to Cart" button and giving it the URL of the redirect page.

Here we are adding the attribute to the example from above, and redirecting to the cart page.

<a href="#" 
  data-ajax-handler="shop:onAddToCart" 
  data-ajax-update="#mini-cart=shop-minicart, #product-page=shop-product" 
  data-ajax-redirect="{{ site_url('cart') }}">Add to Cart</a>