AMPConf 2019 is just wrapping up but we’re just getting started! In this series of blog posts we celebrate AMPConf by highlighting how the React Storefront framework and the Moovweb XDN infrastructure make life easier for developers to support AMP in their React apps.

Keeping your AMPs DRY

We’ve been relatively early adopters in eCommerce for both Progressive Web Apps (PWAs) and Accelerated Mobile Pages (AMP). With React powered PWAs, developers can deliver highly engaging experiences on the web that rival native apps. However, when it comes to search-generated traffic, AMP provide the fastest possible option.

In fact, Google’s recommended customer journey is to first deliver an AMP version of your app to search users and transition to the full PWA version of your site on subsequent pages. And since nearly half of retailer website traffic comes from organic search, supporting both AMP and PWA has become a priority.

Unfortunately, PWA and AMP technologies could not be more wildly divergent. PWAs use modern libraries like React, Vue, and Angular where JavaScript takes on the central role of rendering HTML, CSS, as well as delivering interactivity. AMP pages, on the other hand, are completely prohibited from using JavaScript and have tight restrictions on both CSS and HTML. The result is that to support both AMP and PWA, developers typically need to recode a significant portion of their app in AMP. And the burden on the development team doesn’t end there. Every bug fix, layout change, new feature, etc. may require being propagated to both the AMP and PWA codebases.

Writing a great app once is hard enough. No one wants to write the same app twice. Developers hate this kind of thing so much they’ve given it a popular acronym, DRY (Do not Repeat Yourself). As developers ourselves, we invested early in adding automatic AMP support to the React framework we created for eCommerce PWAs. This way developers can write their progressive web app once, in React, and get AMP support with no extra development effort.

And it’s worked very well for our customers. Instead of rebuilding their app, implementing AMP with React Storefront typically takes our customers a day or two, and most of that is simply verifying everything works as expected.  

In this post, we will cover how React Storefront does the hard work for you in supporting AMP and PWA from a single codebase. In the next installment, we’ll share some exciting new features of the Moovweb XDN that help deliver AMP in a complex, enterprise context.

Why don’t all React frameworks do this?

For a typical React app, automatically deriving AMP content is difficult. That’s because most React apps are designed to run only in the browser, but AMP content can’t have any JavaScript (including React) before it gets to the browser. In other words, you can’t just point the Google AMP Cache to the same React code you send to the browser. They are completely different formats.

To overcome this limitation we benefited from an early architectural decision to make React Storefront apps universal (sometimes called isomorphic) by default and support Server-Side Rendering (SSR) in the Moovweb XDN. With SSR, rendering of the page is done on the server before it is served to users. While our primary goals in adding SSR were to improve performance and SEO, it also enables us to run a React Storefront app on the server and then automatically convert the HTML output to valid AMPHTML in concert with the techniques below.

Notify Google that AMP is supported

The first step towards implementing AMP is to notify Google’s crawler that AMP equivalents of your pages exist. Typically, you would need to create separate AMP versions of your pages, then configure your application’s router to serve both AMP and PWA content on distinct URLs. With React Storefront, all you need to do is add a @withAmp decorator to your top-level page component. React Storefront automatically renders valid AMPHTML for any page simply by adding “.amp” to the URL. The @withAmp decorator leverages this convention to add the necessary link tag to the document head.

In other words, this:

@withAmp
class Product extends Component {
  ...
}

results in something like this on your PWA page:

<link rel="amphtml" href="https://www.mysite.com/p/1.amp"/>

Now, when Google’s crawler sees a link to /p/1, it will download the AMP content and serve it directly from Google’s AMP CDN.

Note that for proper SEO, the AMP page also needs to be embed a rel=canonical tag back to the original PWA version of the page, like this:

<link rel="canonical" href="https://www.mysite.com/p/1"/>

It's worth stressing here the importance of having the AMP and non-AMP pages pointing back to each other with the appropriate (but different versions of) <link> tags. Without these setup properly Google won't serve up your AMP content and, even worse, it could result in your AMP pages cannibalizing the SEO rank of your non-AMP pages. React Storefront does all the "AMP double entry bookkeeping" for you so you don't have to worry about this happening simply because someone forgot to update the right tags.

AMP aware components

Implementing even the most basic interactivity in AMP can be a huge pain. For example, a simple quantity field in AMP might look like:

<amp-form>
  <amp-state id="product">
    <script type="application/json">
      { 
        "id": "123",
        "name": "Product", 
        "quantity": 1 
      }
    </script>
  </amp-state>
  <input 
    type="text" 
    [value]="product.quantity"
    on="input-throttled:AMP.setState({ product: { quantity: event.value } })"
  />
