A short tutorial on how to incorporate Accelerated Mobile Pages (AMP) into your Rails application. This tutorial will also show you how to inline CSS with Webpack and test your AMP pages in production.

TLDR; Take me to the tutorial

What are Accelerated Mobile Pages?

AMP pages are basically web pages on a diet — they're slimmed down versions of regular HTML pages.

AMP pages are a mobile-first search strategy with the primary aim of speeding up page load times on mobile devices. Google crawls your AMP page and stores it in a cache where it can be retrieved extremely quickly.

These pages show up on Google's SERP (Search Engine Results Page) and are preloaded in the background. So when you tap on an AMP page link the page will load in an instant.

Learn more about the AMP project.

What does an AMP page look like?

In addition to designing and building our mobile-first Elastic Teams website, we also spent a little time turning key pages into AMP pages. Here's a demo of the home page:

AMP in action

Are AMP pages worth implementing?

This really depends on the kind of site you have, and whether your budget permits the investment to create an AMP version of your site.

If you're a news publisher or have a lot of frequently visited blog content, then it only seems natural to take the AMP route. Google does give importance to AMP pages when it comes to organic search. So, you could enjoy a 'boost' when it comes to mobile search.

Another deciding factor whether or not to adopt AMP is the page load time of your current site. Google discovered that 53% of mobile users will abandon a site if it takes over 3 seconds to load. More alarmingly, Google's research also revealed that it takes an average of 22 seconds to fully load a page on a mobile device.

Keeping these stats in mind, it's a good idea to first focus on optimising your existing site pages to load fast on mobile devices rather than jumping straight into AMP. It can be argued that AMP should be viewed as the icing on the cake rather than a go-to solution for underperforming mobile sites.

So, does your site suffer from slow loading times? Find out by testing it on Page Speed Insights.

AMPifying your Rails application

While there are a couple of AMP gems that could have been used as basis for this tutorial, these gems are overkill for what we need — which is a simple implementation that avoids adding further dependencies to a Rails project.

This tutorial assumes that you have an existing Rails 6 app to work with and Webpacker gem installed.

1. Add new :amp MIME type

Add a new :amp alias to the text/html MIME type so that AMP templates and partials can be distinctly rendered. Remember to restart your Rails server for the change to take effect.

# config/initializers/mime_types.rb
Mime::Type.register_alias 'text/html', :amp

2. Add a new route for the AMP version of the home page

The root page of your regular HTML site can be accessed via http://localhost:3000/

But, how do you access the AMP root page? The answer is simple: add a new route that responds to the aliased :amp format so that the AMP version of the home page can be accessed via http://localhost:3000/index.amp

Rails.application.routes.draw do
  root to: 'home#index'
  # ensure AMP home page is accessible via http://localhost:3000/index.amp 
  # but not through http://localhost:3000/index or index.html
  get '/index', to: 'home#index', constraints: lambda { |req| req.format == :amp }
end

3. Add a new layout for your AMP application

Next, add a new application.amp.erb layout to your app/views/layouts folder.

Assuming you intend to have regular HTML pages in addition to AMP pages, include a canonical reference to the regular HTML page so that Google can make the correct association when crawling your pages.

<!doctype html>
<%# <html amp> is also valid markup %>
<html >
  <head>
    <title><%= yield :title %></title>
    <meta charset='utf-8'>
    <%# main AMP library %>
    <%= javascript_include_tag 'https://cdn.ampproject.org/v0.js', async: true %>
    <%# include a canonical reference to regular HTML page. %>
    <%# important: nil format ensures that url is rendered as http://localhost:3000/ rather than http://localhost:3000/index %>
    <link rel="canonical" href="<%= url_for(:only_path => false, format: nil) %>">
    <meta name='viewport' content='width=device-width,minimum-scale=1,initial-scale=1'>
    <%# standard AMP styles %>
    <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>
    <%# fallback %>
    <noscript>
      <style amp-boilerplate>
        body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}
      </style>
    </noscript>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

4. Create a new AMP home page

The markup for your regular HTML home page should look something like this:

<% provide(:title, 'Regular page') %>
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

Now create the equivalent AMP page:

<% provide(:title, 'AMP page') %>
<h1>AMP Home#index</h1>
<p>Find me in app/views/home/index.amp.erb</p>

