Products
Fruit Vegetable More

Product Page

Edit on Github
Open in Playground View Demo

Introduction

This sample showcases how to build a product page in AMP HTML.

Import amp-carousel to create an image gallery.

<script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>

Import amp-list to get the latest promotions.

<script async custom-element="amp-list" src="https://cdn.ampproject.org/v0/amp-list-0.1.js"></script>

Import amp-mustache as a format template for amp-list

<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.2.js"></script>

Import amp-social-share for adding share buttons

<script async custom-element="amp-social-share" src="https://cdn.ampproject.org/v0/amp-social-share-0.1.js"></script>

Import amp-lighbox to show additional content such as a video

<script async custom-element="amp-lightbox" src="https://cdn.ampproject.org/v0/amp-lightbox-0.1.js"></script>

Import amp-image-lightbox to show additional content such as an image gallery

<script async custom-element="amp-image-lightbox" src="https://cdn.ampproject.org/v0/amp-image-lightbox-0.1.js"></script>

Import amp-youtube to show a video

<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>

Import amp-form to implement search

<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>

Import amp-form to implement a menu

<script async custom-element="amp-sidebar" src="https://cdn.ampproject.org/v0/amp-sidebar-0.1.js"></script>

Import amp-selector to implement auto suggest search

<script async custom-element="amp-selector" src="https://cdn.ampproject.org/v0/amp-selector-0.1.js"></script>

Import amp-analytics to monitor usage

<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>

Import amp-bind to change color and price of items

<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>

Metadata

The page indexing requires schema.org markup for one of the following types: Type, AggregateRating, Offers. Learn more.

<script type="application/ld+json">
{
    "@context": "http://schema.org/",
    "@type": "Product",
    "name": "Apple",
    "image": "http://www.ampbyexample.com/img/golden_apple1_1024x682.jpg",
    "description": "Lorem ipsum",
    "mpn": "925872",
    "brand": {
        "@type": "Fruit",
        "name": "Apple"
    },
    "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": "4.4",
        "reviewCount": "88"
    },
    "offers": {
        "@type": "Offer",
        "priceCurrency": "USD",
        "price": "1.99",
        "priceValidUntil": "2020-11-05",
        "itemCondition": "http://schema.org/UsedCondition",
        "availability": "http://schema.org/InStock",
        "seller": {
            "@type": "Retail",
            "name": "AMP by Example"
        }
    }
}
</script>

Use the amp-sidebar component to give customers the chance to quickly jump to any of your product categories.

Example

<amp-sidebar id="drawermenu"
  layout="nodisplay">
  <a href="/">Products</a>
  <hr/>
  <a href="/samples_templates/product_browse_page/preview/">Fruit</a>
  <a href="/samples_templates/product_browse_page/preview/">Vegetable</a>
  <a href="/samples_templates/product_browse_page/preview/">More</a>
</amp-sidebar>

AMP supports forms, which means you can directly integrate a product search into your AMPs. Try searching for "Apple".

AMP doesn't support custom Javascript, but you can use CSS3 animations to enrich your page. The expanding text field when the search box is focused is implemented with CSS only.

Example

<div class="header">
  <a id="sample-menu"
    on="tap:drawermenu.toggle">
    <amp-img srcset="/img/ic_menu_white_1x_web_24dp.png 1x, /img/ic_menu_white_2x_web_24dp.png 2x"
      width="24"
      height="24"
      alt="navigation"></amp-img>
  </a>
  <form method="GET"
    action="/samples_templates/product_browse_page/search"
    target="_top">
    <input name="search"
      type="search"
      placeholder="Search">
    <a id="sample-logo"
      href="/">Product</a>
    <input type="submit"
      value="">
  </form>
</div>

Social sharing

The Social Share extension provides a common interface for share buttons. Learn more about amp-social-share here.

Example

