読者です 読者をやめる 読者になる 読者になる

Putting together AMP and Rails

(日本語版はこちら)

Hello!, this is Oskar from the MERY backend team, very nice to meet you!.

Recently we released the AMP version of the MERY article page, and we thought it was a good idea to share why and how we did it. This is the first post we write in English and that is my fault, please bear with me!

AMP? So... what is that, exactly?

Pretty much a new Google invention. It stands for Accelerated Mobile Pages and was launched on February 2016, so this is a very recent one. The main idea behind that pretentious name is trying to come with some standards for coding smartphone sites that loads and gets ready as fast as they possibly can. Makes totally sense, I mean nowadays smartphone sites are very similar in terms of functionality to their big desktop brothers, but most of the time these features are barely used while are consuming bandwidth and resources to load and get in order, resulting in quite performance degradation.

Apple and Facebook thought about this too with their own solutions but AMP, even if is promoted by Google, remains as open-source project, and there are also a couple of invaluable advantages that convinced us to implement it:

  • The AMP page is supposed to load in no time resulting in a great improvement in terms of user experience.
  • When searching from a smartphone, Google will display your pages inside a nice mobile news carousel. User will be able to swipe between articles from different pages and they appear just instantly. This is quite amazing, check it out:

AMP demo

It is integrated with the main search engine, so you can go ahead and try some keywords in the Google page (remember to access from smartphone user agent).

The basic rules

AMP is not a framework. Well technically it is a framework, but doesn't feel like it, is more like a subset of HTML. This means you still get to code more or less the same HTML you already know, but you have to respect some rules and if you do, they ensure your page will be faster than ever.

Before diving in how we tricked Rails to give us the markup we need, let's figure out what these rules are:

No Javascript

Yeah, you read it right: if you want to become an AMP Jedi, forget all you know about Javascript my young Padawan. That's it, really: you can't use Javascript. This seemed to be a tough and big one but wasn't that hard at the end. AMP provides components for the most common functionalities powered by Javascript: analytics, ads, embed video and so on... no need to panic like I did !!

CSS is now inline and must be thin

All the style code has to go inlined between an <style amp-custom> tag inside the <head> section of the page. Moreover, the CSS code is limited to 50Kb, Google thinks that's more than enough for rendering a pretty site. Oh, and you can't use !important, so you must be really sure you are coding it well (I always though that !important shouldn't be used in the first place but we all know we ended using it a lot anyways).

No forms, no inputs

AMP is supposed to just provide information to be read, mostly articles, so they thought forms and inputs are out of the scope. You still get to use buttons, if you need to.

<img> tags are now called <amp-img>

And this is not just a tag rename: you need to specify the size of the image beforehand. There are some options you can use for setting how you want the image to be render using the layout attribute. This ended up being one of the difficult ones, will get into that later.

<img src="image.png"/><amp-img src="image.png" height="250" width="200" layout="responsive"> </amp-img>

The application/ld+json script tag

Actually this is the only one script that can go in your AMP page. It contains a JSON object with information about your content. Be really careful with this one, our pages weren't indexed and after some headaches, we realized it was because of this. Be sure to check the specifications, the one giving us big time was the size of the image which was not big enough.

 <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "Article",
      "mainEntityOfPage": "<%= url_for(only_path: false) %>",
      "headline": "<%= @article.title %>",
      "datePublished": "<%= @article.public_at.utc.iso8601 %>",
      "dateModified": "<%= @article.updated_at.utc.iso8601 %>",
      // [...] Be sure to fill all!
   }
</script>

The setup

It is covered in more detail here, but long history short: you need to render a HTML file (no side JS or CSS files) with the following structure:

<!doctype html>
<html ⚡>
  <meta charset="utf-8">
  <link rel="canonical" href="my-non-amp-index.html">
  <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
  <script type="application/ld+json">
  {
    [... json object with information about your content ...]
  }
  </script>
  <script async src="https://cdn.ampproject.org/v0.js"></script>
  <style amp-boilerplate>
    [... amp basic rules, basically a copy paste...]
  </style>
  <style amp-custom>
    [... your css inlined here ...]
  </style-amp>
  <body>
    [... your html here ...]
  </body>
</html>

Then if you open your page in Chrome appending #development=1 at the end of the URL, then you will know everything is in peace with AMP if you see the following message in the console:

AMP validation succesfull

(aaah, we love that message)

Our goal

Knowing what we know now, we can say that our main goal was trying to render pretty much the same layout we have for smartphones but cleaning the code to the maximum so we are under the rules: no JS, minimum needed CSS and image tags who knows exactly how they have to render themselves.

Like most common Rails setups, at MERY we have two application templates: PC and smartphone. Then, each view, including partials, has its own pc or smartphone version as well.

Our first approach was just replicate the smartphone version and remove everything non-amp-friendly. But then we realized we would need to maintain three versions of the same page, and the article page is pretty much the main of our service so that's the one we are improving and updating the most.

Finally what we did was reusing smartphone styles, views and partials as much as possible. When this was not possible or didn't make much sense, then we created an amp version.

Allow me to go through the steps we took for amp-rizing ourselves:

New mime type, route and layout

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

With this, we are telling Rails there is a new format to be taken into account. There are two good reasons:

  • We can name our view files with .amp.erb and they will be rendered automatically.
  • We can use request.format.amp? inside smartphone templates for deciding what to display.
- config/routes.rb
get "/articles/:id/amp" => "articles#show_amp",  as: :articles_amp, format: :amp

Any route will work here, we just chosen maintaining the same article url with /amp for our new version.

- views/amp/layouts/application.amp.erb

The smartphone version of this file contains too many elements we can't use in amp right away: links to javascript and css files, embedded js for things like Google Analytics, Facebook, Twitter setups including metatags and external libraries....

If there is one file whose amp version is justified, is the layout one without any doubt.

After cleaning all that unusable code, we came out with more or less the amp template described here.

- views/amp/articles/show_amp.amp.erb

And this is the view file which will render after the action show_amp we added in the ArticlesController. We decided also to create a different action because there are many things loaded from database that won't be shown in the much more simplified amp version. I will go through the details later, but here we removed code related to SNS buttons, inline styles, image resizing javascript, some product carousel, and again code for Google Analytics and Ads.

The canonical and amphtml tags

This is how Google will know your AMP page exists, you must link each version in the following way:

  • AMP template, link to the original version with: <link rel="canonical" href="http://example.com/articles/123456" >
  • "Normal" version, link to AMP version with: <link rel="amphtml" href="http://example.com/articles/123456/amp" >

As we are using Rails, this can be done using the URL helpers:

  • AMP: <link rel="canonical" href="<%= article_url(@article) %>" >
  • "Normal": <link rel="amphtml" href="<%= url_for(only_path: false) %>/amp" >

Reusing the styles

At this point, the basic setup is done, not much trouble here: going to /amp should render the show_amp template through the application.amp layout. Well, styles are not in place, but the page should appear.

From here is when it gets tricky, basically we have two things to do:

  • Find a way of reusing the SCSS code we already have for smartphone, but sliced to the very minimum version
  • At the same time, grab that generated CSS file, compress and inject the code directly inside the <style amp-custom> tag placed in the head section of the application.amp.erb layout file.

For the first part, we created an alternative version of the main.scss file and we called it main_amp.scss. That file contains a lot of @import directives that builds the final css file. In the main_amp.scss file we just left the essentials:

//
// Utils
//
@import "utils/sp_sprites";

// =====================================
// Header/Footer
// =====================================
@import "modules/sp/header";
@import "modules/sp/footer";

// =====================================
// Parts(使い回せるパーツ)
// =====================================
@import "modules/mery_font";
@import "modules/amp/layout";
@import "modules/sp/typography";
@import "modules/sp/buttons";
@import "modules/sp/navigation";
@import "modules/sp/headerBanner";
@import "modules/amp/article";
[...]

As the final size was way bigger than the 50Kb limit, we also created some amp versions of big style files like layout and article, leaving just the minimum until the size was below the limit (in the previous code, /sp stands for smartphone).

Now the second part: inject this in the template file. As per Rails standards, we are using the stylesheet helper in our layout:

<%= stylesheet_link_tag 'main_amp' %>

Which will link to the compiled compressed version of the stylesheet:

<link rel="stylesheet" href="/styles/main_amp.26d3d0d3.css"/>

We want the same but instead of referencing it, we want to inject it directly into the template. If we know beforehand the name of the CSS file, then something like this will do:

  <style amp-custom>
     <%= File.read("main_amp.css").html_safe %>
  </style>

