A blog about React and related technologies

Building an AMP website with React & Next.js

A new Next.js 8.1 release with support for authoring AMP pages has been introduced last week. In this post we'll explore how to set up AMP pages with Next.js.

AMP is an open-source web framework aimed at providing a way to create high-performance websites. With AMP Cache support, AMP pages can appear to load instantly when visited from Google and Bing search results.

At its essence, AMP is a stripped-down version of HTML. Some HTML elements are replaced with AMP specific elements (for example, we need to use <amp-img> instead of <img>). Custom JavaScript isn't allowed, although some JavaScript APIs will be supported in the future via an <amp-script> element.

To learn what's coming to AMP, check out the videos from AMP Conf, held in Tokyo on April 17/18.

Whether to implement AMP on a website has been a controversial topic ever since AMP has been introduced. I don't have an AMP website in production yet, but now that it's possible to easily implement it with React and Next.js, I decided to experiment.

Implementing AMP with Next.js

AMP support in Next.js can be defined on a per-page basis and for each page we can specify one of two modes:

AMP First Pages

Here's a simple page:

// pages/index.js
const Index = () => (
  <div className="container">
    <h1>Next.js AMP Example</h1>
  </div>
);

export default Index;

To convert it to an AMP page, we only need to add export const config = { amp: true } to the file:

// pages/index.js
const Index = () => (
  <div className="container">
    <h1>Next.js AMP Example</h1>
  </div>
);

export const config = { amp: true };

export default Index;

AMP First pages have no Next.js or React client-side runtime, they will be served to both regular visitors and search engine visitors.

Hybrid AMP Pages

To have a Hybrid page, we only need to add export const config = { amp: hybrid } to the file:

// pages/index.js
const Index = () => (
  <div className="container">
    <h1>Next.js AMP Example</h1>
  </div>
);

export const config = { amp: hybrid };

export default Index;

With Hybrid mode, regular visitors will see traditional React/Next.js pages, while search engines will be able to show the AMP version of pages.

AMP version of pages will have ?amp=1 in the URL.

Next.js provides a useAmp hook which can be used to differentiate between modes. For example, to use <img> tag in regular mode and <amp-img in AMP mode:

// pages/index.js
import { useAmp } from 'next/amp';

const Index = () => (
  <div className="container">
    <h1>Next.js AMP Example</h1>
    {useAmp() ? (
      <amp-img width="300" height="150" src="/logo.png" alt="Logo" layout="responsive" />
    ) : (
      <img width="300" height="150" src="/logo.png" alt="Logo" />
    )}
  </div>
);

export const config = { amp: hybrid };

export default Index;

Example of an AMP-only website

To get more familiar with what AMP offers, I made a quick example of a news/blog type of website where data is loaded from a jsonplaceholder.typicode.com API:

You can download the complete source code on GitHub.

Using AMP Components

amp-sidebar

<amp-sidebar> can be used for a menu, which is hidden by default and can be opened and closed by button taps.

In the example, I used it to show a list of categories. First I created a Sidebar component with <amp-sidebar> that includes a toggle button and the category menu:

// components/sidebar.js
import Link from 'next/link';
import categories from '../data/categories.json';

const Sidebar = () => {
  return (
    <amp-sidebar id="sidebar" className="sidebar" layout="nodisplay">
      <button on="tap:sidebar.toggle" className="sidebar-trigger"></button>

      <ul className="menu">
        {categories.map(cat => {
          return (
            <li key={cat.id}>
              <Link href={`/category?id=${cat.id}`} as={`/category/${cat.id}`}>
                <a>{cat.title}</a>
              </Link>
            </li>
          );
        })}
      </ul>
    </amp-sidebar>
  );
};

export default Sidebar;

Then, I imported the Sidebar component in pages/_document.js:

import Document, { Html, Head, Main, NextScript } from 'next/document';
import Sidebar from '../components/sidebar';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head>
          <script async custom-element="amp-sidebar" src="https://cdn.ampproject.org/v0/amp-sidebar-0.1.js" />
        </Head>
        <body>
          <Sidebar />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

It was necessary to place it inside the _document.js file because <amp-sidebar> must be a child of the <body> element.

I also included the https://cdn.ampproject.org/v0/amp-sidebar-0.1.js script because <amp-sidebar> isn't part of the main AMP script.

To add a "hamburger" menu button to the header, I used this code:

<button on="tap:sidebar.toggle" className="sidebar-trigger"></button>

The important part is that the on attribute includes the ID that was used for <amp-sidebar> (sidebar in this case).

amp-img

<amp-img> is very similar to the regular HTML <img> element, but <amp-img> must be given an explicit width and height.

For post images, I used this code:

<amp-img width={600} height={400} layout="responsive" alt={post.title} src={`/static/images/${post.id}.jpg`} />

The layout attribute determines how the image behaves. I used responsive so that it automatically resizes to fit the available space.

amp-social-share

<amp-social-share> can be used to display a social share button for various platform providers.

Using it is as simple as loading the required script using Next.js <Head> component on a page where we want to display social buttons:

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

and adding a separate <amp-social-share> element for every social platform that we want to include:

<div className="social-share">
  <amp-social-share type="twitter" width="45" height="33" />
  <amp-social-share type="linkedin" width="45" height="33" />
  <amp-social-share type="pinterest" width="45" height="33" />
  <amp-social-share type="tumblr" width="45" height="33" />
  <amp-social-share type="email" width="45" height="33" />
</div>

Other AMP Components

In this example I used only a couple of components, but the AMP component catalog is pretty large.

With this quick experiment I wanted to see if it's possible to create a dynamic and interactive website using only AMP components and it is. But for a production website I'd probably use the Hybrid mode and implement all interactive features with React, while having simpler AMP pages.