<div class="margin1">
  <amp-social-share type="twitter"
    width="30"
    height="22"></amp-social-share>
  <amp-social-share type="facebook"
    width="30"
    height="22"
    data-attribution="254325784911610"></amp-social-share>
  <amp-social-share type="gplus"
    width="30"
    height="22"></amp-social-share>
  <amp-social-share type="email"
    width="30"
    height="22"></amp-social-share>
  <amp-social-share type="pinterest"
    width="33"
    height="22"></amp-social-share>
</div>

Video

AMP supports a wide range of video platforms. Here we are using amp-youtube to show a product video. You can find an overview of all supported video platforms here.

In this sample we don't embed the video directly into the page, but instead show it in a lightbox using the amp-lightbox component. Learn more about amp-lightbox here.

Example

<button class="watch-video margin1"
  on="tap:watch-video-lightbox">
  Show Video
</button>

On desktop we've added a film strip like slide preview. Each product colour has its own image gallery and slide preview. We use amp-bind to update the page selecting the gallery and image preview corresponding to the selected colour. We realized the color selection by using amp-selector. Find more the doc and info in The Product Page section below.

The green colour is the default color for the gallery when the page loads. Clicking on a preview image will reveal the image in the gallery via the tap action on="tap:AMP.setState({product: {selectedSlideForGreen: 0}})".

See below for more informations on how we useamp-bind.

Example

<div class="product-gallery">
  <ul [hidden]="product.selectedColor != 'green'">
    <li>
      <amp-img on="tap:AMP.setState({product: {selectedSlideForGreen: 0}})"
        src="/img/green_apple_1_60x40.jpg"
        width="60"
        height="40"
        class="selected"
        [class]="product.selectedSlideForGreen == 0 ? 'selected' : '' "
        tabindex="0"
        role="button">
      </amp-img>
    </li>
    <li>
      <amp-img on="tap:AMP.setState({product: {selectedSlideForGreen: 1}})"
        src="/img/green_apple_2_60x40.jpg"
        width="60"
        height="40"
        [class]="product.selectedSlideForGreen == 1 ? 'selected' : '' "
        tabindex="1"
        role="button">
      </amp-img>
    </li>
  </ul>

  <ul hidden
    [hidden]="product.selectedColor != 'golden'">
    <li>
      <amp-img on="tap:AMP.setState({product: {selectedSlideForGolden: 0}})"
        src="/img/product1_alt1_60x40.jpg"
        width="60"
        height="40"
        class="selected fadeIn"
        [class]="product.selectedSlideForGolden == 0 ? 'selected fadeIn' : '' "
        tabindex="0"
        role="button">
      </amp-img>
    </li>
  </ul>

  <ul hidden
    [hidden]="product.selectedColor != 'red'">
    <li>
      <amp-img on="tap:AMP.setState({ product: {selectedSlideForRed : 0}})"
        src="/img/red_apple_1_60x40.jpg"
        width="60"
        height="40"
        class="selected fadeIn"
        [class]="product.selectedSlideForRed == 0 ? 'selected fadeIn' : '' "
        tabindex="0"
        role="button">
      </amp-img>
    </li>
    <li>
      <amp-img on="tap:AMP.setState({ product: {selectedSlideForRed : 1}})"
        src="/img/red_apple_2_60x46.jpg"
        width="60"
        height="40"
        [class]="product.selectedSlideForRed == 1 ? 'selected' : '' "
        tabindex="1"
        role="button">
      </amp-img>
    </li>
  </ul>
</div>

The amp-carousel component works very well for product image galleries. Learn how the amp-carousel component works here.

Notice how we are binding to the slide and class by using to the values of the variables selectedSlideForGreen and selectedColor.

Example