But the file name changes every time is regenerated by the assets pipeline, even more, the file is generate and uploaded to some CDN. So the next step is to figure out where and what is the name of the final css file and luckily for us, this can be done using the stylesheet_url helper.

This is what we ended having in our application.amp.erb:

<style amp-custom>
  <%= style_amp_css %>
</style>

And the style_amp_css code, in a helper, looks like this:

def style_amp_css
  css_route = stylesheet_url('main_amp.css')
  match = /href\s*=\s*"([^"]*)"/.match(css_route)
  if match
    css_url = match[1]
    css_uri = URI(css_url)
    original_css = Net::HTTP.get(css_uri)
    original_css.gsub('@charset "UTF-8";', '').gsub('!important', '').force_encoding("UTF-8").html_safe
  end
end

It gets the href value, retrieves the remote content and does three things needed for the AMP validator:

  • Removes @charset "UTF-8" line added automatically by the sass parser. AMP hates it.
  • Removes the !important directive which is, well, important for our shared smartphone template css. We made a few adjustments so the AMP layout also looks well without them (mostly writing more specific rules)
  • Adds the UTF-8 encoding to the file, this is something Net::HTTP doesn't do well, so we had to do it ourselves.

The first 20 times we run this we got a CSS over 50Kb limit error, after cleaning the code to the maximum and compressing it, you can believe us there was a big smile in our faces when we saw the never desired enough message: AMP validation succesfull

Reusing the partials

Apart from the application.amp.erb and show.amp.erb files, we wanted to reuse as much as possible the code we have for the smartphone template, at the end, our goal was showing an AMP version of the same smartphone design. But it has to make sense, I mean, we don't want a view filled with if request.format.amp? conditionals that makes the code difficult to read and maintain. I think we did a good job there keeping a good balance, here is how we did it:

  • Created a sp_image_tag helper which will render img or amp-img on each case. Will go with this in more detail in the next section. Changed all the rails image_tag calls to sp_image_tag inside the shared views.
  • From AMP view we rendered smartphone views using the following syntax:
<%= render partial: "articles/header", formats: [:html], locals: {user: @user } %>

The image tag helper

The next step in our road is dealing with images. Instead of using <img> we need to use a new tag called <amp-img>. That is pretty easy, the hard part is that we need to tell AMP the size of the image. Most of the time we left that to CSS with something like width: 100% which renders the image with the correct aspect ratio, even more, if we specify the height and width in the img tag, the rendered image will normally be much bigger breaking the layout.

In AMP tags we have an attribute called layout that gives us some power over the way elements renders. It is very well explained here. Basically, we want the big images to be render with layout="responsive" attribute which will tell AMP to display the bigger image that fits in the layout without losing the aspect ratio. If the image is small, then we don't need a layout attribute at all, just render the image as it is.

So at this point we need to code some helper called sp_image_tag that:

  • Outputs img or amp-img tag depending on request.format.amp? condition
  • Adds width and height always in the case of amp-img, and adds layout="responsive" if the image is big.
  • Adds width and height in standard img for smartphone if needed too

Luckily for us we have in our database the width and height of most of the images we show, so here is the code:

def sp_image_tag(src, options={})
  if request.format.amp?
    options[:width]  = options[:ampwidth]  if options[:ampwidth]
    options[:height] = options[:ampheight] if options[:ampheight]
    [:ampwidth, :ampheight, :size].each{|k| options.delete(k)}
    options[:layout] = "responsive" if options[:width].to_i >= 250
    amp_image_tag(options.merge!(src: src)) // Will go through this one later
  else
    [:layout, :ampwidth, :ampheight].each{|k| options.delete(k)}
    image_tag(src, options)
  end
end

This in a shared partial view: <%= sp_image_tag "http://example.com/images/first.png", width: 400, height: 400 %>

Will render:

  • Smartphone: <img src="http://example.com/images/first.png" width="400px" height="400px"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="400px" height="400px" layout="responsive"> </amp-img>

This one: <%= sp_image_tag "http://example.com/images/first.png", ampwidth: 200, ampheight: 400 %>

Will render:

  • Smartphone: <img src="http://example.com/images/first.png"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="200px" height="400px"> </amp-img>