</amp-form>

The same quantity field written in React might look like:

function QuantitySelector({ product }) {
  return (
    <input 
      type="text"
      value={product.quantity}
      onChange={e => product.setQuantity(e.target.value)}
    />
  )
}

Imagine writing two versions of code like this FOR EVERY INTERACTIVE CONTROL in your progressive web app. There goes your launch deadline!

Thankfully, we've done that hard work for you. React Storefront gives you coarse-grained AMP-aware components like QuantitySelector that hide all of these gory details. In React Storefront, you would simply use:

<QuantitySelector product={product}/>

The React framework takes care of rendering the AMP version of the component when Google requests an AMP page.

Conditional Rendering

In most cases, developers can simply use one of the AMP-aware components within the React Storefront framework to provide interactivity in AMP. If, however, you find that you need to render something specific for AMP, the framework exposes a boolean amp field in the app state that you can use to conditionally render specific content in AMP.

For example:

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

@inject('app')
class MyAMPAwareComponent extends Component {

  render() {

    if (this.props.app.amp) {
      return (
        // AMP content
      )
    } else {
      return (
        // PWA content
      )
    }
  }

}

Universal Analytics

AMP’s declarative way of defining interaction events is completely different from React’s. For example, imagine you want to track clicks on specific product links by sending events to Google Analytics. In AMP, this would be something like:

<amp-analytics type="googleanalytics">
  <script type="application/json">
    {
      "triggers": [
        {
          "on":"click", 
          "event":"product_clicked",
          "selector":"#product1",
          "request":"event",
          "productID": "1"
        }
      ]
    }
  </script>
</amp-analytics>

<a id="product1" href="/p/1" alt="Red Shirt">
  <amp-img src="/images/products/1" height="100" width="100"/>
  <div>Red Shirt</div>
</a>

I’ll spare you the React implementation, but it’s likely going to involve an onClick listener and a few calls to ga(). Suffice it to say the two will look nothing alike.

React Storefront provides an abstraction, the Track component, that implements both of these for you:

<Track event="product_clicked" productID={product.id}>
  <a href="/p/1" alt="Red Shirt">
    <Image src="/images/products/1"/>
    <div>Red Shirt</div>
  </a>
</Track>

Transforming server rendered HTML into AMPHTML

The strategies above get us about 80% of the way towards rendering valid AMP. There are, however, additional changes that need to be made to the document. React Storefront transforms the outgoing HTML before it’s sent making several changes, including:

Adding amp-boilerplate and ⚡

AMP requires some specific elements to be present in the document. These include a “⚡” attribute on the root HTML element, and a standard style element called the AMP boilerplate:

<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>

Transforming and consolidating CSS

AMP requires that all CSS be provided in the body of a single style element. Use of the style attribute and <link rel="stylesheet"> are forbidden. React Storefront consolidates all the styles across your codebase, whether embedded within multiple <style> elements, or even inline style attributes, into a single <style amp-custom> element. For each use of a style attribute, React Storefront creates a unique CSS class containing the corresponding rules, applies the class to the element, removes the style attribute, and adds the class to the amp-custom style element. In other words, this:

<div style="font-weight:bold">Red Shirt</div>

becomes:

<style amp-custom>
  …
  .mi1 { 
    font-weight:bold;
  }
</style>

<div class="mi1">Red Shirt</div>

When added up across all your styles, this automated conversion of CSS takes away another major headache when writing AMP compatible pages from your React app.

Cleaning up markup

There are various other rules that AMP imposes on the markup. For example, the way that ReactDOM renders boolean attributes produces invalid AMPHTML. This JSX:

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

will result in this HTML:

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

which results in the AMP validator showing an error like this:

If you use a component library, some of the HTML rendered by the components may run afoul of the AMP validator for various reasons. React Storefront encourages the use of the widely popular Material UI library, and provides specific rules for cleaning up the HTML it produces. This is where using a comprehensive react progressive web app framework that covers everything from state management to components and styling really shows its value in terms of increasing developer velocity.

Infrastructure for AMP and PWA

As a developer implementing AMP and PWA, you could implement two versions of your app and then maintain them through future changes (meaning that every fix becomes a code change in two places), or use automation to derive AMP content from your pages. At Moovweb we chose the latter, and it’s worked very well for our customers. The React Storefront features we’ve described so far are only part of the reason for that success. In the next installment, we’ll explore how the this framework integrates with the Moovweb XDN infrastructure to support AMP in an enterprise environment. Stay tuned.