<div class="product-gallery">

  <amp-carousel id="green-apple-carousel"
    width="1024"
    height="682"
    layout="responsive"
    type="slides"
    [slide]="product.selectedSlideForGreen"
    on="slideChange: AMP.setState({product: {selectedSlideForGreen: event.index}})"
    class="fadeIn"
    [hidden]="product.selectedColor != 'green'">
    <amp-img src="/img/green_apple_1_1024x682.jpg"
      width="1024"
      height="682"
      layout="responsive"
      on="tap:gallery-lightbox"
      role="button"
      tabindex="0">
    </amp-img>
    <amp-img src="/img/green_apple_2_1024x685.jpg"
      width="1024"
      height="682"
      layout="responsive"
      on="tap:gallery-lightbox"
      role="button"
      tabindex="0">
    </amp-img>
  </amp-carousel>

  <amp-carousel id="golden-apple-carousel"
    width="1024"
    height="682"
    layout="responsive"
    type="slides"
    [slide]="product.selectedSlideForGolden"
    on="slideChange: AMP.setState({product: {selectedSlideForGolden: event.index}})"
    hidden
    class="fadeIn"
    [hidden]="product.selectedColor != 'golden'">
    <amp-img src="/img/golden_apple1_1024x682.jpg"
      width="1024"
      height="682"
      layout="responsive"
      on="tap:gallery-lightbox"
      role="button"
      tabindex="0">
    </amp-img>
  </amp-carousel>

  <amp-carousel id="red-apple-carousel"
    width="1024"
    height="682"
    layout="responsive"
    type="slides"
    [slide]="product.selectedSlideForRed"
    on="slideChange: AMP.setState({product: {selectedSlideForRed: event.index}})"
    hidden
    class="fadeIn"
    [hidden]="product.selectedColor != 'red'">
    <amp-img src="/img/red_apple_1_1024x682.jpg"
      width="1024"
      height="682"
      layout="responsive"
      on="tap:gallery-lightbox"
      role="button"
      tabindex="0">
    </amp-img>
    <amp-img src="/img/red_apple_2_1024x793.jpg"
      width="1024"
      height="682"
      layout="responsive"
      on="tap:gallery-lightbox"
      role="button"
      tabindex="0">
    </amp-img>
  </amp-carousel>

Full screen image

The amp-image-lightbox component allows the user to expand an image to fill the viewport. By clicking on each image from the product gallery, the user can see the product image by using the full screen. Learn more here

Example

<amp-image-lightbox id="gallery-lightbox"
  layout="nodisplay">
  <div on="tap:gallery-lightbox.close"
    role="button"
    tabindex="0">
    <button class="close-gallery-button"
      on="tap:gallery-lightbox.close"
      role="button"
      tabindex="0">
      Close
    </button>
  </div>
</amp-image-lightbox>

Product configuration

We use the amp-state component (part of amp-bind) to configure the product price depending on the color and the size. We also configure the defaultSize for each product when you switch between colours and the size you previously selected is not available for the new color

Example

<amp-state id="product">
  <script type="application/json">
    {
      "selectedColor": "green",
      "selectedSize": "S",
      "selectedSlideForRed": 0,
      "selectedSlideForGolden": 0,
      "selectedSlideForGreen": 0,
      "moreItemsPageIndex": 0,
      "hasMorePages": true,
      "green": {
        "sizes": {
          "S": "$5.99",
          "M": "$5.99",
          "L": "unavailable"
        },
        "defaultSize": "S",
        "id": 1
      },
      "golden": {
        "sizes": {
          "S": "$9.99",
          "M": "unavailable",
          "L": "$9.99"
        },
        "defaultSize": "L",
        "id": 2
      },
      "red": {
        "sizes": {
          "S": "$7.99",
          "M": "$7.99",
          "L": "$7.99"
        },
        "defaultSize": "M",
        "id": 3
      }
    }
  </script>
</amp-state>

Product price

We use amp-bind to update the price of the product depending on the selected colour.

We bind the text attribute to the value of expression product[product.selectedColor].sizes[product.selectedSize]. product is the id amp-state json described in the Product Configuration section.

Notice how we set $1.99 value as the default for the price: expressions will not be evaluated at the page load.