And this one: <%= sp_image_tag "http://example.com/images/first.png", ampwidth: 400, ampheight: 400, width: "100%" %>

Will render:

  • Smartphone: <img src="http://example.com/images/first.png" width="100%"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="400px" height="400px" layout="responsive"> </amp-img>

Pretty much we have all the cases covered. But there is still another helper called amp_image_tag used inside the previous code:

def amp_image_tag(options={})
  content_tag("amp-img",
    "<amp-img fallback src='http://example.com/images/noimage2.png')}' width=350 height=350 layout='responsive'></amp-img>".html_safe,
    options.merge!(noloading: ""))
end

That one renders the amp-img tag itself but also adds a couple of things:

  • Another amp-img tag inside with a fallback attribute. If the image fails to load, then AMP will show the fallback image instead (ensure the fallback image exists!).
  • It adds the noloading attribute. That means it won't show a ajax kind of small loading animation in the image space, this is just a matter of taste, we just didn't like it.

Custom components

_ AMP has some components that can be used right away and solves most of the issues we may have because we can't use javascript. Just adding the correct library to the head section of your template and then rendering the tag with the proper attributes will make the trick:

And we are done

Let's see: the /amp route renders the application.amp.erb layout and show_amp.amp.erb views which uses most of the smartphone partials we already had. The CSS is generated reusing existing code and is below 50Kb, there are not inline styles, the markup doesn't contain img tags or any javascript code and the design looks quite good from our iPhone pointing to staging.

Yeah, we are done! :)

Before going to live, be sure to check these:

  • The #development=1 AMP page gives us the wonderful AMP validation succesfull

  • Confirm the Googlebots can access your page! Yeah, I said GooglebotS with 'S' because there are more than one, even a smartphone version!

  • Once more, go through the application/ld+json script tag ensuring it generates all the needed information

  • And don't forget to add to both versions the canonical and amphtml tags that links each other.

  • And go for it!!

If everything is fine, give Google one night, and check if your pages comes here: g.co/ampdemo !!


(日本語版)

¡Hola!

初めまして、MERYのバックエンドチームのオスカルです!

最近、私たちはMERY記事ページのAMPバージョンをリリースしました。そして、「なぜ」と「どのようにして」私たちがAMPに対応したのかを共有することは、よいアイディアだと考えています。この記事は私たちが英語で投稿する最初のもので―それは私のせいなのですが―ちょっとだけ辛抱してお付き合いください!

AMP? えっと、AMPって何?

まあ、Googleの新しい発明品です。Accelerated Mobile Pagesのアクロニムで2016年の2月にローンチされた、とても新しいものです。その派手な名前の裏に隠されたアイディアは、モバイルサイトの読み込みと表示をできるだけ速くするために一種のコーディング規約を定めようとするものです。これはとても有意義なことです。すなわち、今日のモバイルサイトはデスクトップの兄弟と同様の機能を備えていますが、多くの機能はほとんど使われないにも関わらず順調に端末の帯域とリソースを消費して、結果的にパフォーマンスもひどいものになります。

この問題に関してAppleApple NewsFacebookInstant Articlesという独自の解決策を考えています。AMPはGoogleによって推進されてはいますが、オープンソースプロジェクトであり、また、私たちが実装すべきと確信するいくつかの重要な利点があります:

  • AMPに対応したページは瞬時にロードされるので、UXを格段に向上させることができます。
  • スマートフォンから検索した時、Googleのナイスなニュース枠のカルーセルにページを表示してくれる可能性があります。これはとてもクールです。

AMP demo

この機能は既にGoogle検索エンジンに統合されているので、キーワードを入れて試してみることができます(スマートフォンのUser agentでアクセスすることをお忘れなく)。

基本的なルール

AMPはフレームワークではありません。いや、技術的にはフレームワークなのですが、どちらかと言えばHTMLのサブセットという感じを受けます。そのようなわけで、大体は今まで慣れ親しんできたHTMLでコーディングすることができるのですが、いくつかのルールを守る必要があります。そして、そのルールが守られていれば、今までにないくらいあなたのサイトは高速になります。

私たちがどのようにRailsを巧みに操って必要なマークアップを出力させたかを見る前に、それらのルールがどのようなものか把握しましょう。

Javascriptは使えない