To test the pages, point your browser to:

  • http://localhost:3000 to see the regular HTML page
  • http://localhost:3000/index.amp to see the AMP page
  • http://localhost:3000/index – this should generate an error

To validate your AMP page open your browser at http://localhost:3000/index.amp#development=1 and check the browser console (a hard page refresh might be required). You should see the following:

Powered by AMP ⚡ HTML – Version 2004030010070 http://localhost:3000/index.amp#development=1
validator.js:6501 AMP validation successful.
validator.js:6501 Review our 'publishing checklist' to ensure successful AMP document distribution. See https://go.amp.dev/publishing-checklist

All good so far, except that you will notice that we are using a different view for each format.

This approach will work fine if your content is going to vary considerably between regular HTML and AMP views. However, in most cases your content will be similar, if not the same, so this set-up does not lend itself to easy maintenance as a change in content will demand the modification of two distinct views.

A better approach is to use a single view for both formats.

5. Create a shared view for both regular and AMP formats

To illustrate this, let's create a new About page for both regular and AMP formats:

bin/rails g controller about index

This will create a new controller named app/controllers/about_controller.rb and HTML view named app/views/about/index.html.erb

Find the following line in your config/routes.rb file:

get 'about/index'

and replace it with:

get '/about', to: 'about#index'

Next, open app/controllers/about_controller.rb in your editor and change as follows:

class AboutController < ApplicationController

  layout proc { |controller| controller.request.format == :amp ? 'application.amp' : 'application.html' }
  
  def index
    render 'index', formats: [:html]
  end
  
end

This code dynamically selects the appropriate layout depending on the requested format and renders the regular HTML version of the About page for both formats.

To test this out, try the following:

  • http://localhost:3000/about to see the regular HTML page
  • http://localhost:3000/about.amp#development=1 to see the AMP page and view the console log

Now we're using a single view for both formats — nice!

Since AMP restricts what HTML elements we can publish on our page, we need a mechanism to conditionally show or hide AMP elements on the page. The best way to accomplish this is to use a helper method so that we can dynamically switch-in or switch-out content blocks in our view depending on the requested format.

6. AMP view helper

Firstly, create a new helper named app/helpers/amp_helper.rb containing the following code:

module AmpHelper

  def is_amp?
    request.format == :amp
  end

  def is_html?
    request.format == :html
  end

end

Include the helper module in your app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  include AmpHelper

end

Next, update your About view in app/views/about/index.html.erb with the following markup:

<% provide(:title, 'About - Shared view') %>
<h1>About#index</h1>
<p>Find me in app/views/about/index.html.erb</p>

<% if is_amp? %>
  <amp-img src='https://placekitten.com/600/400' layout='fixed' width=600 height=400>
<% else %>
  <%= image_tag 'https://placekitten.com/600/400', size: '600x400' %>
<% end %>

With any luck you should see a furry friend on these pages:

  • http://localhost:3000/about
  • http://localhost:3000/about.amp#development=1

7. Let Google know about your AMP page

In order to make your AMP pages crawlable by search engines you will need to make a few adjustments to your regular HTML layout. But before we do that, let's add a few helper methods to app/helpers/amp_helper.rb:

module AmpHelper

  def is_amp?
    request.format == :amp
  end

  def is_html?
    request.format == :html
  end

  def ampify_off!
    @ampify = false
  end

  def ampified?
    if @ampify.nil? 
      true
    else
      false
    end
  end

  def canonical_amphtml
    return unless ampified?
    capture do
      tag.link(nil, { rel: 'amphtml', href: url_for(only_path: false, format: :amp) })
    end
  end

end

Then modify the regular HTML application layout app/views/layouts/application.html.erb like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield :title %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%# add canonical page for regular html page %>
    <link rel="canonical" href="<%= url_for(:only_path => false) %>">
    <%# add canonical reference for AMP version of page if require %>
    <%= canonical_amphtml %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

The code on line 10 ensures that the default canonical reference is included in the page. The next line generates the canonical reference to the AMP page. These two lines of code will render the following HTML:

<link rel="canonical" href="http://localhost:3000/">
<link rel="amphtml" href="http://localhost:3000/about.amp">