We use amp-selector together with amp-bind to update the page for showing the price, the size availability, the gallery and preview of images. Every time you select a color, we call AMP.setState with {selectedColor: event.targetOption}. This is triggering all the attributes and elements binded to selectedColor to update: the price (binded with the attribute [text]), the gallery, the image preview and the size selector (all binded with [class]).

Example

Price: $5.99

<p class="price-description">Price:
  <span [text]="product[product.selectedColor].sizes[product.selectedSize]">$5.99</span>
</p>

Add To Cart

The add-to-cart action action is implemented using amp-form. In our sample we use the amp-selector for selecting different product properties.

Pressing the ADD TO CART button triggers an XHR POST request to /add_to_cart. Here, we need to distinguish two scenarios:

  • If the user is visiting the cached version of the page, the server receives the request on the POST handler and redirects again to /add_to_cart on the non-cache origin, but transforming the initial request, into GET, using the AMP-Redirect-To header (see redirecting after submission). Next, the GET handler adds the item to the session, and redirects to the final destination: the /shopping_cart page.
  • If the user is visiting the page from its origin, there's no need for a 'double redirect', since the server can access the session directly on the POST handler, then add the item to the cart, and, finally redirect to the /shopping_cart page.

The first step of the previous list is necessary, in order to move the user outside the domain of the cache (where the session can't be accessed on browsers that block third party cookies), into an intermediate step in the origin (where the session cookie, is a first party cookie).

That way, we can maintain user state across origins, in a secure way, while being compatible with all the major browsers.

Example

  • 1
  • 2
  • 3
  • S
  • M
  • L
<form id="order"
  method="POST"
  action-xhr="https://abe-cart-service.glitch.me/add_to_cart"
  target="_top"
  class="order margin1">
  <div class="color-selector">
    <label for="color">Color:</label>
    <amp-selector name="color"
      layout="container"
      [selected]="product.selectedColor"
      on="select:AMP.setState({
                  product: {
                    selectedColor: event.targetOption,
                    selectedSlideForRed: 0,
                    selectedSlideForGreen: 0,
                    selectedSlideForYellow: 0,
                    selectedSize: product[event.targetOption].sizes[product.selectedSize] != 'unavailable' ? product.selectedSize : product[event.targetOption].defaultSize
                  }
                })">
      <ul>
        <li>
          <div option="green"
            selected
            class="square green"></div>
        </li>
        <li>
          <div option="golden"
            class="square golden"></div>
        </li>
        <li>
          <div option="red"
            class="square red"></div>
        </li>
      </ul>
    </amp-selector>
  </div>
  <div class="quantity-selector">
    <label for="quantity">Quantity:</label>
    <amp-selector name="quantity"
      layout="container">
      <ul>
        <li option="1"
          selected>1</li>
        <li option="2">2</li>
        <li option="3">3</li>
      </ul>
    </amp-selector>
  </div>
  <div class="size-selector">
    <label for="size">Size:</label>
    <amp-selector name="size"
      layout="container"
      on="select:AMP.setState({ product: {selectedSize: event.targetOption}})"
      [selected]="(product[product.selectedColor].sizes[product.selectedSize] != 'unavailable')
                    ? product.selectedSize
                    : product[product.selectedColor].defaultSize">
      <ul>
        <li option="S"
          class=""
          [class]="(product[product.selectedColor].sizes['S'] != 'unavailable')
                    ? ''
                    : 'unavailable'">S</li>
        <li option="M"
          class=""
          selected
          [class]="(product[product.selectedColor].sizes['M'] != 'unavailable')
                    ? ''
                    : 'unavailable'">M</li>
        <li option="L"
          class="unavailable"
          [class]="(product[product.selectedColor].sizes['L'] != 'unavailable')
                    ? ''
                    : 'unavailable'">L</li>
      </ul>
    </amp-selector>
  </div>
  <div class="submit-selector">
    <input type="submit"
      name="add-to-cart"
      value="add to cart">
  </div>
  <input type="hidden"
    name="name"
    value="Apple">
  <input type="hidden"
    name="price"
    value="$5.99"
    [value]="product[product.selectedColor].sizes[product.selectedSize]">
  <input type="hidden"
    name="id"
    value="1"
    [value]="product[product.selectedColor].id">
  <div submit-error>
    <template type="amp-mustache">
      Error! Looks like something went wrong with your shopping cart, please try to add an item again. {{error}}
    </template>
  </div>