その通り!「もし君がAMPのジェダイになりたければ、JavaScriptについての全てを忘れたまえ。若きパダワンよ。」これが真実です。JavaScriptは使うことができません。これはとても辛くて困難なことのように思えますが、結果的にはそれほど難しいことではありませんでした。AMPはJavaScriptでよく使われる機能を備えたコンポーネントを提供してくれています: アナリティクス、広告、ビデオ埋め込みなどなど。私がかつて陥ったようなパニックになる必要はありません!

CSSはインラインで小さいサイズで

全てのCSSコードはheadセクションの<style amp-custom>タグの中にインライン化しなければなりません。さらに、そのサイズは50KBに制限されており、Googleは高品質なサイトを作るにはそれで十分だと考えているようです。ああ、そして、!importantは使うことができません。そのため、本当にきちんとしたコーディングをする必要があります。(もちろん、!importantは使うべきではないと私も常々思っていますが、サイトが拡大するにつれて結局たくさん使ってしまうものですよね)。

フォームやinputは使えない

AMPは記事などの読むための情報を提供することを前提としているため、フォームや入力といったものは対象外と考えられているようです。ただし、必要であればボタンを使うことはできます。

<img>タグの代わりに<amp-img>を使う

これはただ単にタグの名前が変わるだけではなく、画像のサイズを事前に指定する必要も出てきます。また、画像がどのようにレンダリングされるかを指定するためのオプションをlayout属性で指定することができます。これは結果的に難しい問題の一つでした(詳細は後述します)。

<img src="image.png"/><amp-img src="image.png" height="250" width="200" layout="responsive"> </amp-img>

application/ld+jsonスクリプトタグ

これがAMPページに存在する唯一のスクリプトで、コンテンツに関する情報を含んだJSONオブジェクトです。このタグはとても注意深く扱って下さい。過去、私たちのページはいつまでもインデックスされず、頭痛に悩まされ続けた末、これが原因だと気付きました。仕様をきちんと確認して下さい。私たちが最も悩まされた問題は、画像サイズが十分大きくないことによるものでした。

 <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "Article",
      "mainEntityOfPage": "<%= url_for(only_path: false) %>",
      "headline": "<%= @article.title %>",
      "datePublished": "<%= @article.public_at.utc.iso8601 %>",
      "dateModified": "<%= @article.updated_at.utc.iso8601 %>",
      // [...] Be sure to fill all!
   }
</script>

基本的な設定

詳しいことは全てここで書かれていますが、手短に言えば、以下の構造を持ったHTLMファイル一つが必要です(他にJSやCSSは要らないです)。

<!doctype html>
<html ⚡>
  <meta charset="utf-8">
  <link rel="canonical" href="my-non-amp-index.html">
  <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
  <script type="application/ld+json">
  {
    [... json object with information about your content ...]
  }
  </script>
  <script async src="https://cdn.ampproject.org/v0.js"></script>
  <style amp-boilerplate>
    [... amp basic rules, basically a copy paste...]
  </style>
  <style amp-custom>
    [... your css inlined here ...]
  </style-amp>
  <body>
    [... your html here ...]
  </body>
</html>

そして、このページのURLに#development=1を付けてGoogle Chromeで開けば、AMPによって平和が訪れたことを知ることになるでしょう(ただし、以下のメッセージがコンソールに出た場合)。

AMP validation succesfull

(aaah, we love that message)

私たちの目的

AMPの基本的なルールを知ってもらった今、私たちの目的を伝えることができます。それは、私たちのモバイルアプリと同じ品質とレイアウトで、AMPのルール―JavaScriptなし、必要最小限のCSS、自分がどうやって描画されるか知っている画像―に最大限則ったクリーンなコードを書くことです。

多くのRailsアプリがそうであるように、MERYでもPCとスマートフォン向けの二つのアプリケーションテンプレートがあります。そして、個々のビュー(パーシャルテンプレートも含む)もPCとスマートフォンそれぞれのバージョンがあります。

私たちの最初のアプローチは、単にスマートフォン向けのテンプレートをコピーして、AMPフレンドリーでない要素を削除するというものでした。しかし、それによって私たちは同じページの三つのバージョンを管理する必要があることに気づきました。また、記事ページは私たちのサービスのメインコンテンツなので、頻繁に改善やアップデートが行われます。

