How to create an AMP page for your dynamic content in Rails
Google AMP is here. And it's great!
AMP is a new standard to create faster mobile pages, built on top of HTML, that allows instant page load.
How to setup an AMP page for user generated articles in Rails? Here's my tutorial!
First of all, I created an example repository here. I'm using it as reference for this article.
Introduction
Let's start with a Model!
In my example we have an Article
model, that stores a title
and a content
. The content can store some HTML generated by a WYSIWYG editor, like Tinymce
.
class Article < ActiveRecord::Base
belongs_to :user
validates :title, :content, presence: true
end
And here's it's controller:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
The view:
<h1><%= @article.title %></h1>
<p><%= @article.content.html_safe %></p>
and it's router:
resources :articles, only: :show
Not more than a standard template for a rails app.
Now, what we want is to create an alternative view for this article page, that follows the AMP standard.
Define a new mime type
An easy way to create a new view for existing pages, without changing the controller, is to define a new mime type.
For example, we want that /articles/1
loads our standard article. Instead, loading /articles/1.amp
will load it's AMP version.
First of all, let's create our mime type, adding it to our config/initializers/mime_types.rb
:
Mime::Type.register 'text/html', :amp
Now, let's create a new layout and a new view.
app/views/layouts/application.amp.erb:
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<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>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>
</head>
<body>
<div class="amp">
<%= yield %>
</div>
</body>
</html>
As you can see, it's substantially the default AMP HTML template, found here. I injected also amp-iframe
and amp-youtube
custom tags, since my articles could contain embedded videos. Moreover I defined the canonical link rel, that refers to my non-AMP version of the page. This is done simpy using url_for
method, passing the html
format.
Ok! Now we can define our custom view for this article in app/views/articles/show.amp.erb
Work with custom css
AMP requires to include only embedded css, not external <link>
tags. Here's a trick to continue using our assets pipeline and the compiled css in our view.
First of all, let's create a new sass file under app/assets/stylesheets/amp/application.scss
:
body {
...some styles here...
}
Now, let's register it in the precompilation, by adding this line to our config/application.rb
file.
config.assets.precompile << 'amp/application.scss'
This says to rails to compile it as a file, instead of bundling inside our standard application.css.
Now, let's add this in our layout <head>
:
<% if Rails.application.assets && Rails.application.assets['amp/application'] %>
<style amp-custom><%= Rails.application.assets['amp/application'].to_s.html_safe %></style>
<% else %>
<style amp-custom><%= File.read "#{Rails.root}/public#{stylesheet_path('amp/application', host: nil)}" %></style>
<% end %>
This simply copies all the sass compiled in our view.
There are two cases:
* In development, we can use the Rails.application.assets
helper, that contains the sass compiled data of our files.
* In production, unfortunately, this variable is nil
, but we can read
the compiled file from the public/assets
folder. Note that I use stylesheet_path
with host: nil
. This is because, normally, it appends the host name to the path.
Rendering article content
Now we have a big problem. In our standard view, we could simply print our @article.content
in a DOM, without any change.
AMP, instead, has many limitations on allowed tags. Moreover, there are also some tags that requires to be changed a bit to work in AMP. For example, an "img" tag becomes "amp-img". The same is for iframes.
How we can deal with this?
Well, my solution was to implement a custom scrubber
for the ActionView built-in sanitize
method.
It uses a best-effort approach: It tries to convert as much as it can from the original DOM, and strips the unreadable parts, in order to make AMP page valid.
Here's how it looks like for now:
class AmpScrubber < Rails::Html::PermitScrubber
TAG_MAPPINGS = {
'img' => lambda { |node|
if node['width'] && node['height']
node.name = 'amp-img'
node['layout'] = 'responsive'
node['srcset'] = node['src']
else
node.remove
end
},
'iframe' => lambda { |node|
find_parent(node).add_child(node)
node['src'] = node['src'].gsub(%r{^(\/\/|http:\/\/)}, 'https://')
url = URI(node['src'])
node['layout'] = 'responsive'
if url.host.include?('youtube.com')
node.name = 'amp-youtube'
node['data-videoid'] = node['src'].match(%r{(\/embed\/|watch?v=)(.*)})[2]
node.remove_attribute('src')
else
node.name = 'amp-iframe'
end
}
}.freeze
def initialize
super
@tags = %w(a em p span h1 h2 h3 h4 h5 h6 div strong s u br blockquote)
@attributes = %w(style contenteditable frameborder allowfullscreen)
end
def self.find_parent(node)
node = node.parent while node.parent
node
end
protected
def scrub_attribute?(name)
!super
end
def scrub_node(node)
if node.name.in?(TAG_MAPPINGS.keys)
remap_node! node, TAG_MAPPINGS[node.name]
else
super
end
end
def remap_node!(node, filter)
case filter
when String
node.name = filter
when Proc
filter.call(node)
end
end
end
And here's how to use it. Just create a file under app/views/articles/show.amp.erb
<h1><%= @article.title %></h1>
<div>
<%= sanitize @article.content, scrubber: AmpScrubber.new %>
</div>
You can check here the resulting view:
http://amp-example.herokuapp.com/articles/1.amp
Refer our AMP page from our standard one
Now we have our AMP page. How to say Google to index it? Well, following the docs, we just have to add a few <link rel>
tags in our main page head.
Here's how to edit our layout.html.erb
:
<link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
<link rel="amphtml" href="<%= url_for(format: :amp, only_path: false) %>" >
Conclusion
We have seen how to create an AMP version for our dynamic contents.
You can find the complete example on github.
Let me now if you have ideas, suggestions or concerns!
Written by ProGM
Related protips
5 Responses
Is there any sense integrating AMP into a site if there's already turbolinks enabled? Isn't that kind of the similar thing? I mean AMP requires a lot of work - adding image/video dimensions, refactoring all the css into specific layout fractions etc.
@banesto Not really. AMP pages are not like common "mobile sites". They usually get a great boost on Google from mobile users, since it shows AMP pages on the top of all search results.
Moreover they are instant to load, since google caches it in its servers ;) I think it's a great tool to implement, but they are not an alternative to a mobile compatible website.
Thanks for the nice tutorial.
I implemented this earlier successfully in a Rails 4 app. Now I upgraded it to Rails 5 with Turbolinks.
The problem is that the registered AMP mimetype becomes the default when navigating the site with turbolinks. This should be HTML.
Any suggestions how to fix this?
Not sure what the configuration difference that made a difference, but when I followed your steps I ended up with amp stylesheets that were passing validation locally but not when deployed. After a few hours of being dumb and not noticing the validator kept spitting back errors around encoded quotation marks, I figured out the solution for my problem: adding .html_safe to this line here:
<style amp-custom><%= File.read "#{Rails.root}/public#{stylesheet_path('amp/application', host: nil)}" %></style>
a la
<style amp-custom><%= File.read("#{Rails.root}/public#{stylesheetpath('amp/application', host: nil)}").html_safe %></style>
I hope that saves someone the trouble I ran into!
I hit a huge gotcha. We wanted to make a single page amp'ified and then move to a more generalized layout. We simply added Mime::Type.register "text/html", :amp
to mimetypes.rb thinking it was adding an additional format. However, this wipes out the original "text/html" mime type in Rails and runs requests through amp layouts. If you don't create the .amp.erb layout, it will give 500 error with ActionView::MissingTemplate exception. Instead, you want `Mime::Type.registeralias "text/html", :amp`.