</form>

Tab panels

Use amp-selector styled as tab panels to add additional data about your product. Learn how to implement tabs in AMP here.

Example

Fruit is rich in vitamins and minerals. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Always wash fruit before eating. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Size may vary. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
<amp-selector role="tablist"
  layout="container"
  class="ampTabContainer"
  keyboard-select-mode="select">
  <div role="tab"
    class="tabButton"
    selected
    option="a">ABOUT</div>
  <div role="tabpanel"
    class="tabContent">Fruit is rich in vitamins and minerals. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
    ex ea commodo consequat.</div>
  <div role="tab"
    class="tabButton"
    option="b">SPECS</div>
  <div role="tabpanel"
    class="tabContent">Always wash fruit before eating. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
  <div role="tab"
    class="tabButton"
    option="c">SIZE</div>
  <div role="tabpanel"
    class="tabContent">Size may vary. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
</amp-selector>

Latest offers

With AMP, you can easily pull in different latest offers or highlights without changing the page. To do so, just use amp-list to fire a CORS request to a JSON endpoint which supplies the list of related products. These are populated into an amp-mustache template on the client. Learn more about amp-list here.

Example

<amp-list class="items margin1"
  width="auto"
  height="145"
  layout="fixed-height"
  src="/json/related_products.json"
  [src]="myState.items"
  id="show-more-list">
  <template type="amp-mustache">
    <a href="/samples_templates/product_page/preview/">
      <amp-img width="70.31"
        height="46.8"
        layout="fixed"
        alt="{{name}}"
        src="{{img}}"></amp-img>
      <p class="name">{{name}}</p>
      <p class="star">{{{stars}}}</p>
      <p class="price">${{price}}</p>
    </a>
  </template>
</amp-list>

Product list state

While infinite scrolling is not possible at the moment in AMP, you can use amp-bind to dynamically change the src of amp-list and add more items to the page. We bind the src attribute to the id of an amp-state component so that the amp-list will use the items from that component as a src. The amp-state initial value is set by setting the src value to the same endpoint used by amp-list. We append items to the amp-state everytime the user click on the Show more button, see below for more info.

Example

<amp-state id="myState"
  src="/json/related_products.json">
</amp-state>

Show more button

You can show or hide a Show more button by using amp-form and the submit-success event: notice how we set the hasMorePages variable to false based on the server response.

Example

<form method="GET"
  action="/json/more_related_products_page"
  action-xhr="/json/more_related_products_page"
  target="_top"
  on="submit-success: AMP.setState({
        myState: { items: myState.items.concat(event.response.items)},
        product: {moreItemsPageIndex: product.moreItemsPageIndex + 1,
                  hasMorePages: event.response.hasMorePages}
      });">
  <input type="hidden"
    name="moreItemsPageIndex"
    value="0"
    [value]="product.moreItemsPageIndex">
  <input type="submit"
    value="Show more"
    class="show-more"
    [hidden]="!product.hasMorePages">
</form>

User analytics

Analytics must be configured in the body. Here we use Google Analytics to track pageviews.

Example

<amp-analytics type="googleanalytics">
  <script type="application/json">
    {
      "vars": {
        "account": "UA-73836974-1"
      },
      "triggers": {
        "default pageview": {
          "on": "visible",
          "request": "pageview",
          "vars": {
            "title": "Product"
          }
        }
      }
    }
  </script>
</amp-analytics>