In the event you do not require an AMP equivalent of your regular HTML page, you can deactivate the AMP format by adding the following method to your controller action or template file: ampify_off!

For example, when used in a controller action:

class ServicesController < ApplicationController

  def index
    ampify_off!
  end

end

Or inside a view template:

<% ampify_off! %>
<% provide(:title, 'Services page') %>
<h1>Services#index</h1>
<p>Find me in app/views/services/index.html.erb</p>

8. Inlining CSS with Webpack

One of the restrictions AMP imposes on our implementation is that all stylesheets must be inlined in the <head> of the page using the following AMP construct:

<style amp-custom>
  /* css code goes here */
</style>

In production, this is easily accomplished using this code excerpt:

<style amp-custom>
  <%= File.read(File.join(Rails.root, "public", asset_pack_path('application.css'))).html_safe %>
</style>

However, replicating the same behaviour in your development environment is a little trickier and requires a small hack.

  1. Open config/webpacker.yml and change the extract_css entry from false to true i.e. extract_css: true
  2. Restart your webpack development server with bin/webpack-dev-server

Now let's add a couple of helper methods that will inline CSS in development and production. Open app/helpers/amp_helper.rb in your editor and add the following code to the module:

require 'open-uri'

module AmpHelper

  def is_amp?
    request.format == :amp
  end

  def is_html?
    request.format == :html
  end

  def ampify_off!
    @ampify = false
  end

  def ampified?
    if @ampify.nil? 
      true
    else
      false
    end
  end

  def canonical_amphtml
    return unless ampified?
    capture do
      tag.link(nil, { rel: 'amphtml', href: url_for(only_path: false, format: :amp) })
    end
  end

  def webpack_inline_css(filename)
    filename = filename + '.css'
    if current_webpacker_instance.dev_server.running?
      open(inline_asset_url(filename)).read.html_safe
    else
      File.read(File.join(Rails.root, "public", asset_pack_path(filename))).html_safe
    end
  end

  def inline_asset_url(name)
    server = current_webpacker_instance.config.dev_server
    protocol = server[:https] ? "https://" : "http://"
    host = server[:public]
    pack = asset_pack_path(name)
    "#{protocol}#{host}#{pack}"
  end

end

Don't forget to include the require 'open-url' on the first line as this is needed to load the CSS pack from the Webpack development server using http.

Next, update the app/views/layouts/application.amp.erb file with the following code:

<!doctype html>
<%# <html amp> is also valid markup %>
<html >
  <head>
    <title><%= yield :title %></title>
    <meta charset='utf-8'>
    <%# main AMP library %>
    <%= javascript_include_tag 'https://cdn.ampproject.org/v0.js', async: true %>
    <%# include a canonical reference to regular HTML page. %>
    <%# important: nil format ensures that url is rendered as http://localhost:3000/ rather than http://localhost:3000/index %>
    <link rel="canonical" href="<%= url_for(:only_path => false, format: nil) %>">
    <meta name='viewport' content='width=device-width,minimum-scale=1,initial-scale=1'>
    <%# standard AMP styles %>
    <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>
    <%# fallback %>
    <noscript>
      <style amp-boilerplate>
        body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}
      </style>
    </noscript>
    <%# custom inline css %>
    <style amp-custom>
      <%= webpack_inline_css 'application' %>
    </style>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

And finally, test your page in your browser by visiting http://localhost:3000/index.amp#development=1. Don't forget to check the browser log for any errors or warnings.

9. Testing your AMP pages in production

Once your AMP pages are live, it's a good idea to test them using the Google Search Console. Simply visit the AMP test site and submit your AMP page (e.g. https://mysite.com/index.amp) to discover any errors or warnings.

Sample AMP Test Results

Moving forward...

As you can see, incorporating basic AMP pages in your Rails application is a pretty straightforward exercise.

The real challenge lies in adapting your site to play nicely with AMP components. You can forget your familiar Javascript frameworks — these are largely unsupported in AMP so you will need to work extra hard to move features found on your regular HTML pages to your AMP pages.

A more robust approach for new projects is to focus on designing and building applications with a mobile-first strategy in mind. By doing so, adopting AMP becomes a much easier and natural undertaking.