React Storefront
|

AMP

The react-storefront-extensions commercial package allows your app to automatically render AMP content based on your React views. This guide shows you how to support AMP in your PWA.

Overview

React Storefront makes your app AMP compatible with minimal development effort by:

  • Providing a @withAmp decorator that you add to page components to render the necessary link tag that lets Google know that there is an AMP version of the page.
  • Providing components that are AMP-aware. When rendering URLs with a .amp extension, certain components like Menu and ImageSwitcher will render AMP components instead of HTML.
  • Sanitizing the resulting AMPHTML to ensure that it passes AMP validation.
  • Rendering AMPHTML to HTML on the server (aka AMP + SSR)

Setup

To begin rendering AMP in React Storefront, install react-storefront-extensions:

npm i --save-dev react-storefront-extensions

The react-storefront-extensions package provides a function called transformAmpHtml which transforms your app's HTML into valid amphtml at runtime whenever an AMP page is served.

To install this feature, update your scripts/index.js file to:

const index = require('react-storefront-moov-xdn').default
const { transformAmpHtml } = require('react-storefront-extensions/amp')

require('../src/analytics')

module.exports = function() {
  index({
    theme: require('../src/theme').default,
    model: require('../src/AppModel').default,
    App: require('../src/App').default,
    router: require('../src/routes').default,
    blob: env.blob || require('../src/blob.dev'),
    transform: transformAmpHtml
  })
}

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-extensions/amp/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 transformAmpHtml, 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">.

Add height and width to all images

Unless an amp-img element has layout="fill", it needs height and width attributes. React Storefront automatically inspects each image to determine its height and width and adds these attributes to the amp-img if they aren't already present.

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.

AMP + SSR

React Storefront automatically renders AMPHTML to HTML on the server. This can be turned off by adding the optimize: false option to transformAmpHtml:

// scripts/index.js

const index = require('react-storefront-moov-xdn').default
const { transformAmpHtml } = require('react-storefront-extensions/amp')

require('../src/analytics')

module.exports = function() {
  index({
    theme: require('../src/theme').default,
    model: require('../src/AppModel').default,
    App: require('../src/App').default,
    router: require('../src/routes').default,
    blob: env.blob || require('../src/blob.dev'),
    transform: (html, options, services) =>
      transformAmpHtml(html, { ...options, optimize: false }, services) // disable AMP SSR
  })
}

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" method="post">
          <input type="hidden" name="id" value={product.id} />
          <label>Quantity:</label>
          <QuantitySelector name="quantity" product={product} />
          <label>Size:</label>
          <SizeSelector name="size" 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>
    )
  }
}

Debugging AMP pages

Add #development=1 to your URL to enable helpful debugging features.

If you need to know the current state of your AMP page while developing custom triggers, you can open the development console and execute:

AMP.printState()

This will log the current AMP state. You can also change the state with:

AMP.setState({ foo: 'bar' })

More information available here

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". In order to send the value of a form input to the server, you need to ensure that it has a name prop.

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

export default function addFromAmp(params, request, response) {
  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 response.redirect('/cart') // display the cart page when done
}

method="post"

// src/routes.js

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

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

export default function addFromAmp(params, request, response) {
  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 response.redirect('/cart') // display the cart page when done
}