最終的に私たちがやったことは、スマートフォン向けのスタイル、ビュー、パーシャルを可能な限り再利用することでした。そして、再利用が不可能だったり、あまりにも難解になった時は、AMPバージョンを作りました。

少し長くなりますが、私たちがMERYをAMP化した際のステップを順を追って説明します。

新しいMIMEタイプ、ルーティング、レイアウト

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

この設定によって、Railsに新たに対応すべきフォーマットを指定しています。これには二つの利点があります。

  • ビューファイル名を.amp.erbとすることができて、自動的にレンダリングされます。
  • スマートフォン用テンプレートでrequest.format.amp?というメソッドを使うことができるようになって、出し分けができるようになります。
- config/routes.rb
get "/articles/:id/amp" => "articles#show_amp",  as: :articles_amp, format: :amp

ルーティングはこのように書くことができます。私たちは記事ページのURLに単に/ampをつけたものをAMPバージョンとしました。

- views/amp/layouts/application.amp.erb

既存のスマートフォン向けのレイアウトファイルはAMPで使えない要素で溢れていました: JavaScriptCSSファイルへのリンク、Google AnalyticsFacebookTwitterの埋め込みスクリプトやメタタグ、外部ライブラリなどなど。

もしAMPバージョンのファイルとして一つだけ存在が許されるものがあるとしたら、それは明らかにレイアウトファイルでしょう。

全ての使えないコードを綺麗にした後、ここで示されているものとほとんど同じものができました。

- views/amp/articles/show_amp.amp.erb

そして、これが私たちがArticlesControllerに追加したshow_ampアクションによって描画されるビューファイルです。私たちは新しいアクションを定義することに決めました。なぜなら、既存のアクションにはシンプルなAMPバージョンでは使われることのないオブジェクトが多数あったからです。詳細は後述しますが、ここではSNSボタン、インラインスタイル、画像の調整スクリプト、いくつかの商品カルーセルGoogle Analyticsと広告を削除しました。

canonicalamphtmlタグ

これがGoogleがAMPページがあるかどうかを知るための方法です。以下のように、それぞれのバージョンをリンクする必要があります。

  • AMPのテンプレートでノーマルバージョンにリンクするには: <link rel="canonical" href="http://example.com/123456" >
  • ノーマルバージョンでAMPバージョンにリンクするには: <link rel="amphtml" href="http://example.com/123456/amp" >

私たちはRailsを使っているので、これはURLヘルパーで達成することができます。

  • AMP: <link rel="canonical" href="<%= article_url(@article) %>" >
  • ノーマル: <link rel="amphtml" href="<%= url_for(only_path: false) %>/amp" >

スタイルの再利用

ここまでで基本的な設定は終わりで、大きなトラブルもありませんでした。/ampにアクセスするとapplication.ampレイアウトを通してshow_ampテンプレートが描画されます。もちろん、まだスタイルはあたっていませんが、ページは表示されます。

ここからはちょっとトリッキーになります。基本的に、私たちのやるべきことは二つあります。

  • 現在のスマートフォン向けのSCSSを再利用しつつ、かつとても小さくします。
  • 同時に、生成されたCSSファイルを圧縮して、application.amp.erbheadセクション内の<style amp-custom>タグに直接注入します。

最初の段階では、main.scssのAMPバージョンのファイルmain_amp.scssを作りました。このファイルは最終的なCSSを出力するための多くの@importディレクティブを含んでおり、main_amp.scssでは本質的なものだけを残しました。

//
// Utils
//
@import "utils/sp_sprites";

// =====================================
// Header/Footer
// =====================================
@import "modules/sp/header";
@import "modules/sp/footer";

// =====================================
// Parts(使い回せるパーツ)
// =====================================
@import "modules/mery_font";
@import "modules/amp/layout";
@import "modules/sp/typography";
@import "modules/sp/buttons";
@import "modules/sp/navigation";
@import "modules/sp/headerBanner";
@import "modules/amp/article";
[...]

最終的なサイズが50KBの制限を超えてしまったため、特に大きなlayoutarticleのスタイルファイルのAMPバージョンを作り、サイズが制限内に収まるまで不必要なものをそぎ落としました(上記のコードでは、/spスマートフォンの略語です)。

