AMP

React Storefront contains built-in support for rendering AMP content based on your existing views. This guide shows you how to support AMP in your PWA.

Making Pages Discoverable with @withAmp

Add the @withAmp decorator to any view to add the <link rel="amphtml"> tag to the page so that Google knows there is an amp equivalent. For example:

import React, { Component } from 'react'
import withAmp from 'react-storefront/withAmp'

@withAmp
export default class Category {

  render() {
    // ...
  }

}

The amp URL for any given page will be the same URL with a .amp suffix. You do not have to make any changes to your routes to support this. For example, if a category can be found at /c/1, you can load the amp equivalent of that category at /c/1.amp.

Rendering Valid AMP HTML

AMP places restrictions on the HTML that your app can use. Many of these restrictions are automatically handled by React Storefront, such as:

No inline styles

React Storefront automatically converts your inline styles to unique HTML classes at runtime and adds them to the style tag in the document head.

Only a single style tag is allowed

React Storefront automatically consolidates all style tags into one.

CSS !important is not allowed

React Storefront automatically removes all !important directives when rendering amphtml. Note that this can alter the style of your pages. For this reason, we strongly encourage you to not use !important.

Use amp-img instead of img

All img tags are automatically converted to amp-img. If you use react-storefront/Image with an aspectRatio prop, it will automatically be converted to <amp-img layout="fill">.

Required Boilerplate

AMP requires certain boilerplate to be inserted into the document head. React Storefront automatically does this for you whenever rendering a page with the .amp suffix.

Conditional Rendering

React Storefront's strategy for implementing AMP is for each component to be AMP aware. Several of the components in react-storefront render AMP-specific content when a page with the .amp suffix is being displayed

To implement AMP-specific rendering logic in your components, inject app and check the value of app.amp:

import React, { Component } from 'react'
import { inject } from 'mobx-react'

@inject('app')
export default class Category {

  render() {
    const { app } = this.props

    if (app.amp) {
      // render amp content
    } else {
      // render normal PWA content
    }
  }

}

Checking for AMP in Route Handlers

You can also determine whether or not you're rendering AMP content in your route handlers by checking the format parameter, which captures the URL suffix.

export default function productHandler({ id, format }) {
  if (format === 'amp') {
    // we're rendering amp
  } else {
    // we're rendering the normal product view
  }
}

React Storefront Components with AMP-specific Functionality

Menu

When rendering AMP content, the Menu component will switch to rendering an amp-sidebar.

Image

When rendering AMP content, the Image component will switch to rendering an amp-img. If aspectRatio is set, layout="fill" is used. Otherwise layout="intrinsic" is used. This can be overridden by providing ampProps.

ExpandableSection

When rendering AMP content, the ExpandableSection component will use amp-accordion.

TabPanel

When rendering AMP content, the TabPanel component will use amp-selector to mimic the look and feel of Material-UI tabs.

Installing the Service Worker

React Storefront automatically installs the service worker when your AMP page is loaded.

Embedding CMS Content within AMP

Generally you cannot rely on HTML blobs imported from a CMS to be valid AMP HTML. Fortunately, React Storefront automatically transforms most HTML into valid AMP HTML automatically. It does, however, require some special help with img elements.

React Storefront automatically converts all img elements to amp-img. AMP requires that all images have an explicit height and width. If your img elements do not have height and width attributes and need to be responsive, you can provide the natural height and width for each image by adding data-amp-height and data-amp-width attributes and specifying data-amp-layout="responsive". Any attributes that begin with data-amp-* are automatically applied to the resulting amp-img.

Here's an example:

<img 
  src="/path/to/img.png"
  data-amp-height="768" 
  data-amp-width="1024" 
  data-amp-layout="responsive"
>

becomes

<amp-img 
  src="/path/to/img.png"
  height="768" 
  width="1024" 
  layout="responsive"
>

For more about images in AMP, see the amp-img docs.

Using amp-bind

AMP's databinding syntax, called amp-bind, requires the use of brackets around attributes in custom elements. JSX does not support this. React Storefront allows you to use amp-bind by providing an alternate syntax, which is transformed to amp-bind's syntax at runtime. To bind an element's property to an AMP state, use:

<TextField
  amp-bind="value=>myState.someValue"
/>

You can bind multiple properties using a comma-separated list:

<TextField
  amp-bind="value=>myState.someValue,class=myState.someOtherProperty"
/>

AmpState and AmpForm

React Storefront's AmpState component initializes an amp state and provides the state id to all descendant components. When used in conjunction with AmpForm, you can create a UI that allows your users to select product options and add to their cart from AMP. Here's a complete example that uses amp-bind to submit the price for the selected size when adding a product to the cart.

import AmpState from 'react-storefront/amp/AmpState'
import React, { Component, Fragment } from 'react'
import { inject, observer } from 'mobx-react'

@inject(({ app }) => ({ product: app.product }))
@observer
export default class Product extends Component {

  render() {
    const { product } = this.props

    return (
      <AmpState id="product" initialState={product}>
        <AmpForm id="form" action="/cart/add-from-amp.json">
          <input type="hidden" name="id" value={product.id}/>
          <label>Quantity:</label>
          <QuantitySelector product={product}/>
          <label>Size:</label>
          <SizeSelector product={product}/>          
        </AmpForm>
      </AmpState>
    )
  }

}

/**
 * A size field that also stores the corresponding price in a hidden field so we can
 * submit it when adding the product to the cart
 */
@inject('ampStateId') // provided by <AmpState/>
class SizeSelector extends Component {

  render() {
    const { product, ampStateId } = this.props
  
    return (
      <Fragment>
        <select 
          name="size" 
          // update amp state with new size so that we can keep price in sync
          on={`change:AMP.setState({ ${ampStateId}: { color: event.value }})`}
        >
          <option value="">Select Size</option>
          {product.sizes.map((sizes, i) => (
            <option key={i} value={sizes.code}>{sizes.name} {product.prices[size.code]}</option>
          ))}
        </select>

        <input 
          // here we create a hidden field for price so that we can submit it when adding to the cart from AMP
          type="hidden" 
          name="price"
          value=""
          // keep selected size and price in sync
          amp-bind={`value=>${ampStateId}.prices[${ampStateId}.size]`}
        />
      </Fragment>    
    )
  }

}

Handling AMP Form Submissions on the Server

You can submit data to the server from AMP using <AmpForm action="/path/to/handler">. AmpForm can be configured with method="get" or method="post".

method="get"

// src/routes.js

import { Router, fromServer } from 'react-storefront/router'

export default new Router()
  .get('/cart/add-from-amp.json',
    fromServer('./cart/add-from-amp-handler')
  )
// src/cart/add-from-amp-handler.js

import { redirectTo } from 'react-storefront/router'

export default function addFromAmp(params) {
  const { id, quantity, size, price } = params // when using method="get", values are passed on the query string and received in the params argument

  // ... make API call to add the product to the cart

  return redirectTo('/cart') // display the cart page when done
}

method="post"

// src/cart/add-from-amp-handler.js

import { redirectTo } from 'react-storefront/router'
import { parseFormData } from 'react-storefront/amp/parseFormData'

export default function addFromAmp(params, state, request) {
  const { id, quantity, size, price } = request.body // when using method="post", values are passed in the post body

  // ... make API call to add the product to the cart

  return redirectTo('/cart') // display the cart page when done
}