二段階目では、これをテンプレートファイルに注入しました。Railsに従って、私たちはスタイルシート用のヘルパーを使っています。

<%= stylesheet_link_tag 'main_amp' %>

そして、これがコンパイルされたCSSへのリンクになります。

<link rel="stylesheet" href="/styles/main_amp.26d3d0d3.css"/>

私たちは同じCSSがほしいわけですが、参照するのではなく、テンプレートに直接注入して欲しいのです。もし私たちがCSSのファイル名を事前に知っていたら、こんな感じで実現できるでしょう。

<style amp-custom>
  <%= File.read("main_amp.css").html_safe %>
</style>

しかしながら、ファイル名はアセットパイプラインによって再生成されますし、さらにCDNにアップロードされます。よって、次のステップは最終的なCSSファイルの場所と名前を知ることです。幸運なことに、これはstylesheet_urlヘルパーを使うことで実現できます。

最終的に、私たちのapplication.amp.erbはこのような形になりました。

<style amp-custom>
  <%= style_amp_css %>
</style>

style_amp_cssはヘルパーとしてこう定義されています。

def style_amp_css
  css_route = stylesheet_url('main_amp.css')
  match = /href\s*=\s*"([^"]*)"/.match(css_route)
  if match
    css_url = match[1]
    css_uri = URI(css_url)
    original_css = Net::HTTP.get(css_uri)
    original_css.gsub('@charset "UTF-8";', '').gsub('!important', '').force_encoding("UTF-8").html_safe
  end
end

このメソッドはhref属性の値を取って、リモートのCSSファイルを取得した後にAMPのために三つのことを行います。

  • SASSパーサーが自動的に付ける@charset "UTF-8"の行はAMPに嫌われているので削除します。
  • !importantディレクティブ―私たちの共通CSSにおいて重要なもの―を削除します。そして、この指示文がなくてもAMPレイアウトがきれいになるように少し調整を加えました(大体は、より限定されたルールを書きました)。
  • ファイルをUTF-8エンコードしました。これはNet::HTTPがうまくできないため、自分たちでやる必要がありました。

最初の20回くらいはCSSが50KB制限を超えているエラーに陥りましたが、できるだけコードをきれいにしたり圧縮した末に、私たちの顔に大きな微笑みをもたらしたあのずっと待ち望んでいたメッセージが現れました。

AMP validation succesfull

パーシャルテンプレートの再利用

一旦application.amp.erbshow_amp.erbファイルのことを忘れて、私たちが成し遂げたいことを思い返してみると、スマートフォンのテンプレートを可能な限り再利用しつつ同じデザインのAMPバージョンを作りだすことでした。そして、それは納得できる形である必要があります。すなわち、私たちはビューテンプレートをif request.format.amp?といった条件文で満たされた、読みにくくメンテしにくいものにはしたくありません。私たちはこの点においてよいバランスを取ることができたと考えていて、そのために私たちがしたことが以下の二つです。

  • imgamp-imgタグを適切に描画するsp_image_tagヘルパーを作りました(詳細については次の節で説明します)。そして、Railsimage_tag呼び出しをsp_image_tagに書き換えました。
  • AMPのビューからは、以下のようにスマートフォンのビューを描画するようにしています。
<%= render partial: "articles/header", formats: [:html], locals: {user: @user } %>

画像タグのヘルパー

私たちの道の次のステップは画像をどうにかすることです。<img>タグを使う代わりに、<amp-img>タグを使う必要があるのです。これは実際とても簡単でした。難しいのは、画像のサイズをAMPに教える必要がある点でした。これまでほとんどの場合、私たちはサイズの問題をCSSに任せてきました: width: 100%と記述すれば、画像をレイアウトに合わせて正しいアスペクト比で表示してくれます。そして、もしheightwidth<img>タグに指定すると、画像は通常大き過ぎるためレイアウトが崩れてしまいます。

AMPタグには、layout属性という私たちに要素のレンダリングを制御する力を与えてくれるものが在ります(ここで詳しく説明されています)。基本的には、私たちは大きな画像にlayout="responsive"属性を付け、AMPに画像のアスペクト比を保ちつつレイアウトにフィットさせるよう指定したいのですが、画像が小さい場合はlayout属性は付けずにそのまま表示させたいのです。

そのため、以下の要件を満たすsp_image_tagヘルパーが私たちには必要でした。

  • request.format.amp?条件に従ってimgまたはamp-imgタグを出力する
  • amp-imgの時は常にwidthheightを付け、また、画像が大きい場合はlayout="responsive"を付ける
  • 普通のimgタグにも必要であればwidthheightを付ける

幸運なことに、私たちが扱うほとんどの画像の幅と高さはデータベースに保存されていました。そして、sp_image_tagは以下のようになりました。

def sp_image_tag(src, options={})
  if request.format.amp?
    options[:width]  = options[:ampwidth]  if options[:ampwidth]
    options[:height] = options[:ampheight] if options[:ampheight]
    [:ampwidth, :ampheight, :size].each{|k| options.delete(k)}
    options[:layout] = "responsive" if options[:width].to_i >= 250
    amp_image_tag(options.merge!(src: src)) // Will go through this one later
  else
    [:layout, :ampwidth, :ampheight].each{|k| options.delete(k)}
    image_tag(src, options)
  end
end

とある共通のパーシャルビューテンプレートのこの行は、

<%= sp_image_tag "http://example.com/images/first.png", width: 400, height: 400 %>

以下のようになります。

  • Smartphone: <img src="http://example.com/images/first.png" width="400px" height="400px"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="400px" height="400px" layout="responsive"> </amp-img>

また、こちらは、

<%= sp_image_tag "http://example.com/images/first.png", ampwidth: 200, ampheight: 400 %>

こうなります:

  • Smartphone: <img src="http://example.com/images/first.png"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="200px" height="400px"> </amp-img>

そして更に、

<%= sp_image_tag "http://example.com/images/first.png", ampwidth: 400, ampheight: 400, width: "100%" %>

こうです:

  • Smartphone: <img src="http://example.com/images/first.png" width="100%"/>
  • AMP <amp-img src="http://example.com/images/first.png" width="400px" height="400px" layout="responsive"> </amp-img>

これでほとんど私たちの対応は終わりました。あと一つだけ、sp_image_tagで使われていたamp_image_tagヘルパーが残っていました。

def amp_image_tag(options={})
  content_tag("amp-img",
    "<amp-img fallback src='http://example.com/images/noimage2.png')}' width=350 height=350 layout='responsive'></amp-img>".html_safe,
    options.merge!(noloading: ""))
end

これはamp-imgタグを返しますが、合わせていくつかのことをしています:

  • fallback属性の付いた別のamp-imgタグを内包しています。もし画像のロードが失敗した場合に、AMPではこのfallback画像が代わりに表示されます(fallback画像が存在することをちゃんと確認してくださいね!)
  • noloading属性を付けています。これによって、AJAX的なローディングのアニメーションが出なくなります。これは単なる好みの問題で、私たちの好みには合わなかったということです。

カスタムコンポーネント

AMPはJavaScriptが使えないことによる多くの問題を簡単に解決するためのいくつかのコンポーネントを用意しています。テンプレートのheadセクションに適切なライブラリを指定して、正しい属性を持ったタグをレンダリングすれば、魔法のようにうまく動きます。

最後に

さて、/ampにアクセスすると、application.amp.erbレイアウトと既存のスマートフォン向けパーシャルテンプレートを多用したshow_amp.amp.erbレンダリングされます。インラインのスタイルはなく、CSSは既存のコードを再利用しつつ50KB以下で再生成され自動的に展開されます。マークアップにはimgタグやJavaScriptコードも入っていません。そして、ステージングに向けたiPhoneから見たデザインはとても素晴らしいものでした!

Yeah, we are done! :)

公開する前に、最後に以下をチェックしましょう。

  • #development=1を付けたAMPページが素晴らしきAMP validation succesfullを表示することを。
  • Googleボット「達」がページにアクセスできることを確認しましょう。そう、Googleボットはスマートフォン向けのものでもいくつかあるのです。
  • もう一度、application/ld+jsonスクリプトタグに必要な情報が全て入っていることを確認しましょう。
  • 通常バージョンとAMPバージョンのcanonicalamphtmlタグが相互にリンクされていることを確認しましょう。
  • リリースしましょう!

もし全てがうまくいったら、Googleボットを一晩だけ待ってあげて、g.co/ampdemoにあなたのページが載っているかをチェックしましょう!

© peroli, Inc.